[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site-settings":3,"$f1g8n0u1z7owzc":14,"$f26l22ppafw1b0":31,"$f183nkndhgcctr":57,"$f2hf8zv88r59zg":67,"render-antlr4-12-string-concat-expressions":80},{"code":4,"message":5,"data":6},0,"ok",{"author_role":7,"icp":8,"skill_one_title":7,"about_hero_image":7,"skill_two_title":7,"author_status":7,"skill_one_desc":7,"site_description":9,"site_tagline":7,"author_years":10,"author_city":11,"home_hero_image":7,"tagline_line_two":7,"site_name":12,"closing_quote":7,"author_bio_two":7,"tagline_line_one":7,"weekly_thought":7,"author_tagline":7,"site_url":13,"site_footer":7,"author_bio":7,"skill_two_desc":7,"toolkit":7,"closing_quote_date":7,"og_default_image":7},"","陕ICP备2021009682号-1","花开空白 - 个人博客","10","西安","花开空白","https:\u002F\u002Firule.cn",{"code":4,"message":5,"data":15},{"id":16,"slug":17,"title":18,"summary":19,"coverUrl":20,"categoryId":21,"status":22,"topFlag":4,"viewCount":4,"likeCount":4,"wordCount":23,"readingMinutes":24,"publishedAt":25,"contentMd":26,"contentHtml":27,"tags":28,"metaTitle":20,"metaDescription":20,"keywords":20,"canonicalUrl":20,"robots":30,"ogImage":20,"faq":20,"aiSummary":20},"2101647962931642369","antlr4-12-string-concat-expressions","Antlr4系列⑫：字符串拼接与通用表达式","calcu升级为通用表达式、字符串拼接、setArg\u002FassignStmt\u002Fprint合并简化",null,"1946000000000000001",1,5692,15,"2026-09-20T20:21:45","上一篇文章末尾提到，要支持字符串拼接，`calcu`需要从\"只处理数值\"升级成能处理任意类型的通用表达式。这篇文章我们就来做这次手术。改动看起来不小，但每一步都很自然，做完之后你会发现语法反而比以前更简单了。\n\n## 一、问题：类型信息卡在了语法层面\n\n回顾一下现在的`setArg`：\n```\nsetArg :'number'  id = ID '='  calcu                      #numarg\n       |'string'  id = ID '='  STRING                     #chararg\n       |'boolean' id = ID '='  BOOL                       #boolarg\n       ;\n```\n`number`定义的变量，等号右边允许写一个完整的表达式（`calcu`）；而`string`、`boolean`定义的变量，等号右边却只能写一个孤零零的字面量（`STRING`\u002F`BOOL`词法）。这种不对称从第③篇文章就一直存在，当时没有引起什么问题，是因为字符串、布尔值从来没有参与过运算，\"能不能写表达式\"根本无所谓。现在我们想支持`\"Hello, \" + name`这样的字符串拼接，问题就暴露出来了：拼接的结果也是一个字符串，如果`string`类型的变量依然只能接一个`STRING`词法，这个拼接出来的结果根本没地方放。\n\n## 二、解决思路\n\n思路很直接：让`calcu`不仅能产生数值，也能产生字符串、布尔值，`ADD`（`+`）运算符在遇到字符串时自动切换成拼接语义。做到这一步之后，一个有意思的副作用出现了——`setArg`、`assignStmt`、`print`里那些因为\"数值\u002F字符串\u002F布尔\"而各自独立的语法分支，很多都可以合并成一个，因为它们的语法结构本来就是一样的，只是等号右边接受的类型不同，而现在等号右边统一都是`calcu`了。\n\n## 三、语法定义\n\n先看`calcu`本身的变化：\n```\ncalcu: calcu opt=(MUL|DIV) calcu                          # mulAndDiv\n     | calcu opt=(ADD|SUB) calcu                          # addAndSub  \u002F\u002F 修改：ADD支持字符串拼接\n     | '(' calcu ')'                                      # parens\n     | NUMBER                                             # number\n     | STRING                                             # stringValue  \u002F\u002F 新增\n     | BOOL                                                # boolValue    \u002F\u002F 新增\n     | ID '(' (calcu (',' calcu)*)? ')'                   # funcCall\n     | ID '[' calcu ']'                                    # arrayGet\n     | ID                                                 # argValue\n     ;\n```\n新增了`STRING`、`BOOL`两个基本值分支，`calcu`现在能表达\"数值 \u002F 字符串 \u002F 布尔值 \u002F 数组 \u002F 函数调用 \u002F 变量\"里的任意一种。\n\n有了这个基础，`setArg`、`assignStmt`、`print`都可以做简化：\n```\n\u002F\u002F setArg：三个关键字后面统一接calcu，关键字的意义从\"决定语法结构\"变成了\"定义时的一次类型断言\"\nsetArg :'number'  id = ID '='  calcu                      #numarg\n       |'string'  id = ID '='  calcu                      #chararg  \u002F\u002F 修改\n       |'boolean' id = ID '='  calcu                      #boolarg  \u002F\u002F 修改\n       |'array'   id = ID '='  arrayLiteral                #arrayarg\n       ;\n\n\u002F\u002F assignStmt：原来numAssign\u002FcharAssign\u002FboolAssign三个分支的语法结构其实完全一样\n\u002F\u002F （都是\"id = calcu\"），只是靠等号右边的词法形状（calcu\u002FSTRING\u002FBOOL）人为地分成三份，\n\u002F\u002F 现在calcu已经能表达全部三种结果，直接合并成一个\nassignStmt: id = ID '=' calcu                              #assign  \u002F\u002F 修改：合并了原来的numAssign\u002FcharAssign\u002FboolAssign\n          | arr = ID '[' index = calcu ']' '=' value = calcu #arrayElemAssign\n          ;\n\n\u002F\u002F print：同理，三个各自只接受一种类型的分支合并成一个，统一接受calcu，\n\u002F\u002F 这意味着现在可以直接print(1 + 2)、print(a + b)这样的表达式，不需要先赋值给一个变量再打印\nprint: PRINT '(' calcu ')'                                 # printValue  \u002F\u002F 修改\n     ;\n```\n可以看到，`setArg`三个分支之所以还保留着三份，是因为`number`\u002F`string`\u002F`boolean`这几个关键字仍然有存在的意义——它们在定义变量的同时对表达式的结果做了一次类型断言（比如`string`要求等号右边算出来必须是字符串）；而`assignStmt`和`print`原本按类型分开纯粹是语法层面的将就，一旦等号（或括号）右边能表达任意类型，这种拆分就没有必要了，合并之后代码反而更简洁。\n\n## 四、语法实现\n\n### 1. 字符串\u002F布尔字面量求值，以及一个悄悄存在的历史遗留问题\n\n```java\n\u002F**\n * 字符串字面量，去掉两端的引号得到真正的文本内容。以前STRING的原始文本（带引号）会被\n * 一路带着跑到打印的地方才显示出来，现在字符串要参与拼接，引号必须在这里就去掉，\n * 否则\"a\" + \"b\"拼出来的会是带着引号的\"a\"\"b\"，不符合直觉\n *\u002F\n@Override\npublic VisitorResult visitStringValue(RuleSetParser.StringValueContext ctx) {\n    String text = ctx.getText();\n    return VisitorResult.value(text.substring(1, text.length() - 1));\n}\n\n@Override\npublic VisitorResult visitBoolValue(RuleSetParser.BoolValueContext ctx) {\n    return VisitorResult.value(Boolean.parseBoolean(ctx.getText()));\n}\n```\n> 关于引号：细心的读者可能记得，从第③篇文章开始，字符串变量存的其实是带引号的原始文本，打印出来也是带引号的（比如之前文章里的执行结果`\"字符串\"`）。当时这么做纯粹是图省事——反正字符串只会被原样打印，长什么样打出来就是什么样，没人会较真。但现在字符串要参与拼接，这个\"省事\"的选择就变成了债——如果不在这里去掉引号，`\"Hello, \" + name`拼出来会变成`\"Hello, \"\"World\"`这种带着引号疙瘩的怪东西。所以这篇文章顺便把这个历史遗留问题一起修正了：从这篇开始，字符串打印出来不再带引号，这是语义上更正确的行为。\n\n### 2. ADD运算符的字符串拼接语义\n\n```java\n@Override\npublic VisitorResult visitAddAndSub(RuleSetParser.AddAndSubContext ctx) {\n    VisitorResult left = visit(ctx.calcu(0));\n    VisitorResult right = visit(ctx.calcu(1));\n    if (ctx.opt.getType() == RuleSetLexer.SUB) {\n        return VisitorResult.value(left.getNumber() - right.getNumber());\n    }\n    \u002F\u002F ADD：只要两侧有一侧是字符串，就做字符串拼接（非字符串的一侧借助toString()转成文本），\n    \u002F\u002F 两侧都是数值时才是加法\n    if (left.isVarchar() || right.isVarchar()) {\n        return VisitorResult.value(left.toString() + right.toString());\n    }\n    return VisitorResult.value(left.getNumber() + right.getNumber());\n}\n```\n`SUB`（减法）语义不变，永远要求两侧是数值。`ADD`则先判断两侧有没有字符串，只要有一侧是，就统一走拼接逻辑，非字符串的一侧借助`VisitorResult.toString()`（这个方法在前几篇文章里就已经实现好了，知道怎么把数值、布尔值格式化成文本）转换成文本再拼起来，所以`\"Score: \" + 87`能够正确地拼出`\"Score: 87.0\"`。\n\n### 3. 合并之后的定义、赋值、打印\n\n```java\n\u002F**\n * 定义字符型变量：等号右边现在是一个通用的calcu，可以是字符串字面量，也可以是字符串拼接\n * 表达式（比如\"a\" + b），调用getString()既完成了求值，也顺带做了一次类型校验\n *\u002F\n@Override\npublic VisitorResult visitChararg(RuleSetParser.CharargContext ctx) {\n    currentScope.define(ctx.id.getText(), visit(ctx.calcu()).getString());\n    return VisitorResult.nil();\n}\n\n\u002F**\n * 定义布尔型变量：等号右边同样改成通用的calcu\n *\u002F\n@Override\npublic VisitorResult visitBoolarg(RuleSetParser.BoolargContext ctx) {\n    currentScope.define(ctx.id.getText(), visit(ctx.calcu()).getBool());\n    return VisitorResult.nil();\n}\n\n\u002F**\n * 给已定义的变量重新赋值。calcu现在能产生任意类型的结果，不再需要像以前那样区分\n * numAssign\u002FcharAssign\u002FboolAssign，取出原始值直接沿作用域链写回去即可\n *\u002F\n@Override\npublic VisitorResult visitAssign(RuleSetParser.AssignContext ctx) {\n    VisitorResult value = visit(ctx.calcu());\n    assign(ctx.id.getText(), value.getValue());\n    return VisitorResult.nil();\n}\n\n\u002F**\n * 打印：value现在是通用的calcu，求值之后直接println——VisitorResult自己的toString()\n * 知道该怎么把数值\u002F字符串\u002F布尔值\u002F数组\u002Fnil分别显示成什么样子\n *\u002F\n@Override\npublic VisitorResult visitPrintValue(RuleSetParser.PrintValueContext ctx) {\n    System.out.println(visit(ctx.calcu()));\n    return VisitorResult.nil();\n}\n```\n三个类型专属的`visitNumAssign`\u002F`visitCharAssign`\u002F`visitBoolAssign`合并成了一个`visitAssign`，三个`visitPrintArg`\u002F`visitPrintBoolean`\u002F`visitPrintString`合并成了一个`visitPrintValue`——这也是这次重构最直观的收益：代码量不增反减。\n\n## 五、测试代码与执行结果\n\n```java\nString expression =\n        \"string greeting = \\\"Hello, \\\" \\n\" +\n                \"string name = \\\"World\\\" \\n\" +\n                \"print(greeting + name + \\\"!\\\") \\n\" +\n                \"number score = 87 \\n\" +\n                \"print(\\\"Score: \\\" + score) \\n\" +\n                \"print(1 + 2 * 3) \\n\" +\n                \"array arr = [10, 20, 30] \\n\" +\n                \"print(arr[0] + arr[1]) \\n\" +\n                \"print(true) \\n\" +\n                \"greeting = \\\"Hi, \\\" \\n\" +\n                \"print(greeting + name + \\\"!\\\")\";\ncalcute(expression);\n```\n\n执行结果如下：\n\n```\n执行:\nstring greeting = \"Hello, \"\nstring name = \"World\"\nprint(greeting + name + \"!\")\nnumber score = 87\nprint(\"Score: \" + score)\nprint(1 + 2 * 3)\narray arr = [10, 20, 30]\nprint(arr[0] + arr[1])\nprint(true)\ngreeting = \"Hi, \"\nprint(greeting + name + \"!\")\n\nHello, World!\nScore: 87.0\n7.0\n30.0\ntrue\nHi, World!\n```\n`greeting + name + \"!\"`正确拼出了`Hello, World!`（字符串左结合，先拼`greeting`和`name`，再拼`\"!\"`）；`\"Score: \" + score`把数值`87.0`自动转成文本拼了上去；`print(1 + 2 * 3)`证明`print`现在可以直接打印一个表达式的计算结果而不需要中间变量；`print(arr[0] + arr[1])`证明数组下标访问的结果也能直接参与加法运算并打印；`print(true)`证明布尔字面量也能被`calcu`直接处理；最后用合并后的`assign`给`greeting`重新赋值成`\"Hi, \"`，再拼接一次得到`Hi, World!`，说明赋值合并之后行为依然正确。另外把之前几篇文章里函数递归、数组读写、`while`遍历、`if`条件的例子重新跑了一遍，结果和之前完全一致，这次重构没有引入回归。\n\n## 六、遗留的问题\n\n到这里，我们的小型DSL已经把数值、字符串、布尔值、数组这几种类型，统一在了同一套通用表达式（`calcu`）之上，`setArg`\u002F`assignStmt`\u002F`print`也都因此变得更简洁。不过`cond`（`if`\u002F`while`的条件表达式）目前还是一套独立的语法，比较运算符、逻辑运算符都定义在`cond`里而不是`calcu`里——这在当初是为了让`calcu`保持\"纯数值运算\"的单纯，现在`calcu`已经是通用表达式了，`cond`和`calcu`之间这条边界还要不要继续保留，是个值得继续思考的问题，这里先留给读者自己想一想，本系列到这里先告一段落。回顾整个系列，从最初一个只会做加减乘除的计算器，到现在拥有变量、分支、循环、作用域、函数递归、数组、内置函数、通用表达式，甚至还有一个能设断点、单步调试的调试器——希望这个循序渐进的过程，能帮助你比较扎实地理解Antlr4从语法设计到解释执行的完整链路。\n","\u003Cp>上一篇文章末尾提到，要支持字符串拼接，\u003Ccode>calcu\u003C\u002Fcode>需要从\"只处理数值\"升级成能处理任意类型的通用表达式。这篇文章我们就来做这次手术。改动看起来不小，但每一步都很自然，做完之后你会发现语法反而比以前更简单了。\u003C\u002Fp>\n\u003Ch2>一、问题：类型信息卡在了语法层面\u003C\u002Fh2>\n\u003Cp>回顾一下现在的\u003Ccode>setArg\u003C\u002Fcode>：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>setArg :'number'  id = ID '='  calcu                      #numarg\n       |'string'  id = ID '='  STRING                     #chararg\n       |'boolean' id = ID '='  BOOL                       #boolarg\n       ;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>number\u003C\u002Fcode>定义的变量，等号右边允许写一个完整的表达式（\u003Ccode>calcu\u003C\u002Fcode>）；而\u003Ccode>string\u003C\u002Fcode>、\u003Ccode>boolean\u003C\u002Fcode>定义的变量，等号右边却只能写一个孤零零的字面量（\u003Ccode>STRING\u003C\u002Fcode>\u002F\u003Ccode>BOOL\u003C\u002Fcode>词法）。这种不对称从第③篇文章就一直存在，当时没有引起什么问题，是因为字符串、布尔值从来没有参与过运算，\"能不能写表达式\"根本无所谓。现在我们想支持\u003Ccode>\"Hello, \" + name\u003C\u002Fcode>这样的字符串拼接，问题就暴露出来了：拼接的结果也是一个字符串，如果\u003Ccode>string\u003C\u002Fcode>类型的变量依然只能接一个\u003Ccode>STRING\u003C\u002Fcode>词法，这个拼接出来的结果根本没地方放。\u003C\u002Fp>\n\u003Ch2>二、解决思路\u003C\u002Fh2>\n\u003Cp>思路很直接：让\u003Ccode>calcu\u003C\u002Fcode>不仅能产生数值，也能产生字符串、布尔值，\u003Ccode>ADD\u003C\u002Fcode>（\u003Ccode>+\u003C\u002Fcode>）运算符在遇到字符串时自动切换成拼接语义。做到这一步之后，一个有意思的副作用出现了——\u003Ccode>setArg\u003C\u002Fcode>、\u003Ccode>assignStmt\u003C\u002Fcode>、\u003Ccode>print\u003C\u002Fcode>里那些因为\"数值\u002F字符串\u002F布尔\"而各自独立的语法分支，很多都可以合并成一个，因为它们的语法结构本来就是一样的，只是等号右边接受的类型不同，而现在等号右边统一都是\u003Ccode>calcu\u003C\u002Fcode>了。\u003C\u002Fp>\n\u003Ch2>三、语法定义\u003C\u002Fh2>\n\u003Cp>先看\u003Ccode>calcu\u003C\u002Fcode>本身的变化：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>calcu: calcu opt=(MUL|DIV) calcu                          # mulAndDiv\n     | calcu opt=(ADD|SUB) calcu                          # addAndSub  \u002F\u002F 修改：ADD支持字符串拼接\n     | '(' calcu ')'                                      # parens\n     | NUMBER                                             # number\n     | STRING                                             # stringValue  \u002F\u002F 新增\n     | BOOL                                                # boolValue    \u002F\u002F 新增\n     | ID '(' (calcu (',' calcu)*)? ')'                   # funcCall\n     | ID '[' calcu ']'                                    # arrayGet\n     | ID                                                 # argValue\n     ;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>新增了\u003Ccode>STRING\u003C\u002Fcode>、\u003Ccode>BOOL\u003C\u002Fcode>两个基本值分支，\u003Ccode>calcu\u003C\u002Fcode>现在能表达\"数值 \u002F 字符串 \u002F 布尔值 \u002F 数组 \u002F 函数调用 \u002F 变量\"里的任意一种。\u003C\u002Fp>\n\u003Cp>有了这个基础，\u003Ccode>setArg\u003C\u002Fcode>、\u003Ccode>assignStmt\u003C\u002Fcode>、\u003Ccode>print\u003C\u002Fcode>都可以做简化：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002F\u002F setArg：三个关键字后面统一接calcu，关键字的意义从\"决定语法结构\"变成了\"定义时的一次类型断言\"\nsetArg :'number'  id = ID '='  calcu                      #numarg\n       |'string'  id = ID '='  calcu                      #chararg  \u002F\u002F 修改\n       |'boolean' id = ID '='  calcu                      #boolarg  \u002F\u002F 修改\n       |'array'   id = ID '='  arrayLiteral                #arrayarg\n       ;\n\n\u002F\u002F assignStmt：原来numAssign\u002FcharAssign\u002FboolAssign三个分支的语法结构其实完全一样\n\u002F\u002F （都是\"id = calcu\"），只是靠等号右边的词法形状（calcu\u002FSTRING\u002FBOOL）人为地分成三份，\n\u002F\u002F 现在calcu已经能表达全部三种结果，直接合并成一个\nassignStmt: id = ID '=' calcu                              #assign  \u002F\u002F 修改：合并了原来的numAssign\u002FcharAssign\u002FboolAssign\n          | arr = ID '[' index = calcu ']' '=' value = calcu #arrayElemAssign\n          ;\n\n\u002F\u002F print：同理，三个各自只接受一种类型的分支合并成一个，统一接受calcu，\n\u002F\u002F 这意味着现在可以直接print(1 + 2)、print(a + b)这样的表达式，不需要先赋值给一个变量再打印\nprint: PRINT '(' calcu ')'                                 # printValue  \u002F\u002F 修改\n     ;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>可以看到，\u003Ccode>setArg\u003C\u002Fcode>三个分支之所以还保留着三份，是因为\u003Ccode>number\u003C\u002Fcode>\u002F\u003Ccode>string\u003C\u002Fcode>\u002F\u003Ccode>boolean\u003C\u002Fcode>这几个关键字仍然有存在的意义——它们在定义变量的同时对表达式的结果做了一次类型断言（比如\u003Ccode>string\u003C\u002Fcode>要求等号右边算出来必须是字符串）；而\u003Ccode>assignStmt\u003C\u002Fcode>和\u003Ccode>print\u003C\u002Fcode>原本按类型分开纯粹是语法层面的将就，一旦等号（或括号）右边能表达任意类型，这种拆分就没有必要了，合并之后代码反而更简洁。\u003C\u002Fp>\n\u003Ch2>四、语法实现\u003C\u002Fh2>\n\u003Ch3>1. 字符串\u002F布尔字面量求值，以及一个悄悄存在的历史遗留问题\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F**\n * 字符串字面量，去掉两端的引号得到真正的文本内容。以前STRING的原始文本（带引号）会被\n * 一路带着跑到打印的地方才显示出来，现在字符串要参与拼接，引号必须在这里就去掉，\n * 否则\"a\" + \"b\"拼出来的会是带着引号的\"a\"\"b\"，不符合直觉\n *\u002F\n@Override\npublic VisitorResult visitStringValue(RuleSetParser.StringValueContext ctx) {\n    String text = ctx.getText();\n    return VisitorResult.value(text.substring(1, text.length() - 1));\n}\n\n@Override\npublic VisitorResult visitBoolValue(RuleSetParser.BoolValueContext ctx) {\n    return VisitorResult.value(Boolean.parseBoolean(ctx.getText()));\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cblockquote>\n \u003Cp>关于引号：细心的读者可能记得，从第③篇文章开始，字符串变量存的其实是带引号的原始文本，打印出来也是带引号的（比如之前文章里的执行结果\u003Ccode>\"字符串\"\u003C\u002Fcode>）。当时这么做纯粹是图省事——反正字符串只会被原样打印，长什么样打出来就是什么样，没人会较真。但现在字符串要参与拼接，这个\"省事\"的选择就变成了债——如果不在这里去掉引号，\u003Ccode>\"Hello, \" + name\u003C\u002Fcode>拼出来会变成\u003Ccode>\"Hello, \"\"World\"\u003C\u002Fcode>这种带着引号疙瘩的怪东西。所以这篇文章顺便把这个历史遗留问题一起修正了：从这篇开始，字符串打印出来不再带引号，这是语义上更正确的行为。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch3>2. ADD运算符的字符串拼接语义\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-java\">@Override\npublic VisitorResult visitAddAndSub(RuleSetParser.AddAndSubContext ctx) {\n    VisitorResult left = visit(ctx.calcu(0));\n    VisitorResult right = visit(ctx.calcu(1));\n    if (ctx.opt.getType() == RuleSetLexer.SUB) {\n        return VisitorResult.value(left.getNumber() - right.getNumber());\n    }\n    \u002F\u002F ADD：只要两侧有一侧是字符串，就做字符串拼接（非字符串的一侧借助toString()转成文本），\n    \u002F\u002F 两侧都是数值时才是加法\n    if (left.isVarchar() || right.isVarchar()) {\n        return VisitorResult.value(left.toString() + right.toString());\n    }\n    return VisitorResult.value(left.getNumber() + right.getNumber());\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>SUB\u003C\u002Fcode>（减法）语义不变，永远要求两侧是数值。\u003Ccode>ADD\u003C\u002Fcode>则先判断两侧有没有字符串，只要有一侧是，就统一走拼接逻辑，非字符串的一侧借助\u003Ccode>VisitorResult.toString()\u003C\u002Fcode>（这个方法在前几篇文章里就已经实现好了，知道怎么把数值、布尔值格式化成文本）转换成文本再拼起来，所以\u003Ccode>\"Score: \" + 87\u003C\u002Fcode>能够正确地拼出\u003Ccode>\"Score: 87.0\"\u003C\u002Fcode>。\u003C\u002Fp>\n\u003Ch3>3. 合并之后的定义、赋值、打印\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F**\n * 定义字符型变量：等号右边现在是一个通用的calcu，可以是字符串字面量，也可以是字符串拼接\n * 表达式（比如\"a\" + b），调用getString()既完成了求值，也顺带做了一次类型校验\n *\u002F\n@Override\npublic VisitorResult visitChararg(RuleSetParser.CharargContext ctx) {\n    currentScope.define(ctx.id.getText(), visit(ctx.calcu()).getString());\n    return VisitorResult.nil();\n}\n\n\u002F**\n * 定义布尔型变量：等号右边同样改成通用的calcu\n *\u002F\n@Override\npublic VisitorResult visitBoolarg(RuleSetParser.BoolargContext ctx) {\n    currentScope.define(ctx.id.getText(), visit(ctx.calcu()).getBool());\n    return VisitorResult.nil();\n}\n\n\u002F**\n * 给已定义的变量重新赋值。calcu现在能产生任意类型的结果，不再需要像以前那样区分\n * numAssign\u002FcharAssign\u002FboolAssign，取出原始值直接沿作用域链写回去即可\n *\u002F\n@Override\npublic VisitorResult visitAssign(RuleSetParser.AssignContext ctx) {\n    VisitorResult value = visit(ctx.calcu());\n    assign(ctx.id.getText(), value.getValue());\n    return VisitorResult.nil();\n}\n\n\u002F**\n * 打印：value现在是通用的calcu，求值之后直接println——VisitorResult自己的toString()\n * 知道该怎么把数值\u002F字符串\u002F布尔值\u002F数组\u002Fnil分别显示成什么样子\n *\u002F\n@Override\npublic VisitorResult visitPrintValue(RuleSetParser.PrintValueContext ctx) {\n    System.out.println(visit(ctx.calcu()));\n    return VisitorResult.nil();\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>三个类型专属的\u003Ccode>visitNumAssign\u003C\u002Fcode>\u002F\u003Ccode>visitCharAssign\u003C\u002Fcode>\u002F\u003Ccode>visitBoolAssign\u003C\u002Fcode>合并成了一个\u003Ccode>visitAssign\u003C\u002Fcode>，三个\u003Ccode>visitPrintArg\u003C\u002Fcode>\u002F\u003Ccode>visitPrintBoolean\u003C\u002Fcode>\u002F\u003Ccode>visitPrintString\u003C\u002Fcode>合并成了一个\u003Ccode>visitPrintValue\u003C\u002Fcode>——这也是这次重构最直观的收益：代码量不增反减。\u003C\u002Fp>\n\u003Ch2>五、测试代码与执行结果\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-java\">String expression =\n        \"string greeting = \\\"Hello, \\\" \\n\" +\n                \"string name = \\\"World\\\" \\n\" +\n                \"print(greeting + name + \\\"!\\\") \\n\" +\n                \"number score = 87 \\n\" +\n                \"print(\\\"Score: \\\" + score) \\n\" +\n                \"print(1 + 2 * 3) \\n\" +\n                \"array arr = [10, 20, 30] \\n\" +\n                \"print(arr[0] + arr[1]) \\n\" +\n                \"print(true) \\n\" +\n                \"greeting = \\\"Hi, \\\" \\n\" +\n                \"print(greeting + name + \\\"!\\\")\";\ncalcute(expression);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>执行结果如下：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>执行:\nstring greeting = \"Hello, \"\nstring name = \"World\"\nprint(greeting + name + \"!\")\nnumber score = 87\nprint(\"Score: \" + score)\nprint(1 + 2 * 3)\narray arr = [10, 20, 30]\nprint(arr[0] + arr[1])\nprint(true)\ngreeting = \"Hi, \"\nprint(greeting + name + \"!\")\n\nHello, World!\nScore: 87.0\n7.0\n30.0\ntrue\nHi, World!\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>greeting + name + \"!\"\u003C\u002Fcode>正确拼出了\u003Ccode>Hello, World!\u003C\u002Fcode>（字符串左结合，先拼\u003Ccode>greeting\u003C\u002Fcode>和\u003Ccode>name\u003C\u002Fcode>，再拼\u003Ccode>\"!\"\u003C\u002Fcode>）；\u003Ccode>\"Score: \" + score\u003C\u002Fcode>把数值\u003Ccode>87.0\u003C\u002Fcode>自动转成文本拼了上去；\u003Ccode>print(1 + 2 * 3)\u003C\u002Fcode>证明\u003Ccode>print\u003C\u002Fcode>现在可以直接打印一个表达式的计算结果而不需要中间变量；\u003Ccode>print(arr[0] + arr[1])\u003C\u002Fcode>证明数组下标访问的结果也能直接参与加法运算并打印；\u003Ccode>print(true)\u003C\u002Fcode>证明布尔字面量也能被\u003Ccode>calcu\u003C\u002Fcode>直接处理；最后用合并后的\u003Ccode>assign\u003C\u002Fcode>给\u003Ccode>greeting\u003C\u002Fcode>重新赋值成\u003Ccode>\"Hi, \"\u003C\u002Fcode>，再拼接一次得到\u003Ccode>Hi, World!\u003C\u002Fcode>，说明赋值合并之后行为依然正确。另外把之前几篇文章里函数递归、数组读写、\u003Ccode>while\u003C\u002Fcode>遍历、\u003Ccode>if\u003C\u002Fcode>条件的例子重新跑了一遍，结果和之前完全一致，这次重构没有引入回归。\u003C\u002Fp>\n\u003Ch2>六、遗留的问题\u003C\u002Fh2>\n\u003Cp>到这里，我们的小型DSL已经把数值、字符串、布尔值、数组这几种类型，统一在了同一套通用表达式（\u003Ccode>calcu\u003C\u002Fcode>）之上，\u003Ccode>setArg\u003C\u002Fcode>\u002F\u003Ccode>assignStmt\u003C\u002Fcode>\u002F\u003Ccode>print\u003C\u002Fcode>也都因此变得更简洁。不过\u003Ccode>cond\u003C\u002Fcode>（\u003Ccode>if\u003C\u002Fcode>\u002F\u003Ccode>while\u003C\u002Fcode>的条件表达式）目前还是一套独立的语法，比较运算符、逻辑运算符都定义在\u003Ccode>cond\u003C\u002Fcode>里而不是\u003Ccode>calcu\u003C\u002Fcode>里——这在当初是为了让\u003Ccode>calcu\u003C\u002Fcode>保持\"纯数值运算\"的单纯，现在\u003Ccode>calcu\u003C\u002Fcode>已经是通用表达式了，\u003Ccode>cond\u003C\u002Fcode>和\u003Ccode>calcu\u003C\u002Fcode>之间这条边界还要不要继续保留，是个值得继续思考的问题，这里先留给读者自己想一想，本系列到这里先告一段落。回顾整个系列，从最初一个只会做加减乘除的计算器，到现在拥有变量、分支、循环、作用域、函数递归、数组、内置函数、通用表达式，甚至还有一个能设断点、单步调试的调试器——希望这个循序渐进的过程，能帮助你比较扎实地理解Antlr4从语法设计到解释执行的完整链路。\u003C\u002Fp>",[29],"Antlr4","index,follow",{"code":4,"message":5,"data":32},[33,41,49],{"id":34,"slug":35,"title":36,"summary":37,"coverUrl":20,"categoryId":21,"status":22,"topFlag":4,"viewCount":4,"likeCount":4,"wordCount":38,"readingMinutes":39,"publishedAt":40},"2101585710597718018","antlr4-03-variables-and-print","Antlr4系列③：支持变量定义和打印函数","number\u002Fstring\u002Fboolean变量定义、print函数、VisitorResult统一返回类型",8172,21,"2026-09-20T16:14:14",{"id":42,"slug":43,"title":44,"summary":45,"coverUrl":20,"categoryId":21,"status":22,"topFlag":4,"viewCount":4,"likeCount":4,"wordCount":46,"readingMinutes":47,"publishedAt":48},"2101642321643094018","antlr4-11-builtin-functions","Antlr4系列⑪：内置函数注册机制","BuiltinFunction注册表、len\u002Fsum\u002Fabs\u002Fmax、放宽变量类型限制",5528,14,"2026-09-20T19:59:19",{"id":50,"slug":51,"title":52,"summary":53,"coverUrl":20,"categoryId":21,"status":22,"topFlag":4,"viewCount":4,"likeCount":4,"wordCount":54,"readingMinutes":55,"publishedAt":56},"2101619933819879425","antlr4-07-functions-and-call-stack","Antlr4系列⑦：自定义函数与调用栈","function定义\u002F调用、递归、词法作用域、调用栈防死递归",6508,17,"2026-09-20T18:33:25",{"code":4,"message":5,"data":58},{"prev":59,"next":60},{"id":42,"slug":43,"title":44,"summary":45,"coverUrl":20,"categoryId":21,"status":22,"topFlag":4,"viewCount":4,"likeCount":4,"wordCount":46,"readingMinutes":47,"publishedAt":48},{"id":61,"slug":62,"title":63,"summary":64,"coverUrl":20,"categoryId":21,"status":22,"topFlag":4,"viewCount":4,"likeCount":4,"wordCount":65,"readingMinutes":47,"publishedAt":66},"2101652901984649217","antlr4-13-listener-vs-visitor","Antlr4系列⑬：Listener模式对比","用Listener重新实现部分能力、实测对比if\u002Felse下Listener与Visitor的行为差异",5439,"2026-09-20T20:41:23",{"code":4,"message":5,"data":68},[69,74],{"id":21,"name":70,"slug":71,"description":72,"sortOrder":4,"articleCount":73},"技术","tech","编程与技术笔记","13",{"id":75,"name":76,"slug":77,"description":78,"sortOrder":22,"articleCount":79},"1946000000000000002","随笔","essay","生活与思考","0",{"html":81,"toc":82},"\u003Cp>上一篇文章末尾提到，要支持字符串拼接，\u003Ccode>calcu\u003C\u002Fcode>需要从\"只处理数值\"升级成能处理任意类型的通用表达式。这篇文章我们就来做这次手术。改动看起来不小，但每一步都很自然，做完之后你会发现语法反而比以前更简单了。\u003C\u002Fp>\n\u003Ch2 id=\"toc-1\">一、问题：类型信息卡在了语法层面\u003C\u002Fh2>\n\u003Cp>回顾一下现在的\u003Ccode>setArg\u003C\u002Fcode>：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>setArg :'number'  id = ID '='  calcu                      #numarg\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>       |'string'  id = ID '='  STRING                     #chararg\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>       |'boolean' id = ID '='  BOOL                       #boolarg\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>       ;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>number\u003C\u002Fcode>定义的变量，等号右边允许写一个完整的表达式（\u003Ccode>calcu\u003C\u002Fcode>）；而\u003Ccode>string\u003C\u002Fcode>、\u003Ccode>boolean\u003C\u002Fcode>定义的变量，等号右边却只能写一个孤零零的字面量（\u003Ccode>STRING\u003C\u002Fcode>\u002F\u003Ccode>BOOL\u003C\u002Fcode>词法）。这种不对称从第③篇文章就一直存在，当时没有引起什么问题，是因为字符串、布尔值从来没有参与过运算，\"能不能写表达式\"根本无所谓。现在我们想支持\u003Ccode>\"Hello, \" + name\u003C\u002Fcode>这样的字符串拼接，问题就暴露出来了：拼接的结果也是一个字符串，如果\u003Ccode>string\u003C\u002Fcode>类型的变量依然只能接一个\u003Ccode>STRING\u003C\u002Fcode>词法，这个拼接出来的结果根本没地方放。\u003C\u002Fp>\n\u003Ch2 id=\"toc-2\">二、解决思路\u003C\u002Fh2>\n\u003Cp>思路很直接：让\u003Ccode>calcu\u003C\u002Fcode>不仅能产生数值，也能产生字符串、布尔值，\u003Ccode>ADD\u003C\u002Fcode>（\u003Ccode>+\u003C\u002Fcode>）运算符在遇到字符串时自动切换成拼接语义。做到这一步之后，一个有意思的副作用出现了——\u003Ccode>setArg\u003C\u002Fcode>、\u003Ccode>assignStmt\u003C\u002Fcode>、\u003Ccode>print\u003C\u002Fcode>里那些因为\"数值\u002F字符串\u002F布尔\"而各自独立的语法分支，很多都可以合并成一个，因为它们的语法结构本来就是一样的，只是等号右边接受的类型不同，而现在等号右边统一都是\u003Ccode>calcu\u003C\u002Fcode>了。\u003C\u002Fp>\n\u003Ch2 id=\"toc-3\">三、语法定义\u003C\u002Fh2>\n\u003Cp>先看\u003Ccode>calcu\u003C\u002Fcode>本身的变化：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>calcu: calcu opt=(MUL|DIV) calcu                          # mulAndDiv\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     | calcu opt=(ADD|SUB) calcu                          # addAndSub  \u002F\u002F 修改：ADD支持字符串拼接\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     | '(' calcu ')'                                      # parens\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     | NUMBER                                             # number\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     | STRING                                             # stringValue  \u002F\u002F 新增\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     | BOOL                                                # boolValue    \u002F\u002F 新增\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     | ID '(' (calcu (',' calcu)*)? ')'                   # funcCall\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     | ID '[' calcu ']'                                    # arrayGet\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     | ID                                                 # argValue\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     ;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>新增了\u003Ccode>STRING\u003C\u002Fcode>、\u003Ccode>BOOL\u003C\u002Fcode>两个基本值分支，\u003Ccode>calcu\u003C\u002Fcode>现在能表达\"数值 \u002F 字符串 \u002F 布尔值 \u002F 数组 \u002F 函数调用 \u002F 变量\"里的任意一种。\u003C\u002Fp>\n\u003Cp>有了这个基础，\u003Ccode>setArg\u003C\u002Fcode>、\u003Ccode>assignStmt\u003C\u002Fcode>、\u003Ccode>print\u003C\u002Fcode>都可以做简化：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>\u002F\u002F setArg：三个关键字后面统一接calcu，关键字的意义从\"决定语法结构\"变成了\"定义时的一次类型断言\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>setArg :'number'  id = ID '='  calcu                      #numarg\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>       |'string'  id = ID '='  calcu                      #chararg  \u002F\u002F 修改\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>       |'boolean' id = ID '='  calcu                      #boolarg  \u002F\u002F 修改\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>       |'array'   id = ID '='  arrayLiteral                #arrayarg\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>       ;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u002F\u002F assignStmt：原来numAssign\u002FcharAssign\u002FboolAssign三个分支的语法结构其实完全一样\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u002F\u002F （都是\"id = calcu\"），只是靠等号右边的词法形状（calcu\u002FSTRING\u002FBOOL）人为地分成三份，\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u002F\u002F 现在calcu已经能表达全部三种结果，直接合并成一个\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>assignStmt: id = ID '=' calcu                              #assign  \u002F\u002F 修改：合并了原来的numAssign\u002FcharAssign\u002FboolAssign\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>          | arr = ID '[' index = calcu ']' '=' value = calcu #arrayElemAssign\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>          ;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u002F\u002F print：同理，三个各自只接受一种类型的分支合并成一个，统一接受calcu，\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u002F\u002F 这意味着现在可以直接print(1 + 2)、print(a + b)这样的表达式，不需要先赋值给一个变量再打印\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>print: PRINT '(' calcu ')'                                 # printValue  \u002F\u002F 修改\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     ;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>可以看到，\u003Ccode>setArg\u003C\u002Fcode>三个分支之所以还保留着三份，是因为\u003Ccode>number\u003C\u002Fcode>\u002F\u003Ccode>string\u003C\u002Fcode>\u002F\u003Ccode>boolean\u003C\u002Fcode>这几个关键字仍然有存在的意义——它们在定义变量的同时对表达式的结果做了一次类型断言（比如\u003Ccode>string\u003C\u002Fcode>要求等号右边算出来必须是字符串）；而\u003Ccode>assignStmt\u003C\u002Fcode>和\u003Ccode>print\u003C\u002Fcode>原本按类型分开纯粹是语法层面的将就，一旦等号（或括号）右边能表达任意类型，这种拆分就没有必要了，合并之后代码反而更简洁。\u003C\u002Fp>\n\u003Ch2 id=\"toc-4\">四、语法实现\u003C\u002Fh2>\n\u003Ch3 id=\"toc-5\">1. 字符串\u002F布尔字面量求值，以及一个悄悄存在的历史遗留问题\u003C\u002Fh3>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F**\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> * 字符串字面量，去掉两端的引号得到真正的文本内容。以前STRING的原始文本（带引号）会被\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> * 一路带着跑到打印的地方才显示出来，现在字符串要参与拼接，引号必须在这里就去掉，\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> * 否则\"a\" + \"b\"拼出来的会是带着引号的\"a\"\"b\"，不符合直觉\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> *\u002F\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">@\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> VisitorResult \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">visitStringValue\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(RuleSetParser.StringValueContext ctx) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    String text \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getText\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> VisitorResult.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">value\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(text.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">substring\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">1\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, text.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">length\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">-\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">@\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> VisitorResult \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">visitBoolValue\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(RuleSetParser.BoolValueContext ctx) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> VisitorResult.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">value\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(Boolean.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">parseBoolean\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getText\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cblockquote>\n \u003Cp>关于引号：细心的读者可能记得，从第③篇文章开始，字符串变量存的其实是带引号的原始文本，打印出来也是带引号的（比如之前文章里的执行结果\u003Ccode>\"字符串\"\u003C\u002Fcode>）。当时这么做纯粹是图省事——反正字符串只会被原样打印，长什么样打出来就是什么样，没人会较真。但现在字符串要参与拼接，这个\"省事\"的选择就变成了债——如果不在这里去掉引号，\u003Ccode>\"Hello, \" + name\u003C\u002Fcode>拼出来会变成\u003Ccode>\"Hello, \"\"World\"\u003C\u002Fcode>这种带着引号疙瘩的怪东西。所以这篇文章顺便把这个历史遗留问题一起修正了：从这篇开始，字符串打印出来不再带引号，这是语义上更正确的行为。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch3 id=\"toc-6\">2. ADD运算符的字符串拼接语义\u003C\u002Fh3>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">@\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> VisitorResult \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">visitAddAndSub\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(RuleSetParser.AddAndSubContext ctx) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    VisitorResult left \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> visit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">calcu\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">0\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    VisitorResult right \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> visit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">calcu\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">1\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (ctx.opt.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getType\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> RuleSetLexer.SUB) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> VisitorResult.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">value\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(left.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getNumber\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">-\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> right.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getNumber\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F ADD：只要两侧有一侧是字符串，就做字符串拼接（非字符串的一侧借助toString()转成文本），\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F 两侧都是数值时才是加法\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (left.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">isVarchar\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">||\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> right.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">isVarchar\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> VisitorResult.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">value\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(left.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">toString\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> right.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">toString\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> VisitorResult.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">value\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(left.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getNumber\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> right.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getNumber\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>SUB\u003C\u002Fcode>（减法）语义不变，永远要求两侧是数值。\u003Ccode>ADD\u003C\u002Fcode>则先判断两侧有没有字符串，只要有一侧是，就统一走拼接逻辑，非字符串的一侧借助\u003Ccode>VisitorResult.toString()\u003C\u002Fcode>（这个方法在前几篇文章里就已经实现好了，知道怎么把数值、布尔值格式化成文本）转换成文本再拼起来，所以\u003Ccode>\"Score: \" + 87\u003C\u002Fcode>能够正确地拼出\u003Ccode>\"Score: 87.0\"\u003C\u002Fcode>。\u003C\u002Fp>\n\u003Ch3 id=\"toc-7\">3. 合并之后的定义、赋值、打印\u003C\u002Fh3>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F**\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> * 定义字符型变量：等号右边现在是一个通用的calcu，可以是字符串字面量，也可以是字符串拼接\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> * 表达式（比如\"a\" + b），调用getString()既完成了求值，也顺带做了一次类型校验\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> *\u002F\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">@\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> VisitorResult \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">visitChararg\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(RuleSetParser.CharargContext ctx) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    currentScope.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">define\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx.id.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getText\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(), \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">visit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">calcu\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()).\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getString\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> VisitorResult.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">nil\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F**\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> * 定义布尔型变量：等号右边同样改成通用的calcu\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> *\u002F\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">@\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> VisitorResult \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">visitBoolarg\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(RuleSetParser.BoolargContext ctx) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    currentScope.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">define\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx.id.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getText\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(), \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">visit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">calcu\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()).\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getBool\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> VisitorResult.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">nil\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F**\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> * 给已定义的变量重新赋值。calcu现在能产生任意类型的结果，不再需要像以前那样区分\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> * numAssign\u002FcharAssign\u002FboolAssign，取出原始值直接沿作用域链写回去即可\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> *\u002F\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">@\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> VisitorResult \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">visitAssign\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(RuleSetParser.AssignContext ctx) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    VisitorResult value \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> visit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">calcu\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">    assign\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx.id.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getText\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(), value.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getValue\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> VisitorResult.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">nil\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F**\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> * 打印：value现在是通用的calcu，求值之后直接println——VisitorResult自己的toString()\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> * 知道该怎么把数值\u002F字符串\u002F布尔值\u002F数组\u002Fnil分别显示成什么样子\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> *\u002F\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">@\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Override\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> VisitorResult \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">visitPrintValue\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(RuleSetParser.PrintValueContext ctx) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">visit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">calcu\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> VisitorResult.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">nil\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>三个类型专属的\u003Ccode>visitNumAssign\u003C\u002Fcode>\u002F\u003Ccode>visitCharAssign\u003C\u002Fcode>\u002F\u003Ccode>visitBoolAssign\u003C\u002Fcode>合并成了一个\u003Ccode>visitAssign\u003C\u002Fcode>，三个\u003Ccode>visitPrintArg\u003C\u002Fcode>\u002F\u003Ccode>visitPrintBoolean\u003C\u002Fcode>\u002F\u003Ccode>visitPrintString\u003C\u002Fcode>合并成了一个\u003Ccode>visitPrintValue\u003C\u002Fcode>——这也是这次重构最直观的收益：代码量不增反减。\u003C\u002Fp>\n\u003Ch2 id=\"toc-8\">五、测试代码与执行结果\u003C\u002Fh2>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">String expression \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"string greeting = \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">Hello, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> \\n\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"string name = \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">World\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> \\n\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"print(greeting + name + \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">!\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">) \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"number score = 87 \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"print(\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">Score: \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> + score) \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"print(1 + 2 * 3) \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"array arr = [10, 20, 30] \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"print(arr[0] + arr[1]) \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"print(true) \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\n\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"greeting = \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">Hi, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> \\n\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">                \"print(greeting + name + \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">!\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">)\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">calcute\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(expression);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>执行结果如下：\u003C\u002Fp>\n\u003Cpre class=\"shiki shiki-themes github-light github-dark\" style=\"background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan>执行:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>string greeting = \"Hello, \"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>string name = \"World\"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>print(greeting + name + \"!\")\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>number score = 87\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>print(\"Score: \" + score)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>print(1 + 2 * 3)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>array arr = [10, 20, 30]\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>print(arr[0] + arr[1])\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>print(true)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>greeting = \"Hi, \"\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>print(greeting + name + \"!\")\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>Hello, World!\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>Score: 87.0\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>7.0\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>30.0\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>true\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>Hi, World!\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>greeting + name + \"!\"\u003C\u002Fcode>正确拼出了\u003Ccode>Hello, World!\u003C\u002Fcode>（字符串左结合，先拼\u003Ccode>greeting\u003C\u002Fcode>和\u003Ccode>name\u003C\u002Fcode>，再拼\u003Ccode>\"!\"\u003C\u002Fcode>）；\u003Ccode>\"Score: \" + score\u003C\u002Fcode>把数值\u003Ccode>87.0\u003C\u002Fcode>自动转成文本拼了上去；\u003Ccode>print(1 + 2 * 3)\u003C\u002Fcode>证明\u003Ccode>print\u003C\u002Fcode>现在可以直接打印一个表达式的计算结果而不需要中间变量；\u003Ccode>print(arr[0] + arr[1])\u003C\u002Fcode>证明数组下标访问的结果也能直接参与加法运算并打印；\u003Ccode>print(true)\u003C\u002Fcode>证明布尔字面量也能被\u003Ccode>calcu\u003C\u002Fcode>直接处理；最后用合并后的\u003Ccode>assign\u003C\u002Fcode>给\u003Ccode>greeting\u003C\u002Fcode>重新赋值成\u003Ccode>\"Hi, \"\u003C\u002Fcode>，再拼接一次得到\u003Ccode>Hi, World!\u003C\u002Fcode>，说明赋值合并之后行为依然正确。另外把之前几篇文章里函数递归、数组读写、\u003Ccode>while\u003C\u002Fcode>遍历、\u003Ccode>if\u003C\u002Fcode>条件的例子重新跑了一遍，结果和之前完全一致，这次重构没有引入回归。\u003C\u002Fp>\n\u003Ch2 id=\"toc-9\">六、遗留的问题\u003C\u002Fh2>\n\u003Cp>到这里，我们的小型DSL已经把数值、字符串、布尔值、数组这几种类型，统一在了同一套通用表达式（\u003Ccode>calcu\u003C\u002Fcode>）之上，\u003Ccode>setArg\u003C\u002Fcode>\u002F\u003Ccode>assignStmt\u003C\u002Fcode>\u002F\u003Ccode>print\u003C\u002Fcode>也都因此变得更简洁。不过\u003Ccode>cond\u003C\u002Fcode>（\u003Ccode>if\u003C\u002Fcode>\u002F\u003Ccode>while\u003C\u002Fcode>的条件表达式）目前还是一套独立的语法，比较运算符、逻辑运算符都定义在\u003Ccode>cond\u003C\u002Fcode>里而不是\u003Ccode>calcu\u003C\u002Fcode>里——这在当初是为了让\u003Ccode>calcu\u003C\u002Fcode>保持\"纯数值运算\"的单纯，现在\u003Ccode>calcu\u003C\u002Fcode>已经是通用表达式了，\u003Ccode>cond\u003C\u002Fcode>和\u003Ccode>calcu\u003C\u002Fcode>之间这条边界还要不要继续保留，是个值得继续思考的问题，这里先留给读者自己想一想，本系列到这里先告一段落。回顾整个系列，从最初一个只会做加减乘除的计算器，到现在拥有变量、分支、循环、作用域、函数递归、数组、内置函数、通用表达式，甚至还有一个能设断点、单步调试的调试器——希望这个循序渐进的过程，能帮助你比较扎实地理解Antlr4从语法设计到解释执行的完整链路。\u003C\u002Fp>",[83,87,90,93,96,100,103,106,109],{"id":84,"text":85,"level":86},"toc-1","一、问题：类型信息卡在了语法层面",2,{"id":88,"text":89,"level":86},"toc-2","二、解决思路",{"id":91,"text":92,"level":86},"toc-3","三、语法定义",{"id":94,"text":95,"level":86},"toc-4","四、语法实现",{"id":97,"text":98,"level":99},"toc-5","1. 字符串\u002F布尔字面量求值，以及一个悄悄存在的历史遗留问题",3,{"id":101,"text":102,"level":99},"toc-6","2. ADD运算符的字符串拼接语义",{"id":104,"text":105,"level":99},"toc-7","3. 合并之后的定义、赋值、打印",{"id":107,"text":108,"level":86},"toc-8","五、测试代码与执行结果",{"id":110,"text":111,"level":86},"toc-9","六、遗留的问题"]