[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site-settings":3,"$f1vxxh2snk56lj":14,"$f2qhxk8nrihqnr":31,"$f390iz8jisekki":57,"$f2hf8zv88r59zg":67,"render-antlr4-09-debugger-step-over":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},"2101631008783122433","antlr4-09-debugger-step-over","Antlr4系列⑨：调试器进阶之Step Over与条件断点","Step Into\u002FOver\u002FOut、调用栈展示、条件断点",null,"1946000000000000001",1,5666,15,"2026-09-20T19:14:22","上一篇文章实现了调试器最基础的能力：断点和单步（严格来说是**Step Into**——不管下一条语句在哪一层调用深度，都会在那里暂停）。这篇文章我们把单步完善成三种更贴近真实调试器的方式——**Step Into \u002F Step Over \u002F Step Out**，并且加上**条件断点**和**调用栈展示**。同样，本篇不涉及.g4语法改动，都是宿主程序层面的增强。\n\n## 一、思路设计\n\n1. Step Into \u002F Over \u002F Out的区别\n\n    - **Step Into**（进入）：不管下一条语句处于多深的调用层级，走到哪就停在哪。如果下一条语句正好是一次函数调用，效果就是\"扎进函数体内部\"。\n    - **Step Over**（跳过）：把接下来遇到的函数调用当成一个整体执行完，不进入其内部，只在**调用深度回到不超过当前深度**时才暂停。\n    - **Step Out**（跳出）：不管函数体里还剩多少语句，一路跑到当前函数返回、**调用深度小于当前深度**（也就是回到了调用者所在的层级）才暂停。\n\n    可以看到，三者的区别本质上是\"暂停条件里对调用深度的要求不同\"，这也是上一篇文章特意先实现调用栈（`callStack`）的原因——没有调用深度这个概念，Step Over\u002FOut根本无从谈起。\n\n2. 条件断点\n\n    有时候我们只关心\"变量`i`等于3的时候到底发生了什么\"，而不想在循环的每一次都手动放行。条件断点就是在命中断点所在的行时，先按语言本身的语法把一个条件表达式求值一遍，只有为真时才真正暂停。\n\n## 二、语法实现\n\n### 1. DebugController：从\"是否单步\"升级成\"单步模式+目标深度\"\n\n```java\n\u002F**\n * 单步模式：NONE表示不处于单步（只按断点暂停）；\n * INTO表示下一条语句无论深度多少都暂停；\n * OVER表示只在深度回到不超过targetDepth时才暂停；\n * OUT表示只在深度小于targetDepth时才暂停\n *\u002F\nprivate enum StepMode {NONE, INTO, OVER, OUT}\n\nprivate volatile StepMode stepMode = StepMode.NONE;\nprivate volatile int targetDepth = 0;\n\n\u002F**\n * 由解释器线程调用：每执行一条语句之前检查是否需要暂停。\n * breakpointHit由调用方（MyRuleSetVisitor）提前算好传进来，因为条件断点的条件表达式\n * 需要用visitor自己去visit求值，DebugController本身并不持有visitor，没办法自己算。\n *\u002F\npublic void checkpoint(int line, boolean breakpointHit, int depth, List\u003CString> callStack, Scope scope) {\n    boolean shouldPause = breakpointHit || matchesStepMode(depth);\n    if (!shouldPause) {\n        return;\n    }\n    stepMode = StepMode.NONE;\n    try {\n        pauseSignal.put(new PauseInfo(line, depth, callStack, scope));\n        resumeSignal.take();\n    } catch (InterruptedException e) {\n        Thread.currentThread().interrupt();\n    }\n}\n\nprivate boolean matchesStepMode(int depth) {\n    switch (stepMode) {\n        case INTO:\n            return true;\n        case OVER:\n            return depth \u003C= targetDepth;\n        case OUT:\n            return depth \u003C targetDepth;\n        default:\n            return false;\n    }\n}\n\npublic void stepInto() {\n    stepMode = StepMode.INTO;\n    resumeSignal.offer(true);\n}\n\n\u002F**\n * @param currentDepth 发出指令时所在的调用深度，通常就是最近一次PauseInfo里的depth\n *\u002F\npublic void stepOver(int currentDepth) {\n    stepMode = StepMode.OVER;\n    targetDepth = currentDepth;\n    resumeSignal.offer(true);\n}\n\npublic void stepOut(int currentDepth) {\n    stepMode = StepMode.OUT;\n    targetDepth = currentDepth;\n    resumeSignal.offer(true);\n}\n```\n`targetDepth`记录的是**发出指令那一刻**所在的调用深度。以`stepOver`为例：命令发出时深度是`D`，只要接下来遇到的语句深度大于`D`（说明正处在被跳过的函数调用内部），就不理会；一旦某条语句的深度又回到了`≤ D`，说明已经跳出了那次调用（或者调用压根就没有发生更深的调用），此时暂停。`stepOut`同理，只是把\"回到不超过D\"换成了\"严格小于D\"——因为如果当前就在某次调用内部，\"跳出\"至少要回到比当前更浅一层才算数。\n\n### 2. 条件断点\n\n```java\nprivate final Map\u003CInteger, RuleSetParser.CondContext> conditionalBreakpoints = new ConcurrentHashMap\u003C>();\n\n\u002F**\n * 增加一个条件断点：只有condition在命中该行时求值为true才真正暂停。\n * condition是提前用RuleSetParser.cond()规则单独解析出来的语法树节点\n *\u002F\npublic void addConditionalBreakpoint(int line, RuleSetParser.CondContext condition) {\n    conditionalBreakpoints.put(line, condition);\n}\n```\n这里有个值得一提的细节：`condition`是提前单独解析好的一段`cond`语法树，而不是每次命中都重新解析文本。antlr4生成的`RuleSetParser`不仅有解析整个程序入口的`prog()`方法，语法文件里的每一条规则都会生成一个同名的公开方法（比如`cond()`、`calcu()`），可以直接拿来复用，单独解析一小段符合该规则的文本，不需要每次都套上一个完整的程序。设置条件断点时就是这么做的：\n\n```java\nRuleSetLexer condLexer = new RuleSetLexer(CharStreams.fromString(\"i == 3\"));\nRuleSetParser condParser = new RuleSetParser(new CommonTokenStream(condLexer));\nRuleSetParser.CondContext condition = condParser.cond();\ndebugController.addConditionalBreakpoint(3, condition);\n```\n\n真正求值放在`MyRuleSetVisitor.visitMain`里，因为只有持有`visit()`方法的`visitor`自己才能对这段语法树求值：\n\n```java\n@Override\npublic VisitorResult visitMain(RuleSetParser.MainContext ctx) {\n    if (debugController != null) {\n        int line = ctx.start.getLine();\n        boolean breakpointHit = debugController.hasUnconditionalBreakpoint(line);\n        if (!breakpointHit) {\n            RuleSetParser.CondContext condition = debugController.getCondition(line);\n            breakpointHit = condition != null && visit(condition).getBool();\n        }\n        debugController.checkpoint(line, breakpointHit, callStack.size(), new ArrayList\u003C>(callStack), currentScope);\n    }\n    return visitChildren(ctx);\n}\n```\n> 关于求值的时机：条件表达式是在这一行语句**即将执行、但还没有执行**的时候求值的，这正是我们希望的语义——断点条件看到的应该是\"执行这一行之前\"的变量状态，和断点本身暂停的时机保持一致。\n>\n> 关于调用深度和调用栈：`callStack.size()`就是当前的调用深度，直接复用了上一篇文章为了防止死递归而维护的那个`Deque\u003CString>`；把它拷贝一份（`new ArrayList\u003C>(callStack)`）传给`DebugController`，是因为`callStack`本身在解释器线程里还会持续变化，拷贝一份快照才能保证控制台线程展示的调用栈是暂停那一刻的真实状态，不会因为后续的修改而错乱。\n\n## 三、测试代码与执行结果\n\n### 1. Step Into \u002F Out \u002F Over\n\n```java\nprivate static void stepDemo() throws InterruptedException {\n    String expression =\n            \"function square(x) { \\n\" +           \u002F\u002F 第1行\n                    \"    number result = x * x \\n\" +  \u002F\u002F 第2行\n                    \"    return result \\n\" +           \u002F\u002F 第3行\n                    \"} \\n\" +                            \u002F\u002F 第4行\n                    \"number a = square(2) \\n\" +          \u002F\u002F 第5行，断点\n                    \"print(a) \\n\" +                       \u002F\u002F 第6行\n                    \"number b = square(3) \\n\" +           \u002F\u002F 第7行，断点\n                    \"print(b)\";                             \u002F\u002F 第8行\n    ...\n    DebugController debugController = new DebugController();\n    debugController.addBreakpoint(5);\n    debugController.addBreakpoint(7);\n    runWithScriptedCommands(prog, debugController, new String[]{\"into\", \"out\", \"c\", \"over\", \"c\"});\n}\n```\n`square(2)`这次调用用`into`+`out`：先扎进函数体内部看一眼，再直接跑到函数返回；`square(3)`这次调用改用`over`：把整个调用当成一步跳过去。执行结果如下：\n\n```\n[调试] 暂停在第5行，调用深度：0，调用栈：[]，可见变量：{}\n[调试] 下发指令：into\n[调试] 暂停在第2行，调用深度：1，调用栈：[square]，可见变量：{x=2.0}\n[调试] 下发指令：out\n[调试] 暂停在第6行，调用深度：0，调用栈：[]，可见变量：{a=4.0}\n[调试] 下发指令：c\n4.0\n[调试] 暂停在第7行，调用深度：0，调用栈：[]，可见变量：{a=4.0}\n[调试] 下发指令：over\n[调试] 暂停在第8行，调用深度：0，调用栈：[]，可见变量：{a=4.0, b=9.0}\n[调试] 下发指令：c\n9.0\n[调试] 程序执行结束\n```\n第一次在第5行暂停后下发`into`，果然停在了`square`函数体内部的第2行，调用深度变成1，调用栈里出现了`square`，能看到参数`x=2.0`；接着下发`out`，直接跳过函数体剩下的部分（第3行的`return`根本没有单独停下来），一路停在了调用返回之后的第6行，此时`a`已经被正确赋值为`4.0`；第二次在第7行暂停后改用`over`，`square(3)`函数体内部完全没有出现暂停，直接停在了调用结束之后的第8行，`b`已经是`9.0`。三种单步方式的区别清清楚楚。\n\n### 2. 条件断点\n\n```java\nprivate static void conditionalBreakpointDemo() throws InterruptedException {\n    String expression =\n            \"number i = 0 \\n\" +\n                    \"while (i \u003C 6) { \\n\" +\n                    \"    print(i) \\n\" +     \u002F\u002F 第3行，条件断点：i == 3\n                    \"    i = i + 1 \\n\" +\n                    \"}\";\n    ...\n    RuleSetLexer condLexer = new RuleSetLexer(CharStreams.fromString(\"i == 3\"));\n    RuleSetParser condParser = new RuleSetParser(new CommonTokenStream(condLexer));\n    RuleSetParser.CondContext condition = condParser.cond();\n\n    DebugController debugController = new DebugController();\n    debugController.addConditionalBreakpoint(3, condition);\n    runWithScriptedCommands(prog, debugController, new String[]{\"c\"});\n}\n```\n\n执行结果如下：\n\n```\n0.0\n1.0\n2.0\n[调试] 暂停在第3行，调用深度：0，调用栈：[]，可见变量：{i=3.0}\n[调试] 下发指令：c\n3.0\n4.0\n5.0\n[调试] 程序执行结束\n```\n循环打印`i`为0、1、2的时候条件断点`i == 3`都不成立，直接跑过去；直到`i`变成3才真正暂停下来，下发`c`之后继续打印3、4、5直至循环结束，和条件断点的语义完全一致。\n\n## 四、遗留的问题\n\n到这里，一个具备断点（含条件断点）、Step Into\u002FOver\u002FOut、调用栈、变量查看的调试器雏形已经搭起来了，核心机制其实非常轻量——一个作用于两个线程之间的\"握手\"队列，加上在语句粒度上打卡。它虽然简陋，但对理解主流IDE（比如jdb、各种脚本语言的调试适配器）背后的基本原理已经足够了：无论多么复杂的调试器，底层往往都是\"可暂停的解释执行 + 深度\u002F条件判断\"的某种变体。\n\n我们的小型DSL系列也告一段落，接下来会转向语言本身的补完，比如数组\u002F列表类型的支持，敬请期待。\n","\u003Cp>上一篇文章实现了调试器最基础的能力：断点和单步（严格来说是\u003Cstrong>Step Into\u003C\u002Fstrong>——不管下一条语句在哪一层调用深度，都会在那里暂停）。这篇文章我们把单步完善成三种更贴近真实调试器的方式——\u003Cstrong>Step Into \u002F Step Over \u002F Step Out\u003C\u002Fstrong>，并且加上\u003Cstrong>条件断点\u003C\u002Fstrong>和\u003Cstrong>调用栈展示\u003C\u002Fstrong>。同样，本篇不涉及.g4语法改动，都是宿主程序层面的增强。\u003C\u002Fp>\n\u003Ch2>一、思路设计\u003C\u002Fh2>\n\u003Col>\n \u003Cli>\n  \u003Cp>Step Into \u002F Over \u002F Out的区别\u003C\u002Fp>\n  \u003Cul>\n   \u003Cli>\u003Cstrong>Step Into\u003C\u002Fstrong>（进入）：不管下一条语句处于多深的调用层级，走到哪就停在哪。如果下一条语句正好是一次函数调用，效果就是\"扎进函数体内部\"。\u003C\u002Fli>\n   \u003Cli>\u003Cstrong>Step Over\u003C\u002Fstrong>（跳过）：把接下来遇到的函数调用当成一个整体执行完，不进入其内部，只在\u003Cstrong>调用深度回到不超过当前深度\u003C\u002Fstrong>时才暂停。\u003C\u002Fli>\n   \u003Cli>\u003Cstrong>Step Out\u003C\u002Fstrong>（跳出）：不管函数体里还剩多少语句，一路跑到当前函数返回、\u003Cstrong>调用深度小于当前深度\u003C\u002Fstrong>（也就是回到了调用者所在的层级）才暂停。\u003C\u002Fli>\n  \u003C\u002Ful>\n  \u003Cp>可以看到，三者的区别本质上是\"暂停条件里对调用深度的要求不同\"，这也是上一篇文章特意先实现调用栈（\u003Ccode>callStack\u003C\u002Fcode>）的原因——没有调用深度这个概念，Step Over\u002FOut根本无从谈起。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>条件断点\u003C\u002Fp>\n  \u003Cp>有时候我们只关心\"变量\u003Ccode>i\u003C\u002Fcode>等于3的时候到底发生了什么\"，而不想在循环的每一次都手动放行。条件断点就是在命中断点所在的行时，先按语言本身的语法把一个条件表达式求值一遍，只有为真时才真正暂停。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>二、语法实现\u003C\u002Fh2>\n\u003Ch3>1. DebugController：从\"是否单步\"升级成\"单步模式+目标深度\"\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F**\n * 单步模式：NONE表示不处于单步（只按断点暂停）；\n * INTO表示下一条语句无论深度多少都暂停；\n * OVER表示只在深度回到不超过targetDepth时才暂停；\n * OUT表示只在深度小于targetDepth时才暂停\n *\u002F\nprivate enum StepMode {NONE, INTO, OVER, OUT}\n\nprivate volatile StepMode stepMode = StepMode.NONE;\nprivate volatile int targetDepth = 0;\n\n\u002F**\n * 由解释器线程调用：每执行一条语句之前检查是否需要暂停。\n * breakpointHit由调用方（MyRuleSetVisitor）提前算好传进来，因为条件断点的条件表达式\n * 需要用visitor自己去visit求值，DebugController本身并不持有visitor，没办法自己算。\n *\u002F\npublic void checkpoint(int line, boolean breakpointHit, int depth, List&lt;String&gt; callStack, Scope scope) {\n    boolean shouldPause = breakpointHit || matchesStepMode(depth);\n    if (!shouldPause) {\n        return;\n    }\n    stepMode = StepMode.NONE;\n    try {\n        pauseSignal.put(new PauseInfo(line, depth, callStack, scope));\n        resumeSignal.take();\n    } catch (InterruptedException e) {\n        Thread.currentThread().interrupt();\n    }\n}\n\nprivate boolean matchesStepMode(int depth) {\n    switch (stepMode) {\n        case INTO:\n            return true;\n        case OVER:\n            return depth &lt;= targetDepth;\n        case OUT:\n            return depth &lt; targetDepth;\n        default:\n            return false;\n    }\n}\n\npublic void stepInto() {\n    stepMode = StepMode.INTO;\n    resumeSignal.offer(true);\n}\n\n\u002F**\n * @param currentDepth 发出指令时所在的调用深度，通常就是最近一次PauseInfo里的depth\n *\u002F\npublic void stepOver(int currentDepth) {\n    stepMode = StepMode.OVER;\n    targetDepth = currentDepth;\n    resumeSignal.offer(true);\n}\n\npublic void stepOut(int currentDepth) {\n    stepMode = StepMode.OUT;\n    targetDepth = currentDepth;\n    resumeSignal.offer(true);\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>targetDepth\u003C\u002Fcode>记录的是\u003Cstrong>发出指令那一刻\u003C\u002Fstrong>所在的调用深度。以\u003Ccode>stepOver\u003C\u002Fcode>为例：命令发出时深度是\u003Ccode>D\u003C\u002Fcode>，只要接下来遇到的语句深度大于\u003Ccode>D\u003C\u002Fcode>（说明正处在被跳过的函数调用内部），就不理会；一旦某条语句的深度又回到了\u003Ccode>≤ D\u003C\u002Fcode>，说明已经跳出了那次调用（或者调用压根就没有发生更深的调用），此时暂停。\u003Ccode>stepOut\u003C\u002Fcode>同理，只是把\"回到不超过D\"换成了\"严格小于D\"——因为如果当前就在某次调用内部，\"跳出\"至少要回到比当前更浅一层才算数。\u003C\u002Fp>\n\u003Ch3>2. 条件断点\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-java\">private final Map&lt;Integer, RuleSetParser.CondContext&gt; conditionalBreakpoints = new ConcurrentHashMap&lt;&gt;();\n\n\u002F**\n * 增加一个条件断点：只有condition在命中该行时求值为true才真正暂停。\n * condition是提前用RuleSetParser.cond()规则单独解析出来的语法树节点\n *\u002F\npublic void addConditionalBreakpoint(int line, RuleSetParser.CondContext condition) {\n    conditionalBreakpoints.put(line, condition);\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>这里有个值得一提的细节：\u003Ccode>condition\u003C\u002Fcode>是提前单独解析好的一段\u003Ccode>cond\u003C\u002Fcode>语法树，而不是每次命中都重新解析文本。antlr4生成的\u003Ccode>RuleSetParser\u003C\u002Fcode>不仅有解析整个程序入口的\u003Ccode>prog()\u003C\u002Fcode>方法，语法文件里的每一条规则都会生成一个同名的公开方法（比如\u003Ccode>cond()\u003C\u002Fcode>、\u003Ccode>calcu()\u003C\u002Fcode>），可以直接拿来复用，单独解析一小段符合该规则的文本，不需要每次都套上一个完整的程序。设置条件断点时就是这么做的：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">RuleSetLexer condLexer = new RuleSetLexer(CharStreams.fromString(\"i == 3\"));\nRuleSetParser condParser = new RuleSetParser(new CommonTokenStream(condLexer));\nRuleSetParser.CondContext condition = condParser.cond();\ndebugController.addConditionalBreakpoint(3, condition);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>真正求值放在\u003Ccode>MyRuleSetVisitor.visitMain\u003C\u002Fcode>里，因为只有持有\u003Ccode>visit()\u003C\u002Fcode>方法的\u003Ccode>visitor\u003C\u002Fcode>自己才能对这段语法树求值：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">@Override\npublic VisitorResult visitMain(RuleSetParser.MainContext ctx) {\n    if (debugController != null) {\n        int line = ctx.start.getLine();\n        boolean breakpointHit = debugController.hasUnconditionalBreakpoint(line);\n        if (!breakpointHit) {\n            RuleSetParser.CondContext condition = debugController.getCondition(line);\n            breakpointHit = condition != null &amp;&amp; visit(condition).getBool();\n        }\n        debugController.checkpoint(line, breakpointHit, callStack.size(), new ArrayList&lt;&gt;(callStack), currentScope);\n    }\n    return visitChildren(ctx);\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cblockquote>\n \u003Cp>关于求值的时机：条件表达式是在这一行语句\u003Cstrong>即将执行、但还没有执行\u003C\u002Fstrong>的时候求值的，这正是我们希望的语义——断点条件看到的应该是\"执行这一行之前\"的变量状态，和断点本身暂停的时机保持一致。\u003C\u002Fp>\n \u003Cp>关于调用深度和调用栈：\u003Ccode>callStack.size()\u003C\u002Fcode>就是当前的调用深度，直接复用了上一篇文章为了防止死递归而维护的那个\u003Ccode>Deque&lt;String&gt;\u003C\u002Fcode>；把它拷贝一份（\u003Ccode>new ArrayList&lt;&gt;(callStack)\u003C\u002Fcode>）传给\u003Ccode>DebugController\u003C\u002Fcode>，是因为\u003Ccode>callStack\u003C\u002Fcode>本身在解释器线程里还会持续变化，拷贝一份快照才能保证控制台线程展示的调用栈是暂停那一刻的真实状态，不会因为后续的修改而错乱。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2>三、测试代码与执行结果\u003C\u002Fh2>\n\u003Ch3>1. Step Into \u002F Out \u002F Over\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-java\">private static void stepDemo() throws InterruptedException {\n    String expression =\n            \"function square(x) { \\n\" +           \u002F\u002F 第1行\n                    \"    number result = x * x \\n\" +  \u002F\u002F 第2行\n                    \"    return result \\n\" +           \u002F\u002F 第3行\n                    \"} \\n\" +                            \u002F\u002F 第4行\n                    \"number a = square(2) \\n\" +          \u002F\u002F 第5行，断点\n                    \"print(a) \\n\" +                       \u002F\u002F 第6行\n                    \"number b = square(3) \\n\" +           \u002F\u002F 第7行，断点\n                    \"print(b)\";                             \u002F\u002F 第8行\n    ...\n    DebugController debugController = new DebugController();\n    debugController.addBreakpoint(5);\n    debugController.addBreakpoint(7);\n    runWithScriptedCommands(prog, debugController, new String[]{\"into\", \"out\", \"c\", \"over\", \"c\"});\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>square(2)\u003C\u002Fcode>这次调用用\u003Ccode>into\u003C\u002Fcode>+\u003Ccode>out\u003C\u002Fcode>：先扎进函数体内部看一眼，再直接跑到函数返回；\u003Ccode>square(3)\u003C\u002Fcode>这次调用改用\u003Ccode>over\u003C\u002Fcode>：把整个调用当成一步跳过去。执行结果如下：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>[调试] 暂停在第5行，调用深度：0，调用栈：[]，可见变量：{}\n[调试] 下发指令：into\n[调试] 暂停在第2行，调用深度：1，调用栈：[square]，可见变量：{x=2.0}\n[调试] 下发指令：out\n[调试] 暂停在第6行，调用深度：0，调用栈：[]，可见变量：{a=4.0}\n[调试] 下发指令：c\n4.0\n[调试] 暂停在第7行，调用深度：0，调用栈：[]，可见变量：{a=4.0}\n[调试] 下发指令：over\n[调试] 暂停在第8行，调用深度：0，调用栈：[]，可见变量：{a=4.0, b=9.0}\n[调试] 下发指令：c\n9.0\n[调试] 程序执行结束\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>第一次在第5行暂停后下发\u003Ccode>into\u003C\u002Fcode>，果然停在了\u003Ccode>square\u003C\u002Fcode>函数体内部的第2行，调用深度变成1，调用栈里出现了\u003Ccode>square\u003C\u002Fcode>，能看到参数\u003Ccode>x=2.0\u003C\u002Fcode>；接着下发\u003Ccode>out\u003C\u002Fcode>，直接跳过函数体剩下的部分（第3行的\u003Ccode>return\u003C\u002Fcode>根本没有单独停下来），一路停在了调用返回之后的第6行，此时\u003Ccode>a\u003C\u002Fcode>已经被正确赋值为\u003Ccode>4.0\u003C\u002Fcode>；第二次在第7行暂停后改用\u003Ccode>over\u003C\u002Fcode>，\u003Ccode>square(3)\u003C\u002Fcode>函数体内部完全没有出现暂停，直接停在了调用结束之后的第8行，\u003Ccode>b\u003C\u002Fcode>已经是\u003Ccode>9.0\u003C\u002Fcode>。三种单步方式的区别清清楚楚。\u003C\u002Fp>\n\u003Ch3>2. 条件断点\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-java\">private static void conditionalBreakpointDemo() throws InterruptedException {\n    String expression =\n            \"number i = 0 \\n\" +\n                    \"while (i &lt; 6) { \\n\" +\n                    \"    print(i) \\n\" +     \u002F\u002F 第3行，条件断点：i == 3\n                    \"    i = i + 1 \\n\" +\n                    \"}\";\n    ...\n    RuleSetLexer condLexer = new RuleSetLexer(CharStreams.fromString(\"i == 3\"));\n    RuleSetParser condParser = new RuleSetParser(new CommonTokenStream(condLexer));\n    RuleSetParser.CondContext condition = condParser.cond();\n\n    DebugController debugController = new DebugController();\n    debugController.addConditionalBreakpoint(3, condition);\n    runWithScriptedCommands(prog, debugController, new String[]{\"c\"});\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>执行结果如下：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>0.0\n1.0\n2.0\n[调试] 暂停在第3行，调用深度：0，调用栈：[]，可见变量：{i=3.0}\n[调试] 下发指令：c\n3.0\n4.0\n5.0\n[调试] 程序执行结束\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>循环打印\u003Ccode>i\u003C\u002Fcode>为0、1、2的时候条件断点\u003Ccode>i == 3\u003C\u002Fcode>都不成立，直接跑过去；直到\u003Ccode>i\u003C\u002Fcode>变成3才真正暂停下来，下发\u003Ccode>c\u003C\u002Fcode>之后继续打印3、4、5直至循环结束，和条件断点的语义完全一致。\u003C\u002Fp>\n\u003Ch2>四、遗留的问题\u003C\u002Fh2>\n\u003Cp>到这里，一个具备断点（含条件断点）、Step Into\u002FOver\u002FOut、调用栈、变量查看的调试器雏形已经搭起来了，核心机制其实非常轻量——一个作用于两个线程之间的\"握手\"队列，加上在语句粒度上打卡。它虽然简陋，但对理解主流IDE（比如jdb、各种脚本语言的调试适配器）背后的基本原理已经足够了：无论多么复杂的调试器，底层往往都是\"可暂停的解释执行 + 深度\u002F条件判断\"的某种变体。\u003C\u002Fp>\n\u003Cp>我们的小型DSL系列也告一段落，接下来会转向语言本身的补完，比如数组\u002F列表类型的支持，敬请期待。\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},"2101623894064939009","antlr4-08-debugger-breakpoints","Antlr4系列⑧：调试器基础之断点与单步执行","双线程+阻塞队列实现暂停\u002F恢复、断点、单步、变量查看",5558,14,"2026-09-20T18:46:06",{"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},"2101608263357743105","antlr4-05-loops","Antlr4系列⑤：支持循环语句","while循环、break\u002Fcontinue（用异常做控制流跳转）",4801,13,"2026-09-20T17:44:00",{"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":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},{"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},"2101635146136862722","antlr4-10-arrays","Antlr4系列⑩：支持数组","array类型、字面量、下标读写、len",5036,"2026-09-20T19:30:48",{"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>上一篇文章实现了调试器最基础的能力：断点和单步（严格来说是\u003Cstrong>Step Into\u003C\u002Fstrong>——不管下一条语句在哪一层调用深度，都会在那里暂停）。这篇文章我们把单步完善成三种更贴近真实调试器的方式——\u003Cstrong>Step Into \u002F Step Over \u002F Step Out\u003C\u002Fstrong>，并且加上\u003Cstrong>条件断点\u003C\u002Fstrong>和\u003Cstrong>调用栈展示\u003C\u002Fstrong>。同样，本篇不涉及.g4语法改动，都是宿主程序层面的增强。\u003C\u002Fp>\n\u003Ch2 id=\"toc-1\">一、思路设计\u003C\u002Fh2>\n\u003Col>\n \u003Cli>\n  \u003Cp>Step Into \u002F Over \u002F Out的区别\u003C\u002Fp>\n  \u003Cul>\n   \u003Cli>\u003Cstrong>Step Into\u003C\u002Fstrong>（进入）：不管下一条语句处于多深的调用层级，走到哪就停在哪。如果下一条语句正好是一次函数调用，效果就是\"扎进函数体内部\"。\u003C\u002Fli>\n   \u003Cli>\u003Cstrong>Step Over\u003C\u002Fstrong>（跳过）：把接下来遇到的函数调用当成一个整体执行完，不进入其内部，只在\u003Cstrong>调用深度回到不超过当前深度\u003C\u002Fstrong>时才暂停。\u003C\u002Fli>\n   \u003Cli>\u003Cstrong>Step Out\u003C\u002Fstrong>（跳出）：不管函数体里还剩多少语句，一路跑到当前函数返回、\u003Cstrong>调用深度小于当前深度\u003C\u002Fstrong>（也就是回到了调用者所在的层级）才暂停。\u003C\u002Fli>\n  \u003C\u002Ful>\n  \u003Cp>可以看到，三者的区别本质上是\"暂停条件里对调用深度的要求不同\"，这也是上一篇文章特意先实现调用栈（\u003Ccode>callStack\u003C\u002Fcode>）的原因——没有调用深度这个概念，Step Over\u002FOut根本无从谈起。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\n  \u003Cp>条件断点\u003C\u002Fp>\n  \u003Cp>有时候我们只关心\"变量\u003Ccode>i\u003C\u002Fcode>等于3的时候到底发生了什么\"，而不想在循环的每一次都手动放行。条件断点就是在命中断点所在的行时，先按语言本身的语法把一个条件表达式求值一遍，只有为真时才真正暂停。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"toc-2\">二、语法实现\u003C\u002Fh2>\n\u003Ch3 id=\"toc-3\">1. DebugController：从\"是否单步\"升级成\"单步模式+目标深度\"\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\"> * 单步模式：NONE表示不处于单步（只按断点暂停）；\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> * INTO表示下一条语句无论深度多少都暂停；\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> * OVER表示只在深度回到不超过targetDepth时才暂停；\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> * OUT表示只在深度小于targetDepth时才暂停\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\"> enum\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> StepMode\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">NONE\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">INTO\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">OVER\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">OUT\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\"> volatile\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> StepMode stepMode \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> StepMode.NONE;\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\"> volatile\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> int\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> targetDepth \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\">;\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\"> * breakpointHit由调用方（MyRuleSetVisitor）提前算好传进来，因为条件断点的条件表达式\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> * 需要用visitor自己去visit求值，DebugController本身并不持有visitor，没办法自己算。\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\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> checkpoint\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\"> line, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">boolean\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> breakpointHit, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">int\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> depth, 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:#24292E;--shiki-dark:#E1E4E8\"> callStack, Scope scope) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    boolean\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> shouldPause \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> breakpointHit \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">||\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> matchesStepMode\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(depth);\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\"> (\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">!\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">shouldPause) {\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\">;\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\">    stepMode \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> StepMode.NONE;\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:#24292E;--shiki-dark:#E1E4E8\">        pauseSignal.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">put\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> PauseInfo\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(line, depth, callStack, scope));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        resumeSignal.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">take\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\"> (InterruptedException \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:#24292E;--shiki-dark:#E1E4E8\">        Thread.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">currentThread\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">().\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">interrupt\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:#D73A49;--shiki-dark:#F97583\"> boolean\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> matchesStepMode\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\"> depth) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    switch\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (stepMode) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        case\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> INTO\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">:\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\"> true\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        case\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> OVER\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">:\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\"> depth \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> targetDepth;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        case\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> OUT\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">:\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\"> depth \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> targetDepth;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        default:\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\"> false\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\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> stepInto\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    stepMode \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> StepMode.INTO;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    resumeSignal.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">offer\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">true\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\"> * \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">@param\u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\"> currentDepth\u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> 发出指令时所在的调用深度，通常就是最近一次PauseInfo里的depth\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\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> stepOver\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\"> currentDepth) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    stepMode \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> StepMode.OVER;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    targetDepth \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> currentDepth;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    resumeSignal.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">offer\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">true\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:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> stepOut\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\"> currentDepth) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    stepMode \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> StepMode.OUT;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    targetDepth \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> currentDepth;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    resumeSignal.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">offer\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">true\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>targetDepth\u003C\u002Fcode>记录的是\u003Cstrong>发出指令那一刻\u003C\u002Fstrong>所在的调用深度。以\u003Ccode>stepOver\u003C\u002Fcode>为例：命令发出时深度是\u003Ccode>D\u003C\u002Fcode>，只要接下来遇到的语句深度大于\u003Ccode>D\u003C\u002Fcode>（说明正处在被跳过的函数调用内部），就不理会；一旦某条语句的深度又回到了\u003Ccode>≤ D\u003C\u002Fcode>，说明已经跳出了那次调用（或者调用压根就没有发生更深的调用），此时暂停。\u003Ccode>stepOut\u003C\u002Fcode>同理，只是把\"回到不超过D\"换成了\"严格小于D\"——因为如果当前就在某次调用内部，\"跳出\"至少要回到比当前更浅一层才算数。\u003C\u002Fp>\n\u003Ch3 id=\"toc-4\">2. 条件断点\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:#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\">Integer\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\">CondContext\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> conditionalBreakpoints \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\"> ConcurrentHashMap&#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\"> * 增加一个条件断点：只有condition在命中该行时求值为true才真正暂停。\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\"> * condition是提前用RuleSetParser.cond()规则单独解析出来的语法树节点\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\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> addConditionalBreakpoint\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\"> line, RuleSetParser.CondContext condition) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    conditionalBreakpoints.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">put\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(line, condition);\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>condition\u003C\u002Fcode>是提前单独解析好的一段\u003Ccode>cond\u003C\u002Fcode>语法树，而不是每次命中都重新解析文本。antlr4生成的\u003Ccode>RuleSetParser\u003C\u002Fcode>不仅有解析整个程序入口的\u003Ccode>prog()\u003C\u002Fcode>方法，语法文件里的每一条规则都会生成一个同名的公开方法（比如\u003Ccode>cond()\u003C\u002Fcode>、\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 style=\"color:#24292E;--shiki-dark:#E1E4E8\">RuleSetLexer condLexer \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\"> RuleSetLexer\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(CharStreams.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">fromString\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"i == 3\"\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 condParser \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\"> RuleSetParser\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> CommonTokenStream\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(condLexer));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">RuleSetParser.CondContext condition \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> condParser.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">cond\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">debugController.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">addConditionalBreakpoint\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">3\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, condition);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>真正求值放在\u003Ccode>MyRuleSetVisitor.visitMain\u003C\u002Fcode>里，因为只有持有\u003Ccode>visit()\u003C\u002Fcode>方法的\u003Ccode>visitor\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:#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\">visitMain\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(RuleSetParser.MainContext ctx) {\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\"> (debugController \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\">        int\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ctx.start.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getLine\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        boolean\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> breakpointHit \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> debugController.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">hasUnconditionalBreakpoint\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(line);\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\"> (\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">!\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">breakpointHit) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            RuleSetParser.CondContext condition \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> debugController.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getCondition\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(line);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            breakpointHit \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> condition \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\"> &#x26;&#x26;\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> visit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(condition).\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:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        debugController.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">checkpoint\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(line, breakpointHit, 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\">new\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ArrayList&#x3C;>(callStack), currentScope);\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:#6F42C1;--shiki-dark:#B392F0\"> visitChildren\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(ctx);\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>关于求值的时机：条件表达式是在这一行语句\u003Cstrong>即将执行、但还没有执行\u003C\u002Fstrong>的时候求值的，这正是我们希望的语义——断点条件看到的应该是\"执行这一行之前\"的变量状态，和断点本身暂停的时机保持一致。\u003C\u002Fp>\n \u003Cp>关于调用深度和调用栈：\u003Ccode>callStack.size()\u003C\u002Fcode>就是当前的调用深度，直接复用了上一篇文章为了防止死递归而维护的那个\u003Ccode>Deque&lt;String&gt;\u003C\u002Fcode>；把它拷贝一份（\u003Ccode>new ArrayList&lt;&gt;(callStack)\u003C\u002Fcode>）传给\u003Ccode>DebugController\u003C\u002Fcode>，是因为\u003Ccode>callStack\u003C\u002Fcode>本身在解释器线程里还会持续变化，拷贝一份快照才能保证控制台线程展示的调用栈是暂停那一刻的真实状态，不会因为后续的修改而错乱。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2 id=\"toc-5\">三、测试代码与执行结果\u003C\u002Fh2>\n\u003Ch3 id=\"toc-6\">1. Step Into \u002F Out \u002F Over\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:#D73A49;--shiki-dark:#F97583\">private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> stepDemo\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() throws InterruptedException {\u003C\u002Fspan>\u003C\u002Fspan>\n\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\">            \"function square(x) { \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>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">           \u002F\u002F 第1行\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"    number result = x * x \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>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">  \u002F\u002F 第2行\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"    return result \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>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">           \u002F\u002F 第3行\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>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">                            \u002F\u002F 第4行\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"number a = square(2) \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>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">          \u002F\u002F 第5行，断点\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"print(a) \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>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">                       \u002F\u002F 第6行\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"number b = square(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>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">           \u002F\u002F 第7行，断点\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"print(b)\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;                             \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 第8行\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\">    DebugController debugController \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\"> DebugController\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    debugController.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">addBreakpoint\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">5\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    debugController.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">addBreakpoint\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">7\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">    runWithScriptedCommands\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(prog, debugController, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">new\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">[]{\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"into\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"out\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"c\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"over\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"c\"\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>square(2)\u003C\u002Fcode>这次调用用\u003Ccode>into\u003C\u002Fcode>+\u003Ccode>out\u003C\u002Fcode>：先扎进函数体内部看一眼，再直接跑到函数返回；\u003Ccode>square(3)\u003C\u002Fcode>这次调用改用\u003Ccode>over\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>[调试] 暂停在第5行，调用深度：0，调用栈：[]，可见变量：{}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[调试] 下发指令：into\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[调试] 暂停在第2行，调用深度：1，调用栈：[square]，可见变量：{x=2.0}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[调试] 下发指令：out\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[调试] 暂停在第6行，调用深度：0，调用栈：[]，可见变量：{a=4.0}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[调试] 下发指令：c\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>4.0\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[调试] 暂停在第7行，调用深度：0，调用栈：[]，可见变量：{a=4.0}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[调试] 下发指令：over\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[调试] 暂停在第8行，调用深度：0，调用栈：[]，可见变量：{a=4.0, b=9.0}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[调试] 下发指令：c\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>9.0\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>第一次在第5行暂停后下发\u003Ccode>into\u003C\u002Fcode>，果然停在了\u003Ccode>square\u003C\u002Fcode>函数体内部的第2行，调用深度变成1，调用栈里出现了\u003Ccode>square\u003C\u002Fcode>，能看到参数\u003Ccode>x=2.0\u003C\u002Fcode>；接着下发\u003Ccode>out\u003C\u002Fcode>，直接跳过函数体剩下的部分（第3行的\u003Ccode>return\u003C\u002Fcode>根本没有单独停下来），一路停在了调用返回之后的第6行，此时\u003Ccode>a\u003C\u002Fcode>已经被正确赋值为\u003Ccode>4.0\u003C\u002Fcode>；第二次在第7行暂停后改用\u003Ccode>over\u003C\u002Fcode>，\u003Ccode>square(3)\u003C\u002Fcode>函数体内部完全没有出现暂停，直接停在了调用结束之后的第8行，\u003Ccode>b\u003C\u002Fcode>已经是\u003Ccode>9.0\u003C\u002Fcode>。三种单步方式的区别清清楚楚。\u003C\u002Fp>\n\u003Ch3 id=\"toc-7\">2. 条件断点\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:#D73A49;--shiki-dark:#F97583\">private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> conditionalBreakpointDemo\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() throws InterruptedException {\u003C\u002Fspan>\u003C\u002Fspan>\n\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\">            \"number i = 0 \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\">                    \"while (i &#x3C; 6) { \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(i) \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>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">     \u002F\u002F 第3行，条件断点：i == 3\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"    i = i + 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:#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\">    RuleSetLexer condLexer \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\"> RuleSetLexer\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(CharStreams.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">fromString\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"i == 3\"\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 condParser \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\"> RuleSetParser\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">new\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> CommonTokenStream\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(condLexer));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    RuleSetParser.CondContext condition \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> condParser.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">cond\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:#24292E;--shiki-dark:#E1E4E8\">    DebugController debugController \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\"> DebugController\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">();\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    debugController.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">addConditionalBreakpoint\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">3\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, condition);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">    runWithScriptedCommands\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(prog, debugController, \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">new\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">[]{\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"c\"\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>执行结果如下：\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>0.0\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>1.0\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>2.0\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[调试] 暂停在第3行，调用深度：0，调用栈：[]，可见变量：{i=3.0}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[调试] 下发指令：c\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>3.0\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>4.0\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>5.0\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>i\u003C\u002Fcode>为0、1、2的时候条件断点\u003Ccode>i == 3\u003C\u002Fcode>都不成立，直接跑过去；直到\u003Ccode>i\u003C\u002Fcode>变成3才真正暂停下来，下发\u003Ccode>c\u003C\u002Fcode>之后继续打印3、4、5直至循环结束，和条件断点的语义完全一致。\u003C\u002Fp>\n\u003Ch2 id=\"toc-8\">四、遗留的问题\u003C\u002Fh2>\n\u003Cp>到这里，一个具备断点（含条件断点）、Step Into\u002FOver\u002FOut、调用栈、变量查看的调试器雏形已经搭起来了，核心机制其实非常轻量——一个作用于两个线程之间的\"握手\"队列，加上在语句粒度上打卡。它虽然简陋，但对理解主流IDE（比如jdb、各种脚本语言的调试适配器）背后的基本原理已经足够了：无论多么复杂的调试器，底层往往都是\"可暂停的解释执行 + 深度\u002F条件判断\"的某种变体。\u003C\u002Fp>\n\u003Cp>我们的小型DSL系列也告一段落，接下来会转向语言本身的补完，比如数组\u002F列表类型的支持，敬请期待。\u003C\u002Fp>",[83,87,90,94,97,100,103,105],{"id":84,"text":85,"level":86},"toc-1","一、思路设计",2,{"id":88,"text":89,"level":86},"toc-2","二、语法实现",{"id":91,"text":92,"level":93},"toc-3","1. DebugController：从\"是否单步\"升级成\"单步模式+目标深度\"",3,{"id":95,"text":96,"level":93},"toc-4","2. 条件断点",{"id":98,"text":99,"level":86},"toc-5","三、测试代码与执行结果",{"id":101,"text":102,"level":93},"toc-6","1. Step Into \u002F Out \u002F Over",{"id":104,"text":96,"level":93},"toc-7",{"id":106,"text":107,"level":86},"toc-8","四、遗留的问题"]