[{"data":1,"prerenderedAt":424},["ShallowReactive",2],{"content:\u002Ffiddling\u002Fparser-type-variable-ambiguity":3,"surround:\u002Ffiddling\u002Fparser-type-variable-ambiguity":413},{"id":4,"title":5,"body":6,"categories":388,"date":390,"description":391,"draft":392,"extension":393,"image":394,"meta":395,"navigation":397,"path":398,"permalink":394,"published":394,"readingTime":399,"recommend":394,"references":394,"seo":404,"sitemap":405,"stem":406,"tags":407,"type":411,"__hash__":412},"content\u002Fposts\u002Ffiddling\u002Fparser-type-variable-ambiguity.md","语法分析中类型名-变量名歧义消除",{"type":7,"value":8,"toc":380},"minimark",[9,13,26,29,39,54,57,60,63,92,98,106,112,140,162,165,168,171,175,178,186,193,196,202,205,216,227,230,256,259,277,294,309,313,316,322,325,331,342,353,362,365,371,374],[10,11,12],"h3",{"id":12},"引",[14,15,16,17,21,22,25],"p",{},"由于语法分析阶段不维护符号表，用户自定义类型名（typedef）和普通变量名难以区分。对于函数体中的 ",[18,19,20],"code",{"code":20},"a*b;","​ 语句，既可以解释为 a 乘 b，忽略表达式结果，也可以解释为声明一个类型为 ",[18,23,24],{"code":24},"a*","​ 的变量 b",[14,27,28],{},"同时，由于存在如下语法规则",[30,31,36],"pre",{"className":32,"code":34,"language":35},[33],"language-text","declaration := declaration_specifiers SEMICOLON\ndeclaration_specifiers := type_specifier declaration_specifiers\ntype_specifier := INT\ntype_specifier := typedef_name\ntypedef_name := IDENTIFIER\n","text",[18,37,34],{"__ignoreMap":38},"",[14,40,41,42,45,46,49,50,53],{},"规则 1 多用于无需指定标识符的结构体前向声明。如 ",[18,43,44],{"code":44},"struct Node;","​ 表示前向声明一个 ",[18,47,48],{"code":48},"struct Node","​ 类型。由于该规则的存在，譬如 ",[18,51,52],{"code":52},"int a;","​ 也可能会使用规则 1 规约，符号 a 会被识别为一个用户自定义类型名而非变量名。由于类似的无初始化变量的定义语句容易在源码中大量出现，会导致一份源码会对应较多满足语法规则的 AST。这个问题可以在语义分析阶段通过维护符号表解决，但是在语法分析阶段也可以以较小的代价提前处理，减少语义分析需要处理的 AST 个数",[10,55,56],{"id":56},"思路",[14,58,59],{},"对于这种类型名 - 变量名的歧义，可以在 GLR 的执行过程中或执行后通过维护代价较小的简易符号表的方式对错误的分支进行剪枝。这个符号表至少要维护变量名和用户自定义类型名，同时由于存在深层作用域变量\u002F类型名对浅层作用域符号的遮蔽，这个符号表还需要跟踪符号的作用域",[14,61,62],{},"那么需要做的事情就很简单了：",[64,65,66,70,73,84],"ol",{},[67,68,69],"li",{},"维护符号表，收集类型定义和变量声明",[67,71,72],{},"添加符号时校验同级作用域是否有同名变量声明和类型定义",[67,74,75,76,79,80,83],{},"对所有使用 ",[18,77,78],{"code":78},"primary_expression := IDENTIFIER","​ 规约的节点检查 ",[18,81,82],{"code":82},"IDENTIFIER","​ 是否是一个已经定义且未被遮蔽的变量名",[67,85,75,86,79,89,91],{},[18,87,88],{"code":88},"typedef_name := IDENTIFIER",[18,90,82],{"code":82},"​ 是否是一个已经定义过且未被遮蔽的用户自定义类型名",[14,93,94,95,97],{},"以经典 ",[18,96,20],{"code":20},"​ 为例：",[30,99,104],{"className":100,"code":102,"language":103,"meta":38},[101],"language-c","\u002F\u002F 例 1\ntypedef int a;\nfunc test_func()\n{\n\ta*b;\n}\n","c",[18,105,102],{"__ignoreMap":38},[30,107,110],{"className":108,"code":109,"language":103,"meta":38},[101],"\u002F\u002F 例 2\ntypedef int a;\nfunc test_func()\n{\n\tint a;\n\ta*b;\n}\n",[18,111,109],{"__ignoreMap":38},[14,113,114,115,117,118,120,121,123,124,127,128,130,131,134,135,127,137,139],{},"例 1：解析到 ",[18,116,20],{"code":20},"​ 时，符号表的 1 级作用域（最外层）中有一个自定义类型 a，2 级作用域（函数）中无符号。对于将 ",[18,119,20],{"code":20},"​ 解析为声明一个类型为 ",[18,122,24],{"code":24},"​ 的变量 ",[18,125,126],{"code":126},"b","​ 的 AST，a 会使用 ",[18,129,88],{"code":88},"​ 进行规约，检查符号表发现 a 确实是一个声明在 1 级作用域的自定义类型名，于是该 AST 会被保留；对于将其解析为变量 ",[18,132,133],{"code":133},"a","​ 乘以变量 ",[18,136,126],{"code":126},[18,138,78],{"code":78},"​ 进行规约，检查符号表发现表中并没有变量 a，于是该 AST 会被抛弃",[14,141,142,143,145,146,120,148,123,150,127,152,154,155,134,157,127,159,161],{},"例 2：解析到 ",[18,144,20],{"code":20},"​ 时，符号表的 1 级作用域（最外层）中有一个自定义类型 a，，2 级作用域（函数）中有一个变量 a，此时变量 a 会遮蔽自定义类型 a。对于将 ",[18,147,20],{"code":20},[18,149,24],{"code":24},[18,151,126],{"code":126},[18,153,88],{"code":88},"​ 进行规约，检查符号表发现在该作用域下 a 是一个变量而非自定义类型（遮蔽），于是该 AST 会被抛弃；对于将其解析为变量 ",[18,156,133],{"code":133},[18,158,126],{"code":126},[18,160,78],{"code":78},"​ 进行规约，检查符号表发现 a 确实是一个变量名，于是该 AST 会被保留",[14,163,164],{},"相对于完整的符号表，简易符号表不会检查作用域内同类变量的同名问题（可以检查用户自定义类型名的同名），由于存在前向声明，对相同变量的多次声明是合法的，而包含初始化的声明只可以有一次，在语法分析阶段区分声明是否包含初始化成本还是比较高的，建议延后到语义分析阶段；同时由于成本问题类型检查也不会在这个阶段进行",[14,166,167],{},"在 GLR 执行过程维护简易符号表，难以处理函数参数和 for 循环中的变量声明，这些变量的作用域实际在更深层，且作用域的开始和结束并不完全和大括号的范围重合，由于对于函数定义的规约，一定是首先移入左右大括号，之后才能规约为函数定义，这就导致没法简单地在移入左右大括号时处理作用域，只有综合看当前符号栈中的多个符号才能判断。究其原因，是因为 GLR 构建 AST 是自底向下构建的——从叶子节点一步步构建出根节点，底层节点会被优先处理，从而缺乏对节点上下文的感知",[14,169,170],{},"在 GLR 执行完成后通过遍历 AST 森林对单棵 AST 进行检查排除的方法就相对简单了很多，尽管无法在执行中剪枝，导致了相对较高的时间和空间复杂度，但是其优点在于实现简单、实现简单和实现简单。在实际实现过程中，可以综合使用执行中剪枝和执行后排除：执行中剪枝代价较小，且一旦剪枝成功，就能够减少执行后排除需要处理的 AST 数量。由于执行后排除一定可以解决该歧义，执行中剪枝需要保证“宁可放过不可错杀”",[10,172,174],{"id":173},"ast-构建中","AST 构建中",[14,176,177],{},"AST 构建中可以在移入一些符号或发生规约时，在节点中存储一些信息并向上传递，方便后续再自顶向下处理时可以快速获取信息，对于处理这个歧义，我的处理是增加两个标记",[30,179,184],{"className":180,"code":182,"language":183,"meta":38},[181],"language-go","type GLRLabel struct {\n\t\u002F\u002F Declaration 使用，规约出 Declaration 后消除\n\tTypeDef      bool     \u002F\u002F 是否是 TypeDef\n\tDeclaratorID []*Token \u002F\u002F 包含的 Identifier\n}\n","go",[18,185,182],{"__ignoreMap":38},[14,187,188,189,192],{},"typedef 用于标识这个声明是否是一个类型定义，如果最终规约出 declaration 时没有这个标识，说明这个声明只是一个普通的变量声明。DeclaratorID 为这个 declaration 定义的符号，如果这个声明是一个类型定义，那么 DeclaratorID 中为自定义类型的名称。同样，由于 function_definition 中的函数名称也使用了和 declaration 类似的 declarator 处理（",[18,190,191],{"code":191},"function_definition := declaration_specifiers declarator compound_statement","​），所以 DeclaratorID 中也会包含函数名称",[14,194,195],{},"这两个符号在构建 AST 树时，从下层向上层节点传递",[30,197,200],{"className":198,"code":199,"language":183,"meta":38},[181],"gslice.ForEach(children, func(child *AstNode) {\n    if child.TypeDef {\n        parent.TypeDef = true\n    }\n    parent.DeclaratorID = append(parent.DeclaratorID, child.DeclaratorID...)\n})\n",[18,201,199],{"__ignoreMap":38},[14,203,204],{},"那么在哪些场景设置这两个符号呢？",[14,206,207,208,211,212,215],{},"typedef 很明显，当使用 ",[18,209,210],{"code":210},"storage_class_specifier := TYPEDEF","​进行规约时设置当前节点的 ",[18,213,214],{"code":214},"typedef","​ 为 true",[14,217,218,219,222,223,226],{},"DeclaratorID 就比较复杂了，比较基础的如 ",[18,220,221],{"code":221},"direct_declarator := IDENTIFIER","​。另外，考虑枚举常量的场景，还需要处理 ",[18,224,225],{"code":225},"enumeration_constant := IDENTIFIER","​",[14,228,229],{},"当然，不能允许这两个符号无限向上传播，构建后自顶向下处理某一节点时应当只期望处理该级节点的信息，而非混杂着下层节点的信息（由于 C 语言中作用域的限制，信息只能从上级向下级传递，下级信息无法影响上级）。这时就需要在规约出某些节点时清空标记，阻断标记的向上传播",[14,231,232,233,236,237,240,241,244,245,248,249,251,252,255],{},"阻断向上传播的时机，除了规约出 ",[18,234,235],{"code":235},"declaration","​ 和 ",[18,238,239],{"code":239},"function_definition","​ 外，规约出 ",[18,242,243],{"code":243},"direct_declarator","​时，对于通过形如 ",[18,246,247],{"code":247},"direct_declarator := direct_declarator LEFT_PARENTHESES parameter_type_list RIGHT_PARENTHESES","​ 规约出的节点，应当只取右侧 ",[18,250,243],{"code":243},"​中的 DeclaratorID 继续传播，以避免 ",[18,253,254],{"code":254},"parameter_type_list","​ 包含函数参数声明的影响",[14,257,258],{},"在 AST 构建中，可以进行的、确定无误的检查，有以下两个：",[64,260,261,264],{},[67,262,263],{},"使用用户自定义类型时，检查是否是先前声明过的类型（无法检查变量声明遮蔽）",[67,265,226,266,269,270,273,274,276],{},[18,267,268],{"code":268},"declaration_specifiers","​中如果包含自定义类型的 ",[18,271,272],{"code":272},"type_specifier","​，那么只可以包含这一个 ",[18,275,272],{"code":272},"​（自定义类型已是完整类型，不应和其他类型符号一起使用）",[14,278,279,280,283,284,287,288,290,291,293],{},"第一个检查在构建时维护一个自定义类型符号栈，在移入 ",[18,281,282],{"code":282},"{","​ 时压入新作用域，在移入 ",[18,285,286],{"code":286},"}","​ 时弹出顶栈作用域。在规约出 Declaration 时，检查节点的 typedef 符号，如果是一个 typedef，就将这个节点的所有 DeclaratorID 加入顶栈作用域。在遇到通过 ",[18,289,88],{"code":88},"​ 规约的节点时，说明这个 ",[18,292,82],{"code":82},"​ 是一个此前定义的自定义类型，从栈顶开始向栈底检查即可",[14,295,296,297,299,300,236,302,305,306,308],{},"第二个检查比较简单，在规约出 ",[18,298,235],{"code":235},"​、",[18,301,239],{"code":239},[18,303,304],{"code":304},"parameter_declaration","​ 时，检查 ",[18,307,268],{"code":268},"​ 即可",[10,310,312],{"id":311},"ast-构建后","AST 构建后",[14,314,315],{},"在通过上述方式完成构建后，由于构建中的检查只处理了自定义类型，没有处理变量名，所以依然会有一些错误和歧义无法解决，如",[30,317,320],{"className":318,"code":319,"language":103,"meta":38},[101],"typedef int a;\nint main() {\n\tint a;\n\ta c;\t\u002F\u002F 类型 a 已经被变量 a 遮蔽，此处声明不合法\n}\n",[18,321,319],{"__ignoreMap":38},[14,323,324],{},"所以在构建后需要维护一个相对更全面一些的符号表，在每个作用域中同时记录类型名和变量名",[30,326,329],{"className":327,"code":328,"language":183,"meta":38},[181],"type ScopeSymbols struct {\n\tTypeNames map[string]*entity.Token\n\tVarNames  map[string]*entity.Token\n}\n",[18,330,328],{"__ignoreMap":38},[14,332,333,334,283,336,338,339,341],{},"和构建中检查类似，在移入 ",[18,335,282],{"code":282},[18,337,286],{"code":286},"​ 时弹出栈顶作用域，遇到 ",[18,340,235],{"code":235},"​ 节点，如果包含 typedef 符号，则将 DeclaratorID 加入栈顶作用域的类型名处，否则加入栈顶作用域的变量名中。在加入变量名时，需要检查栈顶作用域是否包含同名的类型名，若包含则需要返回错误；反之加入类型名时亦然",[14,343,344,345,348,349,352],{},"除此以外，函数定义中的函数名也需要作为变量符号加入符号表，函数变量的定义都形如",[18,346,347],{"code":347},"function_definition := declaration_specifiers declarator...","​，",[18,350,351],{"code":351},"declarator","​ 中的 DeclaratorID 即为函数名",[14,354,355,356,358,359,361],{},"接着就是检查自定义类型名和变量名的使用点了。自定义类型仅在 ",[18,357,88],{"code":88},"​ 处使用，在 AST 构建中已经检查过这个自定义类型是否已经定义过。在构建后检查中，需要额外检查这个类型名是否被变量名遮蔽，如果在更深层作用域中被遮蔽时仍然需要返回错误。变量名则是在 ",[18,360,78],{"code":78},"​ 处，其检查和类型名的检查类似",[14,363,364],{},"一个简单的变量名检查的例子",[30,366,369],{"className":367,"code":368,"language":183,"meta":38},[181],"func (s *symbolStack) CheckVar(token *entity.Token, depth int) error {\n\tfor i := depth; i >= 0; i-- {\n\t\tif previous, ok := s.stack[i].TypeNames[token.Lexeme]; ok {\n\t\t\treturn InvalidSymbolKind(token.SourceStart, previous.SourceStart, token.Lexeme)\n\t\t}\n\t\tif _, ok := s.stack[i].VarNames[token.Lexeme]; ok {\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn UndeclaredIdentifier(token.SourceStart, token.Lexeme)\n}\n",[18,370,368],{"__ignoreMap":38},[14,372,373],{},"在函数定义和 for 循环中，需要特殊处理作用域。函数定义中的函数参数和 for 循环条件（括号中的内容），并不在函数\u002F循环所在的作用域中，而在其内层作用域，在函数体\u002F循环体作用域中，以 for 循环为例",[30,375,378],{"className":376,"code":377,"language":183,"meta":38},[181],"currentSymbolStackDepth := s.symbolStack.currentSymbolStackDepth\ns.symbolStack.SwitchScope(currentSymbolStackDepth + 1)\t\u002F\u002F 切换到深层作用域\nfor i := 0; i \u003C len(node.Children)-1; i++ {\n\tif err := s.Chop(node.Children[i]); err != nil {\n\t\treturn err\n\t}\n}\ns.symbolStack.SwitchScope(currentSymbolStackDepth)\t\t\u002F\u002F 切换回当前作用域\nif err := s.Chop(node.Children[len(node.Children)-1]); err != nil {\n\t\u002F\u002F 如果循环体中包含 {，则会自然进入\n\treturn err\n}\ns.symbolStack.EnterScope(currentSymbolStackDepth)\t\t\u002F\u002F 若不存在循环体，则会导致深层作用域无法弹出，这里强行重置一下\n",[18,379,377],{"__ignoreMap":38},{"title":38,"searchDepth":381,"depth":381,"links":382},4,[383,385,386,387],{"id":12,"depth":384,"text":12},3,{"id":56,"depth":384,"text":56},{"id":173,"depth":384,"text":174},{"id":311,"depth":384,"text":312},[389],"折腾","2025-03-15 20:35:00","在语法分析过程中，用户自定义类型名与普通变量名的混淆成为一大难题。特别是在某些情况下，如 `a*b;`，该语句既可能被解读为数学表达式，也可能被视为类型声明。这种模糊性源于语法规则的设计，特别是涉及类型说明符的部分，可能会导致变量名被误认作类型名，影响代码的正确性与可读性。随着无初始化变量定义的普遍存在，这一问题在源码中愈发明显。",false,"md",null,{"slots":396},{},true,"\u002Ffiddling\u002Fparser-type-variable-ambiguity",{"text":400,"minutes":401,"time":402,"words":403},"15 min read",14.165,849900,2833,{"title":5,"description":391},{"loc":398},"posts\u002Ffiddling\u002Fparser-type-variable-ambiguity",[389,408,409,410],"编译原理","语法分析","歧义消除","tech","Z6g445uKDhcuUjgMwWEvDlO_XmtkmUuS5d4M8d4hZg4",[414,419],{"title":415,"path":416,"stem":417,"date":418,"type":411,"children":-1},"VPS 套 Warp 分流指定出口 IPv6","\u002Ffiddling\u002Fvps-warp-ipv6","posts\u002Ffiddling\u002Fvps-warp-ipv6","2025-03-15 16:24:00",{"title":420,"path":421,"stem":422,"date":423,"type":411,"children":-1},"My heart beats for U —— 心率同步 Grafana 展示","\u002Ffiddling\u002Fheart-rate-to-grafana","posts\u002Ffiddling\u002Fheart-rate-to-grafana","2025-03-31 23:51:00",1787554444268]