[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site-settings":3,"$f2hixeuhal1yv1":14,"$f2l63mzp595fcd":31,"$f30h0os86audbo":56,"$f2hf8zv88r59zg":74,"render-antlr4-07-functions-and-call-stack":87},{"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},"2101619933819879425","antlr4-07-functions-and-call-stack","Antlr4系列⑦：自定义函数与调用栈","function定义\u002F调用、递归、词法作用域、调用栈防死递归",null,"1946000000000000001",1,6508,17,"2026-09-20T18:33:25","在上一篇文章中我们给语言加上了作用域链，解决了变量定义和赋值混淆、代码块变量泄漏的问题。到目前为止，我们的小型DSL已经具备了变量、四则运算、字符串\u002F布尔类型、分支、循环这些编程语言的基本要素，但还缺一样重要的东西——**函数**。本篇文章我们就来实现自定义函数：函数的定义、参数传递、`return`返回值，并支持递归调用。\n\n## 一、语法设计\n\n1. 函数定义\n\n    参考主流语言设计`function 函数名(参数1, 参数2, ...) { 语句 }`，为了不让本篇的内容过于膨胀，这里做一个简化：函数的参数和返回值统一按数值（`calcu`）处理，暂不支持字符串、布尔类型的参数和返回值。\n\n2. 函数调用\n\n    函数调用形如`函数名(参数1, 参数2, ...)`，可以作为`calcu`表达式的一部分参与四则运算（比如`n * factorial(n - 1)`），也可以单独作为一条语句调用（不关心返回值，只是为了利用函数体内的副作用，比如打印）。\n\n3. return语句\n\n    使用`return`关键字返回一个值，也可以不带值，表示函数执行到此结束但没有返回值。\n\n4. 函数需要先定义后调用\n\n    和大多数脚本语言顶层代码从上往下顺序执行的习惯一致，函数必须写在调用它的代码之前，暂不支持前向引用。\n\n## 二、语法定义\n\n根据以上设计新增语法，完善后的结果如下（只列出本篇新增\u002F改动的部分，完整文件见仓库）：\n```\nmain: setArg SEMI?\n    | assignStmt SEMI?\n    | print SEMI?\n    | ifStmt\n    | whileStmt\n    | breakStmt\n    | continueStmt\n    | funcDecl                                              \u002F\u002F 新增\n    | returnStmt SEMI?                                      \u002F\u002F 新增\n    | calcu SEMI?                                           \u002F\u002F 新增：表达式语句，用于不关心返回值、单纯调用一个函数的场景\n    ;\n\n\u002F\u002F 新增：函数定义，参数列表可以为空\nfuncDecl: FUNCTION name = ID '(' paramList? ')' block\n        ;\nparamList: ID (',' ID)*\n         ;\n\n\u002F\u002F 新增：return语句，可以不带返回值\nreturnStmt: RETURN calcu?\n          ;\n\ncalcu: calcu opt=(MUL|DIV) calcu                          # mulAndDiv\n     | calcu opt=(ADD|SUB) calcu                          # addAndSub\n     | '(' calcu ')'                                      # parens\n     | NUMBER                                             # number\n     | ID '(' (calcu (',' calcu)*)? ')'                   # funcCall  \u002F\u002F 新增\n     | ID                                                 # argValue\n     ;\n\n\u002F\u002F以下为本篇新增词法\nFUNCTION : 'function' ;\nRETURN : 'return' ;\n```\n接下来对本次新增的部分进行解释。\n\n1. `main`新增了三个分支：`funcDecl`（函数定义）、`returnStmt`（return语句）、`calcu SEMI?`（表达式语句）。其中`calcu SEMI?`这个分支值得说一下：`print(...)`这类写法本质上也是一种\"只是为了产生副作用、不关心结果\"的语句，这里我们用更通用的方式，让任意一个`calcu`表达式都可以独立作为一条语句出现，最直接的用途就是像`tryReadY()`这样单独调用一个函数而不使用它的返回值。\n\n2. `funcCall`被定义成`calcu`的一个新分支，意味着函数调用可以出现在任何`calcu`能出现的位置——可以是`number result = factorial(5)`里等号右边的一部分，也可以是`n * factorial(n - 1)`这样嵌套在四则运算里的一部分，这也是后面能够写出递归函数的语法基础。\n\n3. `ID '(' ... ')'`（函数调用）和`ID`（变量，即`argValue`）都是以`ID`开头，antlr4会根据紧跟在`ID`后面的到底是不是`(`来自动判断走哪个分支，不需要我们操心。\n\n## 三、语法实现\n\n接下来使用idea插件将定义好的.g4文件重新生成java文件覆盖之前的文件。函数的实现涉及三个关键设计点，逐一说明。\n\n### 1. return如何跳出函数\n\n`return`可能写在函数体内部很深的`if`\u002F`while`嵌套里，和上一篇`break`\u002F`continue`的处理思路完全一样：用异常把控制权一路传递回函数调用的地方。\n\n```java\n\u002F**\n * return语句使用的控制流异常，命中return时抛出，携带返回值，由函数调用处捕获\n *\u002F\npublic class ReturnException extends RuntimeException {\n\n    private final VisitorResult value;\n\n    public ReturnException(VisitorResult value) {\n        this.value = value;\n    }\n\n    public VisitorResult getValue() {\n        return value;\n    }\n\n    @Override\n    public synchronized Throwable fillInStackTrace() {\n        return this;\n    }\n}\n```\n\n### 2. 函数体的作用域：词法作用域 而不是 动态作用域\n\n这是本篇最容易踩坑的地方。函数体执行的时候，新建的作用域`parent`应该指向谁？直觉上可能会写成`new Scope(currentScope)`——也就是\"调用方当时所在的作用域\"，这样写编译不会报错，简单的例子也能跑通，但存在一个严重的问题：如果调用方在自己的局部作用域里定义了一个变量，恰好和函数内部用到的某个变量重名，函数就会**意外地**读到调用方的局部变量，而不是报\"未定义\"的错误。这种\"函数能看到调用它的地方的局部变量\"的行为叫**动态作用域**，绝大多数现代编程语言都不是这样设计的。\n\n正确的做法是**词法作用域**：函数只能访问自己定义时所在的作用域（在我们的实现里，函数只能在最外层定义，也就是全局作用域）和自己的参数，访问不到调用方的局部变量。因此函数执行时新作用域的`parent`要固定指向`globalScope`，而不是`currentScope`：\n\n```java\n\u002F**\n * 最外层的全局作用域，函数体执行时的父作用域固定指向它，而不是调用者当时所在的作用域，\n * 这样函数内部只能访问全局变量和自己的参数，访问不到调用者的局部变量（词法作用域而非动态作用域）\n *\u002F\nprivate final Scope globalScope = new Scope(null);\n\nprivate Scope currentScope = globalScope;\n```\n\n### 3. 调用栈：让死递归报错更友好\n\n递归函数一旦忘了写终止条件，会无限调用下去，Java本身会抛出`StackOverflowError`，报错信息对使用者不太友好。这里自己维护一个简单的调用栈，记录当前调用链上的函数名，超过阈值就主动抛出一个更清晰的异常：\n\n```java\n\u002F**\n * 已定义的函数，key为函数名，value为函数定义语法树节点，函数需要先定义后调用\n *\u002F\nprivate final Map\u003CString, RuleSetParser.FuncDeclContext> funcDefine = new HashMap\u003C>();\n\n\u002F**\n * 调用栈，保存当前调用链上的函数名，用于在递归层级过深时给出比Java原生StackOverflowError更友好的报错\n *\u002F\nprivate final Deque\u003CString> callStack = new ArrayDeque\u003C>();\n\nprivate static final int MAX_CALL_DEPTH = 1000;\n```\n\n把以上三点串起来，完整的函数定义、调用实现如下：\n\n```java\n\u002F**\n * 函数定义，只是把函数名和语法树节点记录下来，真正的执行发生在被调用的时候，因此函数需要先定义后调用\n *\u002F\n@Override\npublic VisitorResult visitFuncDecl(RuleSetParser.FuncDeclContext ctx) {\n    funcDefine.put(ctx.name.getText(), ctx);\n    return VisitorResult.nil();\n}\n\n\u002F**\n * return语句，把返回值包装进ReturnException抛出，由函数调用处捕获\n *\u002F\n@Override\npublic VisitorResult visitReturnStmt(RuleSetParser.ReturnStmtContext ctx) {\n    VisitorResult value = ctx.calcu() == null ? VisitorResult.nil() : visit(ctx.calcu());\n    throw new ReturnException(value);\n}\n\n\u002F**\n * 函数调用：\n * 1. 参数表达式必须在调用方当前作用域里求值，不能等切换到函数自己的作用域之后再算，\n *    否则参数表达式里如果引用了调用方的局部变量将无法解析。\n * 2. 函数执行时使用的新作用域的parent固定是globalScope而不是调用方的currentScope，\n *    这样函数内部只能看到全局变量和自己的参数，看不到调用方的局部变量。\n * 3. 用callStack记录调用深度，超过阈值主动抛出异常，避免死递归时让Java抛出不友好的StackOverflowError。\n * 4. block执行时如果遇到return会抛出ReturnException，在这里捕获取出返回值；\n *    如果一直执行到block结束都没有return，则函数调用的结果视为VisitorResult.nil()。\n *\u002F\n@Override\npublic VisitorResult visitFuncCall(RuleSetParser.FuncCallContext ctx) {\n    String funcName = ctx.ID().getText();\n    RuleSetParser.FuncDeclContext funcDecl = funcDefine.get(funcName);\n    if (funcDecl == null) {\n        throw new RuntimeException(\"未定义的函数：\" + funcName);\n    }\n    List\u003CString> paramNames = funcDecl.paramList() == null\n            ? Collections.emptyList()\n            : paramNames(funcDecl.paramList());\n    List\u003CRuleSetParser.CalcuContext> argExprs = ctx.calcu();\n    if (argExprs.size() != paramNames.size()) {\n        throw new RuntimeException(\"函数\" + funcName + \"需要\" + paramNames.size() + \"个参数，实际传入\" + argExprs.size() + \"个\");\n    }\n    if (callStack.size() >= MAX_CALL_DEPTH) {\n        throw new RuntimeException(\"函数调用层级超过\" + MAX_CALL_DEPTH + \"层，可能存在死递归：\" + callStack);\n    }\n    List\u003CDouble> argValues = new ArrayList\u003C>(argExprs.size());\n    for (RuleSetParser.CalcuContext argExpr : argExprs) {\n        argValues.add(visit(argExpr).getNumber());\n    }\n\n    Scope callerScope = currentScope;\n    Scope funcScope = new Scope(globalScope);\n    for (int i = 0; i \u003C paramNames.size(); i++) {\n        funcScope.define(paramNames.get(i), argValues.get(i));\n    }\n\n    callStack.push(funcName);\n    currentScope = funcScope;\n    try {\n        visit(funcDecl.block());\n        return VisitorResult.nil();\n    } catch (ReturnException e) {\n        return e.getValue();\n    } finally {\n        currentScope = callerScope;\n        callStack.pop();\n    }\n}\n\nprivate List\u003CString> paramNames(RuleSetParser.ParamListContext paramList) {\n    List\u003CString> names = new ArrayList\u003C>();\n    paramList.ID().forEach(id -> names.add(id.getText()));\n    return names;\n}\n```\n> 关于参数求值的顺序：注意参数表达式`argValues`是在切换`currentScope`**之前**求值的。如果先切换作用域再计算参数，参数表达式里但凡引用了调用方的局部变量（比如`add(x + 1, y)`里的`x`、`y`），就会因为找不到而报错——因为切换之后`currentScope`已经变成了只能看到全局作用域的函数作用域。\n\n## 四、测试代码与执行结果\n\n用三个例子分别验证：普通函数调用、递归函数、以及词法作用域是否生效。\n\n```java\nString addExpression =\n        \"function add(a, b) { \\n\" +\n                \"    return a + b \\n\" +\n                \"} \\n\" +\n                \"number sum = add(3, 4) \\n\" +\n                \"print(sum)\";\ncalcute(addExpression);\n\nString factorialExpression =\n        \"function factorial(n) { \\n\" +\n                \"    if (n \u003C= 1) { \\n\" +\n                \"        return 1 \\n\" +\n                \"    } \\n\" +\n                \"    return n * factorial(n - 1) \\n\" +\n                \"} \\n\" +\n                \"number result = factorial(5) \\n\" +\n                \"print(result)\";\ncalcute(factorialExpression);\n\nString scopeExpression =\n        \"function tryReadY() { \\n\" +\n                \"    print(y) \\n\" +\n                \"} \\n\" +\n                \"if (true) { \\n\" +\n                \"    number y = 1 \\n\" +\n                \"    tryReadY() \\n\" +\n                \"}\";\ncalcute(scopeExpression);\n```\n\n执行结果如下：\n\n```\n执行:\nfunction add(a, b) {\n    return a + b\n}\nnumber sum = add(3, 4)\nprint(sum)\n\n7.0\n执行:\nfunction factorial(n) {\n    if (n \u003C= 1) {\n        return 1\n    }\n    return n * factorial(n - 1)\n}\nnumber result = factorial(5)\nprint(result)\n\n120.0\n执行:\nfunction tryReadY() {\n    print(y)\n}\nif (true) {\n    number y = 1\n    tryReadY()\n}\n\nException in thread \"main\" java.lang.RuntimeException: 未定义的变量：y\n\tat cn.irule.MyRuleSetVisitor.visitPrintArg(MyRuleSetVisitor.java:127)\n\t......\n\tat cn.irule.MyRuleSetVisitor.visitFuncCall(MyRuleSetVisitor.java:408)\n\t......\n```\n\n第一个例子`add(3, 4)`打印出`7.0`；第二个例子`factorial(5)`通过`n * factorial(n - 1)`递归计算阶乘，打印出`120.0`，说明函数调用可以正确嵌套在四则运算里，也支持递归；第三个例子里，`y`是在调用`tryReadY()`的`if`代码块里定义的局部变量，`tryReadY`函数体内直接访问`y`并没有\"意外地\"拿到调用方的`1`，而是正确地报出\"未定义的变量：y\"——证明我们的函数确实是词法作用域，而不是动态作用域，符合预期。\n\n## 五、遗留的问题\n\n到这里，我们的小型DSL已经是一门\"五脏俱全\"的编程语言了：变量、四则运算、条件、循环、作用域、函数递归，一应俱全。也正是因为有了函数调用和调用栈的概念，接下来终于可以着手实现一个有实际意义的功能——**单步调试**：设置断点、单步执行、查看调用栈和变量值。下一篇文章我们将实现调试器的第一部分：断点和单步执行的基础机制。\n","\u003Cp>在上一篇文章中我们给语言加上了作用域链，解决了变量定义和赋值混淆、代码块变量泄漏的问题。到目前为止，我们的小型DSL已经具备了变量、四则运算、字符串\u002F布尔类型、分支、循环这些编程语言的基本要素，但还缺一样重要的东西——\u003Cstrong>函数\u003C\u002Fstrong>。本篇文章我们就来实现自定义函数：函数的定义、参数传递、\u003Ccode>return\u003C\u002Fcode>返回值，并支持递归调用。\u003C\u002Fp>\n\u003Ch2>一、语法设计\u003C\u002Fh2>\n\u003Col>\n \u003Cli>\n  \u003Cp>函数定义\u003C\u002Fp>\n  \u003Cp>参考主流语言设计\u003Ccode>function 函数名(参数1, 参数2, ...) { 语句 }\u003C\u002Fcode>，为了不让本篇的内容过于膨胀，这里做一个简化：函数的参数和返回值统一按数值（\u003Ccode>calcu\u003C\u002Fcode>）处理，暂不支持字符串、布尔类型的参数和返回值。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>函数调用\u003C\u002Fp>\n  \u003Cp>函数调用形如\u003Ccode>函数名(参数1, 参数2, ...)\u003C\u002Fcode>，可以作为\u003Ccode>calcu\u003C\u002Fcode>表达式的一部分参与四则运算（比如\u003Ccode>n * factorial(n - 1)\u003C\u002Fcode>），也可以单独作为一条语句调用（不关心返回值，只是为了利用函数体内的副作用，比如打印）。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>return语句\u003C\u002Fp>\n  \u003Cp>使用\u003Ccode>return\u003C\u002Fcode>关键字返回一个值，也可以不带值，表示函数执行到此结束但没有返回值。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>函数需要先定义后调用\u003C\u002Fp>\n  \u003Cp>和大多数脚本语言顶层代码从上往下顺序执行的习惯一致，函数必须写在调用它的代码之前，暂不支持前向引用。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>二、语法定义\u003C\u002Fh2>\n\u003Cp>根据以上设计新增语法，完善后的结果如下（只列出本篇新增\u002F改动的部分，完整文件见仓库）：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>main: setArg SEMI?\n    | assignStmt SEMI?\n    | print SEMI?\n    | ifStmt\n    | whileStmt\n    | breakStmt\n    | continueStmt\n    | funcDecl                                              \u002F\u002F 新增\n    | returnStmt SEMI?                                      \u002F\u002F 新增\n    | calcu SEMI?                                           \u002F\u002F 新增：表达式语句，用于不关心返回值、单纯调用一个函数的场景\n    ;\n\n\u002F\u002F 新增：函数定义，参数列表可以为空\nfuncDecl: FUNCTION name = ID '(' paramList? ')' block\n        ;\nparamList: ID (',' ID)*\n         ;\n\n\u002F\u002F 新增：return语句，可以不带返回值\nreturnStmt: RETURN calcu?\n          ;\n\ncalcu: calcu opt=(MUL|DIV) calcu                          # mulAndDiv\n     | calcu opt=(ADD|SUB) calcu                          # addAndSub\n     | '(' calcu ')'                                      # parens\n     | NUMBER                                             # number\n     | ID '(' (calcu (',' calcu)*)? ')'                   # funcCall  \u002F\u002F 新增\n     | ID                                                 # argValue\n     ;\n\n\u002F\u002F以下为本篇新增词法\nFUNCTION : 'function' ;\nRETURN : 'return' ;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>接下来对本次新增的部分进行解释。\u003C\u002Fp>\n\u003Col>\n \u003Cli>\n  \u003Cp>\u003Ccode>main\u003C\u002Fcode>新增了三个分支：\u003Ccode>funcDecl\u003C\u002Fcode>（函数定义）、\u003Ccode>returnStmt\u003C\u002Fcode>（return语句）、\u003Ccode>calcu SEMI?\u003C\u002Fcode>（表达式语句）。其中\u003Ccode>calcu SEMI?\u003C\u002Fcode>这个分支值得说一下：\u003Ccode>print(...)\u003C\u002Fcode>这类写法本质上也是一种\"只是为了产生副作用、不关心结果\"的语句，这里我们用更通用的方式，让任意一个\u003Ccode>calcu\u003C\u002Fcode>表达式都可以独立作为一条语句出现，最直接的用途就是像\u003Ccode>tryReadY()\u003C\u002Fcode>这样单独调用一个函数而不使用它的返回值。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Ccode>funcCall\u003C\u002Fcode>被定义成\u003Ccode>calcu\u003C\u002Fcode>的一个新分支，意味着函数调用可以出现在任何\u003Ccode>calcu\u003C\u002Fcode>能出现的位置——可以是\u003Ccode>number result = factorial(5)\u003C\u002Fcode>里等号右边的一部分，也可以是\u003Ccode>n * factorial(n - 1)\u003C\u002Fcode>这样嵌套在四则运算里的一部分，这也是后面能够写出递归函数的语法基础。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Ccode>ID '(' ... ')'\u003C\u002Fcode>（函数调用）和\u003Ccode>ID\u003C\u002Fcode>（变量，即\u003Ccode>argValue\u003C\u002Fcode>）都是以\u003Ccode>ID\u003C\u002Fcode>开头，antlr4会根据紧跟在\u003Ccode>ID\u003C\u002Fcode>后面的到底是不是\u003Ccode>(\u003C\u002Fcode>来自动判断走哪个分支，不需要我们操心。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>三、语法实现\u003C\u002Fh2>\n\u003Cp>接下来使用idea插件将定义好的.g4文件重新生成java文件覆盖之前的文件。函数的实现涉及三个关键设计点，逐一说明。\u003C\u002Fp>\n\u003Ch3>1. return如何跳出函数\u003C\u002Fh3>\n\u003Cp>\u003Ccode>return\u003C\u002Fcode>可能写在函数体内部很深的\u003Ccode>if\u003C\u002Fcode>\u002F\u003Ccode>while\u003C\u002Fcode>嵌套里，和上一篇\u003Ccode>break\u003C\u002Fcode>\u002F\u003Ccode>continue\u003C\u002Fcode>的处理思路完全一样：用异常把控制权一路传递回函数调用的地方。\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F**\n * return语句使用的控制流异常，命中return时抛出，携带返回值，由函数调用处捕获\n *\u002F\npublic class ReturnException extends RuntimeException {\n\n    private final VisitorResult value;\n\n    public ReturnException(VisitorResult value) {\n        this.value = value;\n    }\n\n    public VisitorResult getValue() {\n        return value;\n    }\n\n    @Override\n    public synchronized Throwable fillInStackTrace() {\n        return this;\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>2. 函数体的作用域：词法作用域 而不是 动态作用域\u003C\u002Fh3>\n\u003Cp>这是本篇最容易踩坑的地方。函数体执行的时候，新建的作用域\u003Ccode>parent\u003C\u002Fcode>应该指向谁？直觉上可能会写成\u003Ccode>new Scope(currentScope)\u003C\u002Fcode>——也就是\"调用方当时所在的作用域\"，这样写编译不会报错，简单的例子也能跑通，但存在一个严重的问题：如果调用方在自己的局部作用域里定义了一个变量，恰好和函数内部用到的某个变量重名，函数就会\u003Cstrong>意外地\u003C\u002Fstrong>读到调用方的局部变量，而不是报\"未定义\"的错误。这种\"函数能看到调用它的地方的局部变量\"的行为叫\u003Cstrong>动态作用域\u003C\u002Fstrong>，绝大多数现代编程语言都不是这样设计的。\u003C\u002Fp>\n\u003Cp>正确的做法是\u003Cstrong>词法作用域\u003C\u002Fstrong>：函数只能访问自己定义时所在的作用域（在我们的实现里，函数只能在最外层定义，也就是全局作用域）和自己的参数，访问不到调用方的局部变量。因此函数执行时新作用域的\u003Ccode>parent\u003C\u002Fcode>要固定指向\u003Ccode>globalScope\u003C\u002Fcode>，而不是\u003Ccode>currentScope\u003C\u002Fcode>：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F**\n * 最外层的全局作用域，函数体执行时的父作用域固定指向它，而不是调用者当时所在的作用域，\n * 这样函数内部只能访问全局变量和自己的参数，访问不到调用者的局部变量（词法作用域而非动态作用域）\n *\u002F\nprivate final Scope globalScope = new Scope(null);\n\nprivate Scope currentScope = globalScope;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>3. 调用栈：让死递归报错更友好\u003C\u002Fh3>\n\u003Cp>递归函数一旦忘了写终止条件，会无限调用下去，Java本身会抛出\u003Ccode>StackOverflowError\u003C\u002Fcode>，报错信息对使用者不太友好。这里自己维护一个简单的调用栈，记录当前调用链上的函数名，超过阈值就主动抛出一个更清晰的异常：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F**\n * 已定义的函数，key为函数名，value为函数定义语法树节点，函数需要先定义后调用\n *\u002F\nprivate final Map&lt;String, RuleSetParser.FuncDeclContext&gt; funcDefine = new HashMap&lt;&gt;();\n\n\u002F**\n * 调用栈，保存当前调用链上的函数名，用于在递归层级过深时给出比Java原生StackOverflowError更友好的报错\n *\u002F\nprivate final Deque&lt;String&gt; callStack = new ArrayDeque&lt;&gt;();\n\nprivate static final int MAX_CALL_DEPTH = 1000;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>把以上三点串起来，完整的函数定义、调用实现如下：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F**\n * 函数定义，只是把函数名和语法树节点记录下来，真正的执行发生在被调用的时候，因此函数需要先定义后调用\n *\u002F\n@Override\npublic VisitorResult visitFuncDecl(RuleSetParser.FuncDeclContext ctx) {\n    funcDefine.put(ctx.name.getText(), ctx);\n    return VisitorResult.nil();\n}\n\n\u002F**\n * return语句，把返回值包装进ReturnException抛出，由函数调用处捕获\n *\u002F\n@Override\npublic VisitorResult visitReturnStmt(RuleSetParser.ReturnStmtContext ctx) {\n    VisitorResult value = ctx.calcu() == null ? VisitorResult.nil() : visit(ctx.calcu());\n    throw new ReturnException(value);\n}\n\n\u002F**\n * 函数调用：\n * 1. 参数表达式必须在调用方当前作用域里求值，不能等切换到函数自己的作用域之后再算，\n *    否则参数表达式里如果引用了调用方的局部变量将无法解析。\n * 2. 函数执行时使用的新作用域的parent固定是globalScope而不是调用方的currentScope，\n *    这样函数内部只能看到全局变量和自己的参数，看不到调用方的局部变量。\n * 3. 用callStack记录调用深度，超过阈值主动抛出异常，避免死递归时让Java抛出不友好的StackOverflowError。\n * 4. block执行时如果遇到return会抛出ReturnException，在这里捕获取出返回值；\n *    如果一直执行到block结束都没有return，则函数调用的结果视为VisitorResult.nil()。\n *\u002F\n@Override\npublic VisitorResult visitFuncCall(RuleSetParser.FuncCallContext ctx) {\n    String funcName = ctx.ID().getText();\n    RuleSetParser.FuncDeclContext funcDecl = funcDefine.get(funcName);\n    if (funcDecl == null) {\n        throw new RuntimeException(\"未定义的函数：\" + funcName);\n    }\n    List&lt;String&gt; paramNames = funcDecl.paramList() == null\n            ? Collections.emptyList()\n            : paramNames(funcDecl.paramList());\n    List&lt;RuleSetParser.CalcuContext&gt; argExprs = ctx.calcu();\n    if (argExprs.size() != paramNames.size()) {\n        throw new RuntimeException(\"函数\" + funcName + \"需要\" + paramNames.size() + \"个参数，实际传入\" + argExprs.size() + \"个\");\n    }\n    if (callStack.size() &gt;= MAX_CALL_DEPTH) {\n        throw new RuntimeException(\"函数调用层级超过\" + MAX_CALL_DEPTH + \"层，可能存在死递归：\" + callStack);\n    }\n    List&lt;Double&gt; argValues = new ArrayList&lt;&gt;(argExprs.size());\n    for (RuleSetParser.CalcuContext argExpr : argExprs) {\n        argValues.add(visit(argExpr).getNumber());\n    }\n\n    Scope callerScope = currentScope;\n    Scope funcScope = new Scope(globalScope);\n    for (int i = 0; i &lt; paramNames.size(); i++) {\n        funcScope.define(paramNames.get(i), argValues.get(i));\n    }\n\n    callStack.push(funcName);\n    currentScope = funcScope;\n    try {\n        visit(funcDecl.block());\n        return VisitorResult.nil();\n    } catch (ReturnException e) {\n        return e.getValue();\n    } finally {\n        currentScope = callerScope;\n        callStack.pop();\n    }\n}\n\nprivate List&lt;String&gt; paramNames(RuleSetParser.ParamListContext paramList) {\n    List&lt;String&gt; names = new ArrayList&lt;&gt;();\n    paramList.ID().forEach(id -&gt; names.add(id.getText()));\n    return names;\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cblockquote>\n \u003Cp>关于参数求值的顺序：注意参数表达式\u003Ccode>argValues\u003C\u002Fcode>是在切换\u003Ccode>currentScope\u003C\u002Fcode>\u003Cstrong>之前\u003C\u002Fstrong>求值的。如果先切换作用域再计算参数，参数表达式里但凡引用了调用方的局部变量（比如\u003Ccode>add(x + 1, y)\u003C\u002Fcode>里的\u003Ccode>x\u003C\u002Fcode>、\u003Ccode>y\u003C\u002Fcode>），就会因为找不到而报错——因为切换之后\u003Ccode>currentScope\u003C\u002Fcode>已经变成了只能看到全局作用域的函数作用域。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2>四、测试代码与执行结果\u003C\u002Fh2>\n\u003Cp>用三个例子分别验证：普通函数调用、递归函数、以及词法作用域是否生效。\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">String addExpression =\n        \"function add(a, b) { \\n\" +\n                \"    return a + b \\n\" +\n                \"} \\n\" +\n                \"number sum = add(3, 4) \\n\" +\n                \"print(sum)\";\ncalcute(addExpression);\n\nString factorialExpression =\n        \"function factorial(n) { \\n\" +\n                \"    if (n &lt;= 1) { \\n\" +\n                \"        return 1 \\n\" +\n                \"    } \\n\" +\n                \"    return n * factorial(n - 1) \\n\" +\n                \"} \\n\" +\n                \"number result = factorial(5) \\n\" +\n                \"print(result)\";\ncalcute(factorialExpression);\n\nString scopeExpression =\n        \"function tryReadY() { \\n\" +\n                \"    print(y) \\n\" +\n                \"} \\n\" +\n                \"if (true) { \\n\" +\n                \"    number y = 1 \\n\" +\n                \"    tryReadY() \\n\" +\n                \"}\";\ncalcute(scopeExpression);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>执行结果如下：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>执行:\nfunction add(a, b) {\n    return a + b\n}\nnumber sum = add(3, 4)\nprint(sum)\n\n7.0\n执行:\nfunction factorial(n) {\n    if (n &lt;= 1) {\n        return 1\n    }\n    return n * factorial(n - 1)\n}\nnumber result = factorial(5)\nprint(result)\n\n120.0\n执行:\nfunction tryReadY() {\n    print(y)\n}\nif (true) {\n    number y = 1\n    tryReadY()\n}\n\nException in thread \"main\" java.lang.RuntimeException: 未定义的变量：y\n\tat cn.irule.MyRuleSetVisitor.visitPrintArg(MyRuleSetVisitor.java:127)\n\t......\n\tat cn.irule.MyRuleSetVisitor.visitFuncCall(MyRuleSetVisitor.java:408)\n\t......\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>第一个例子\u003Ccode>add(3, 4)\u003C\u002Fcode>打印出\u003Ccode>7.0\u003C\u002Fcode>；第二个例子\u003Ccode>factorial(5)\u003C\u002Fcode>通过\u003Ccode>n * factorial(n - 1)\u003C\u002Fcode>递归计算阶乘，打印出\u003Ccode>120.0\u003C\u002Fcode>，说明函数调用可以正确嵌套在四则运算里，也支持递归；第三个例子里，\u003Ccode>y\u003C\u002Fcode>是在调用\u003Ccode>tryReadY()\u003C\u002Fcode>的\u003Ccode>if\u003C\u002Fcode>代码块里定义的局部变量，\u003Ccode>tryReadY\u003C\u002Fcode>函数体内直接访问\u003Ccode>y\u003C\u002Fcode>并没有\"意外地\"拿到调用方的\u003Ccode>1\u003C\u002Fcode>，而是正确地报出\"未定义的变量：y\"——证明我们的函数确实是词法作用域，而不是动态作用域，符合预期。\u003C\u002Fp>\n\u003Ch2>五、遗留的问题\u003C\u002Fh2>\n\u003Cp>到这里，我们的小型DSL已经是一门\"五脏俱全\"的编程语言了：变量、四则运算、条件、循环、作用域、函数递归，一应俱全。也正是因为有了函数调用和调用栈的概念，接下来终于可以着手实现一个有实际意义的功能——\u003Cstrong>单步调试\u003C\u002Fstrong>：设置断点、单步执行、查看调用栈和变量值。下一篇文章我们将实现调试器的第一部分：断点和单步执行的基础机制。\u003C\u002Fp>",[29],"Antlr4","index,follow",{"code":4,"message":5,"data":32},[33,41,48],{"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":24,"publishedAt":47},"2101605987801022465","antlr4-04-if-else","Antlr4系列④：支持分支语句","比较运算符、逻辑运算符、if\u002Felse if\u002Felse、代码块block",6668,"2026-09-20T17:34:58",{"id":49,"slug":50,"title":51,"summary":52,"coverUrl":20,"categoryId":21,"status":22,"topFlag":4,"viewCount":4,"likeCount":4,"wordCount":53,"readingMinutes":54,"publishedAt":55},"2101635146136862722","antlr4-10-arrays","Antlr4系列⑩：支持数组","array类型、字面量、下标读写、len",5036,13,"2026-09-20T19:30:48",{"code":4,"message":5,"data":57},{"prev":58,"next":66},{"id":59,"slug":60,"title":61,"summary":62,"coverUrl":20,"categoryId":21,"status":22,"topFlag":4,"viewCount":4,"likeCount":4,"wordCount":63,"readingMinutes":64,"publishedAt":65},"2101615963315552258","antlr4-06-scope-chain","Antlr4系列⑥：作用域链与变量赋值","Scope作用域链、区分\"定义\"与\"赋值\"、修复变量泄漏",5701,15,"2026-09-20T18:14:34",{"id":67,"slug":68,"title":69,"summary":70,"coverUrl":20,"categoryId":21,"status":22,"topFlag":4,"viewCount":4,"likeCount":4,"wordCount":71,"readingMinutes":72,"publishedAt":73},"2101623894064939009","antlr4-08-debugger-breakpoints","Antlr4系列⑧：调试器基础之断点与单步执行","双线程+阻塞队列实现暂停\u002F恢复、断点、单步、变量查看",5558,14,"2026-09-20T18:46:06",{"code":4,"message":5,"data":75},[76,81],{"id":21,"name":77,"slug":78,"description":79,"sortOrder":4,"articleCount":80},"技术","tech","编程与技术笔记","12",{"id":82,"name":83,"slug":84,"description":85,"sortOrder":22,"articleCount":86},"1946000000000000002","随笔","essay","生活与思考","0",{"html":88,"toc":89},"\u003Cp>在上一篇文章中我们给语言加上了作用域链，解决了变量定义和赋值混淆、代码块变量泄漏的问题。到目前为止，我们的小型DSL已经具备了变量、四则运算、字符串\u002F布尔类型、分支、循环这些编程语言的基本要素，但还缺一样重要的东西——\u003Cstrong>函数\u003C\u002Fstrong>。本篇文章我们就来实现自定义函数：函数的定义、参数传递、\u003Ccode>return\u003C\u002Fcode>返回值，并支持递归调用。\u003C\u002Fp>\n\u003Ch2 id=\"toc-1\">一、语法设计\u003C\u002Fh2>\n\u003Col>\n \u003Cli>\n  \u003Cp>函数定义\u003C\u002Fp>\n  \u003Cp>参考主流语言设计\u003Ccode>function 函数名(参数1, 参数2, ...) { 语句 }\u003C\u002Fcode>，为了不让本篇的内容过于膨胀，这里做一个简化：函数的参数和返回值统一按数值（\u003Ccode>calcu\u003C\u002Fcode>）处理，暂不支持字符串、布尔类型的参数和返回值。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>函数调用\u003C\u002Fp>\n  \u003Cp>函数调用形如\u003Ccode>函数名(参数1, 参数2, ...)\u003C\u002Fcode>，可以作为\u003Ccode>calcu\u003C\u002Fcode>表达式的一部分参与四则运算（比如\u003Ccode>n * factorial(n - 1)\u003C\u002Fcode>），也可以单独作为一条语句调用（不关心返回值，只是为了利用函数体内的副作用，比如打印）。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>return语句\u003C\u002Fp>\n  \u003Cp>使用\u003Ccode>return\u003C\u002Fcode>关键字返回一个值，也可以不带值，表示函数执行到此结束但没有返回值。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>函数需要先定义后调用\u003C\u002Fp>\n  \u003Cp>和大多数脚本语言顶层代码从上往下顺序执行的习惯一致，函数必须写在调用它的代码之前，暂不支持前向引用。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"toc-2\">二、语法定义\u003C\u002Fh2>\n\u003Cp>根据以上设计新增语法，完善后的结果如下（只列出本篇新增\u002F改动的部分，完整文件见仓库）：\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>main: setArg SEMI?\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    | assignStmt SEMI?\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    | print SEMI?\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    | ifStmt\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    | whileStmt\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    | breakStmt\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    | continueStmt\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    | funcDecl                                              \u002F\u002F 新增\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    | returnStmt SEMI?                                      \u002F\u002F 新增\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    | calcu SEMI?                                           \u002F\u002F 新增：表达式语句，用于不关心返回值、单纯调用一个函数的场景\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 新增：函数定义，参数列表可以为空\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>funcDecl: FUNCTION name = ID '(' paramList? ')' block\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>        ;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>paramList: ID (',' ID)*\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 新增：return语句，可以不带返回值\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>returnStmt: RETURN calcu?\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>calcu: calcu opt=(MUL|DIV) calcu                          # mulAndDiv\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>     | calcu opt=(ADD|SUB) calcu                          # addAndSub\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>     | ID '(' (calcu (',' calcu)*)? ')'                   # funcCall  \u002F\u002F 新增\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>\n\u003Cspan class=\"line\">\u003Cspan>\u002F\u002F以下为本篇新增词法\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>FUNCTION : 'function' ;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>RETURN : 'return' ;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>接下来对本次新增的部分进行解释。\u003C\u002Fp>\n\u003Col>\n \u003Cli>\n  \u003Cp>\u003Ccode>main\u003C\u002Fcode>新增了三个分支：\u003Ccode>funcDecl\u003C\u002Fcode>（函数定义）、\u003Ccode>returnStmt\u003C\u002Fcode>（return语句）、\u003Ccode>calcu SEMI?\u003C\u002Fcode>（表达式语句）。其中\u003Ccode>calcu SEMI?\u003C\u002Fcode>这个分支值得说一下：\u003Ccode>print(...)\u003C\u002Fcode>这类写法本质上也是一种\"只是为了产生副作用、不关心结果\"的语句，这里我们用更通用的方式，让任意一个\u003Ccode>calcu\u003C\u002Fcode>表达式都可以独立作为一条语句出现，最直接的用途就是像\u003Ccode>tryReadY()\u003C\u002Fcode>这样单独调用一个函数而不使用它的返回值。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Ccode>funcCall\u003C\u002Fcode>被定义成\u003Ccode>calcu\u003C\u002Fcode>的一个新分支，意味着函数调用可以出现在任何\u003Ccode>calcu\u003C\u002Fcode>能出现的位置——可以是\u003Ccode>number result = factorial(5)\u003C\u002Fcode>里等号右边的一部分，也可以是\u003Ccode>n * factorial(n - 1)\u003C\u002Fcode>这样嵌套在四则运算里的一部分，这也是后面能够写出递归函数的语法基础。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>\u003Ccode>ID '(' ... ')'\u003C\u002Fcode>（函数调用）和\u003Ccode>ID\u003C\u002Fcode>（变量，即\u003Ccode>argValue\u003C\u002Fcode>）都是以\u003Ccode>ID\u003C\u002Fcode>开头，antlr4会根据紧跟在\u003Ccode>ID\u003C\u002Fcode>后面的到底是不是\u003Ccode>(\u003C\u002Fcode>来自动判断走哪个分支，不需要我们操心。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"toc-3\">三、语法实现\u003C\u002Fh2>\n\u003Cp>接下来使用idea插件将定义好的.g4文件重新生成java文件覆盖之前的文件。函数的实现涉及三个关键设计点，逐一说明。\u003C\u002Fp>\n\u003Ch3 id=\"toc-4\">1. return如何跳出函数\u003C\u002Fh3>\n\u003Cp>\u003Ccode>return\u003C\u002Fcode>可能写在函数体内部很深的\u003Ccode>if\u003C\u002Fcode>\u002F\u003Ccode>while\u003C\u002Fcode>嵌套里，和上一篇\u003Ccode>break\u003C\u002Fcode>\u002F\u003Ccode>continue\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 style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F**\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> * return语句使用的控制流异常，命中return时抛出，携带返回值，由函数调用处捕获\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:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> ReturnException\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> extends\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> RuntimeException\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> VisitorResult value;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> ReturnException\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(VisitorResult \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">value\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">        this\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">.value \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> value;\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:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> VisitorResult \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\"> value;\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:#D73A49;--shiki-dark:#F97583\"> synchronized\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Throwable \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">fillInStackTrace\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:#005CC5;--shiki-dark:#79B8FF\"> this\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:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3 id=\"toc-5\">2. 函数体的作用域：词法作用域 而不是 动态作用域\u003C\u002Fh3>\n\u003Cp>这是本篇最容易踩坑的地方。函数体执行的时候，新建的作用域\u003Ccode>parent\u003C\u002Fcode>应该指向谁？直觉上可能会写成\u003Ccode>new Scope(currentScope)\u003C\u002Fcode>——也就是\"调用方当时所在的作用域\"，这样写编译不会报错，简单的例子也能跑通，但存在一个严重的问题：如果调用方在自己的局部作用域里定义了一个变量，恰好和函数内部用到的某个变量重名，函数就会\u003Cstrong>意外地\u003C\u002Fstrong>读到调用方的局部变量，而不是报\"未定义\"的错误。这种\"函数能看到调用它的地方的局部变量\"的行为叫\u003Cstrong>动态作用域\u003C\u002Fstrong>，绝大多数现代编程语言都不是这样设计的。\u003C\u002Fp>\n\u003Cp>正确的做法是\u003Cstrong>词法作用域\u003C\u002Fstrong>：函数只能访问自己定义时所在的作用域（在我们的实现里，函数只能在最外层定义，也就是全局作用域）和自己的参数，访问不到调用方的局部变量。因此函数执行时新作用域的\u003Ccode>parent\u003C\u002Fcode>要固定指向\u003Ccode>globalScope\u003C\u002Fcode>，而不是\u003Ccode>currentScope\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 style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F**\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\"> * 这样函数内部只能访问全局变量和自己的参数，访问不到调用者的局部变量（词法作用域而非动态作用域）\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:#D73A49;--shiki-dark:#F97583\">private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Scope globalScope \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Scope\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">null\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">private\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Scope currentScope \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> globalScope;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3 id=\"toc-6\">3. 调用栈：让死递归报错更友好\u003C\u002Fh3>\n\u003Cp>递归函数一旦忘了写终止条件，会无限调用下去，Java本身会抛出\u003Ccode>StackOverflowError\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 style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F**\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> * 已定义的函数，key为函数名，value为函数定义语法树节点，函数需要先定义后调用\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:#D73A49;--shiki-dark:#F97583\">private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Map&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">RuleSetParser\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">.\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">FuncDeclContext\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> funcDefine \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> HashMap&#x3C;>();\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\"> * 调用栈，保存当前调用链上的函数名，用于在递归层级过深时给出比Java原生StackOverflowError更友好的报错\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:#D73A49;--shiki-dark:#F97583\">private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Deque&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> callStack \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ArrayDeque&#x3C;>();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> int\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> MAX_CALL_DEPTH \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 1000\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\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 style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F**\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\"> *\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\">visitFuncDecl\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(RuleSetParser.FuncDeclContext ctx) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    funcDefine.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">put\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx.name.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getText\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(), 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\">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\"> * return语句，把返回值包装进ReturnException抛出，由函数调用处捕获\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\">visitReturnStmt\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(RuleSetParser.ReturnStmtContext 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:#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:#D73A49;--shiki-dark:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> null\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> ?\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>\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:#D73A49;--shiki-dark:#F97583\">    throw\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> ReturnException\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(value);\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\"> * 函数调用：\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> * 1. 参数表达式必须在调用方当前作用域里求值，不能等切换到函数自己的作用域之后再算，\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\"> * 2. 函数执行时使用的新作用域的parent固定是globalScope而不是调用方的currentScope，\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\"> * 3. 用callStack记录调用深度，超过阈值主动抛出异常，避免死递归时让Java抛出不友好的StackOverflowError。\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> * 4. block执行时如果遇到return会抛出ReturnException，在这里捕获取出返回值；\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> *    如果一直执行到block结束都没有return，则函数调用的结果视为VisitorResult.nil()。\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\">visitFuncCall\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(RuleSetParser.FuncCallContext ctx) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    String funcName \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\">ID\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">().\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\">    RuleSetParser.FuncDeclContext funcDecl \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> funcDefine.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">get\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(funcName);\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\"> (funcDecl \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> null\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        throw\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> RuntimeException\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"未定义的函数：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> funcName);\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:#24292E;--shiki-dark:#E1E4E8\">    List&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> paramNames \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> funcDecl.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">paramList\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\"> null\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            ?\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Collections.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">emptyList\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            :\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> paramNames\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(funcDecl.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">paramList\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    List&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">RuleSetParser\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">.\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">CalcuContext\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> argExprs \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\">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\">    if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (argExprs.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">size\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\"> paramNames.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">size\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        throw\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> RuntimeException\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"函数\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> funcName \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"需要\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> paramNames.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">size\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"个参数，实际传入\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> argExprs.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">size\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\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:#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\"> (callStack.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">size\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\"> MAX_CALL_DEPTH) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        throw\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> RuntimeException\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"函数调用层级超过\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> MAX_CALL_DEPTH \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">+\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\"> \"层，可能存在死递归：\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> callStack);\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:#24292E;--shiki-dark:#E1E4E8\">    List&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Double\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> argValues \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ArrayList&#x3C;>(argExprs.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">size\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    for\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (RuleSetParser.CalcuContext argExpr \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> argExprs) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        argValues.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">add\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\">(argExpr).\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>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    Scope callerScope \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> currentScope;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    Scope funcScope \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> Scope\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(globalScope);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    for\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">int\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> i \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> 0\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">; i \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> paramNames.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">size\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(); i\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">++\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        funcScope.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">define\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(paramNames.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">get\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(i), argValues.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">get\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(i));\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\">    callStack.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">push\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(funcName);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    currentScope \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> funcScope;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    try\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">        visit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(funcDecl.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">block\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>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">catch\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (ReturnException \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">e\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\"> e.\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:#24292E;--shiki-dark:#E1E4E8\">    } \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">finally\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        currentScope \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> callerScope;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        callStack.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">pop\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:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">private\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> List\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">String\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">>\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> paramNames\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(RuleSetParser.ParamListContext paramList) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    List&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> names \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> new\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ArrayList&#x3C;>();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    paramList.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">ID\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">().\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">forEach\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">->\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> names.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">add\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(id.\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\"> names;\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>argValues\u003C\u002Fcode>是在切换\u003Ccode>currentScope\u003C\u002Fcode>\u003Cstrong>之前\u003C\u002Fstrong>求值的。如果先切换作用域再计算参数，参数表达式里但凡引用了调用方的局部变量（比如\u003Ccode>add(x + 1, y)\u003C\u002Fcode>里的\u003Ccode>x\u003C\u002Fcode>、\u003Ccode>y\u003C\u002Fcode>），就会因为找不到而报错——因为切换之后\u003Ccode>currentScope\u003C\u002Fcode>已经变成了只能看到全局作用域的函数作用域。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2 id=\"toc-7\">四、测试代码与执行结果\u003C\u002Fh2>\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 style=\"color:#24292E;--shiki-dark:#E1E4E8\">String addExpression \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"function add(a, b) { \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\">                \"    return a + b \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\">                \"} \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 sum = add(3, 4) \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(sum)\"\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\">(addExpression);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">String factorialExpression \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"function factorial(n) { \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\">                \"    if (n &#x3C;= 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\">                \"        return 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\">                \"    } \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\">                \"    return n * factorial(n - 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\">                \"} \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 result = factorial(5) \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(result)\"\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\">(factorialExpression);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">String scopeExpression \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">        \"function tryReadY() { \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(y) \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\">                \"} \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\">                \"if (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\">                \"    number y = 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\">                \"    tryReadY() \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\">                \"}\"\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\">(scopeExpression);\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>function add(a, b) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    return a + b\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>number sum = add(3, 4)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>print(sum)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>7.0\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>执行:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>function factorial(n) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    if (n &#x3C;= 1) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>        return 1\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    return n * factorial(n - 1)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>number result = factorial(5)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>print(result)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>120.0\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>执行:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>function tryReadY() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    print(y)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>if (true) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    number y = 1\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    tryReadY()\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>Exception in thread \"main\" java.lang.RuntimeException: 未定义的变量：y\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\tat cn.irule.MyRuleSetVisitor.visitPrintArg(MyRuleSetVisitor.java:127)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\t......\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\tat cn.irule.MyRuleSetVisitor.visitFuncCall(MyRuleSetVisitor.java:408)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\t......\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>第一个例子\u003Ccode>add(3, 4)\u003C\u002Fcode>打印出\u003Ccode>7.0\u003C\u002Fcode>；第二个例子\u003Ccode>factorial(5)\u003C\u002Fcode>通过\u003Ccode>n * factorial(n - 1)\u003C\u002Fcode>递归计算阶乘，打印出\u003Ccode>120.0\u003C\u002Fcode>，说明函数调用可以正确嵌套在四则运算里，也支持递归；第三个例子里，\u003Ccode>y\u003C\u002Fcode>是在调用\u003Ccode>tryReadY()\u003C\u002Fcode>的\u003Ccode>if\u003C\u002Fcode>代码块里定义的局部变量，\u003Ccode>tryReadY\u003C\u002Fcode>函数体内直接访问\u003Ccode>y\u003C\u002Fcode>并没有\"意外地\"拿到调用方的\u003Ccode>1\u003C\u002Fcode>，而是正确地报出\"未定义的变量：y\"——证明我们的函数确实是词法作用域，而不是动态作用域，符合预期。\u003C\u002Fp>\n\u003Ch2 id=\"toc-8\">五、遗留的问题\u003C\u002Fh2>\n\u003Cp>到这里，我们的小型DSL已经是一门\"五脏俱全\"的编程语言了：变量、四则运算、条件、循环、作用域、函数递归，一应俱全。也正是因为有了函数调用和调用栈的概念，接下来终于可以着手实现一个有实际意义的功能——\u003Cstrong>单步调试\u003C\u002Fstrong>：设置断点、单步执行、查看调用栈和变量值。下一篇文章我们将实现调试器的第一部分：断点和单步执行的基础机制。\u003C\u002Fp>",[90,94,97,100,104,107,110,113],{"id":91,"text":92,"level":93},"toc-1","一、语法设计",2,{"id":95,"text":96,"level":93},"toc-2","二、语法定义",{"id":98,"text":99,"level":93},"toc-3","三、语法实现",{"id":101,"text":102,"level":103},"toc-4","1. return如何跳出函数",3,{"id":105,"text":106,"level":103},"toc-5","2. 函数体的作用域：词法作用域 而不是 动态作用域",{"id":108,"text":109,"level":103},"toc-6","3. 调用栈：让死递归报错更友好",{"id":111,"text":112,"level":93},"toc-7","四、测试代码与执行结果",{"id":114,"text":115,"level":93},"toc-8","五、遗留的问题"]