[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"site-settings":3,"$f11ruzlrxm447d":14,"$f2talbhhfslzeh":31,"$f2ufknnm7lkl00":56,"$f2hf8zv88r59zg":67,"render-antlr4-08-debugger-breakpoints":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},"2101623894064939009","antlr4-08-debugger-breakpoints","Antlr4系列⑧：调试器基础之断点与单步执行","双线程+阻塞队列实现暂停\u002F恢复、断点、单步、变量查看",null,"1946000000000000001",1,5558,14,"2026-09-20T18:46:06","上一篇文章实现了自定义函数和调用栈，我们的小型DSL已经是一门功能齐全的编程语言了。从本篇开始，我们尝试实现一个更有意思的能力——**调试器**：设置断点、单步执行、暂停时查看变量。这篇文章先实现最基础的部分：断点和单步。注意本篇不涉及.g4语法的任何改动，纯粹是宿主程序（Java）层面新增的能力。\n\n## 一、思路设计\n\n我们的解释器是基于`Visitor`模式的树遍历，本质上是一次\"一口气从头跑到尾\"的递归函数调用，天然没有\"暂停点\"。要实现断点和单步，核心问题是：**怎么让解释器在执行到某一条语句时暂停下来，等外部指令决定它是继续跑还是再往前挪一步**？\n\n这里采用的方案是**把解释器的执行放到一个独立线程里跑**，在每条语句执行之前\"打卡\"一次：\n- 如果打卡的这一行命中了断点，或者当前处于单步模式，就把这个线程阻塞住。\n- 阻塞期间，另一个\"控制台线程\"（在真实场景里就是接收用户输入的线程）可以拿到暂停时的行号和当前作用域，展示给用户看。\n- 用户输入`n`（单步）或`c`（继续）之后，控制台线程通知被阻塞的解释器线程继续往下走。\n\n两个线程之间的通信，本质上是两次\"握手\"：解释器线程要让控制台线程知道\"我暂停了\"，控制台线程要让解释器线程知道\"可以继续了\"。这里用两个容量为1的阻塞队列（`ArrayBlockingQueue`）分别表示这两个方向的信号，谁先到谁就等着，不会出现请求丢失的问题，比用`wait\u002Fnotify`手写锁更不容易出错。\n\n## 二、语法实现\n\n### 1. DebugController：两个线程之间的桥梁\n\n```java\npublic class DebugController {\n\n    private final Set\u003CInteger> breakpoints = ConcurrentHashMap.newKeySet();\n\n    \u002F**\n     * 是否处于单步模式，只对触发暂停后的下一条语句生效，消费一次就会被重置\n     *\u002F\n    private volatile boolean stepMode = false;\n\n    private final BlockingQueue\u003CPauseInfo> pauseSignal = new ArrayBlockingQueue\u003C>(1);\n    private final BlockingQueue\u003CBoolean> resumeSignal = new ArrayBlockingQueue\u003C>(1);\n\n    public void addBreakpoint(int line) {\n        breakpoints.add(line);\n    }\n\n    \u002F**\n     * 由解释器线程调用：每执行一条语句之前检查是否需要暂停\n     *\u002F\n    public void checkpoint(int line, Scope scope) {\n        if (!stepMode && !breakpoints.contains(line)) {\n            return;\n        }\n        stepMode = false;\n        try {\n            pauseSignal.put(new PauseInfo(line, scope));\n            \u002F\u002F 阻塞在这里，直到控制台线程调用resume或stepNext\n            resumeSignal.take();\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n        }\n    }\n\n    \u002F**\n     * 由解释器线程调用：程序正常结束或者抛出异常终止时调用，发送一个哨兵值告知控制台线程\"结束了\"\n     *\u002F\n    public void finish() {\n        try {\n            pauseSignal.put(PauseInfo.FINISHED);\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n        }\n    }\n\n    \u002F**\n     * 由控制台线程调用：阻塞直到解释器线程暂停下来（命中断点\u002F单步\u002F程序结束），返回当时的信息\n     *\u002F\n    public PauseInfo awaitPause() throws InterruptedException {\n        return pauseSignal.take();\n    }\n\n    \u002F**\n     * 由控制台线程调用：让解释器线程继续跑，直到遇到下一个断点或程序结束\n     *\u002F\n    public void resume() {\n        stepMode = false;\n        resumeSignal.offer(true);\n    }\n\n    \u002F**\n     * 由控制台线程调用：只放行到下一条语句就再次暂停\n     *\u002F\n    public void stepNext() {\n        stepMode = true;\n        resumeSignal.offer(true);\n    }\n\n    \u002F**\n     * 解释器每次暂停时的快照：暂停在第几行、当时的作用域是什么\n     *\u002F\n    public static class PauseInfo {\n        static final PauseInfo FINISHED = new PauseInfo(-1, null);\n        public final int line;\n        public final Scope scope;\n        PauseInfo(int line, Scope scope) {\n            this.line = line;\n            this.scope = scope;\n        }\n        public boolean isFinished() {\n            return line == -1;\n        }\n    }\n}\n```\n> 关于`stepMode`只生效一次：`n`（单步）应该只让\"紧接着的下一条语句\"暂停，而不是之后所有语句都暂停，所以`checkpoint`一旦因为`stepMode`触发了暂停，会立刻把它重置为`false`。如果用户接下来想继续单步，需要再次调用`stepNext()`。\n\n### 2. 打卡点：visitMain\n\n前面几篇文章里，`main`语法对应的正是\"一条语句\"（变量定义、赋值、打印、if、while、break、continue……），天然就是解释器执行的最小步进单位，所以打卡点就选在`visitMain`：\n\n```java\n\u002F**\n * 调试控制器，为null时表示不启用调试，按普通方式一路执行到底，不产生任何额外开销\n *\u002F\nprivate final DebugController debugController;\n\npublic MyRuleSetVisitor() {\n    this(null);\n}\n\npublic MyRuleSetVisitor(DebugController debugController) {\n    this.debugController = debugController;\n}\n\n\u002F**\n * main对应语法里的每一条语句，是解释器执行的最小步进单位。在真正执行这条语句之前先\"打卡\"一下，\n * 如果启用了调试且命中了断点或处于单步模式，这里会阻塞住，等待调试控制台发出继续\u002F单步指令。\n *\u002F\n@Override\npublic VisitorResult visitMain(RuleSetParser.MainContext ctx) {\n    if (debugController != null) {\n        debugController.checkpoint(ctx.start.getLine(), currentScope);\n    }\n    return visitChildren(ctx);\n}\n```\n`debugController`允许传`null`，这样不需要调试的场景（前几篇文章的用法）不会受到任何影响，也不会有额外开销——这是给已有功能\"平滑加装\"新能力的常见做法。\n\n### 3. 查看变量：区分\"自己的变量\"和\"看得见的变量\"\n\n暂停时要把变量展示出来，但`Scope`当前只保存\"当前这一层自己的变量\"。如果只展示这一层，很多时候会是空的（比如`while`循环体`block`刚进入时，自己还没有定义任何变量，`i`是在外层定义的）。所以在`Scope`里补充一个方法，沿着作用域链把所有能看到的变量都合并起来，内层同名变量覆盖外层的：\n\n```java\n\u002F**\n * 返回当前这一层作用域自己定义的变量（不含外层），主要给调试器展示\"当前作用域内的变量\"用\n *\u002F\npublic Map\u003CString, Object> getOwnVariables() {\n    return Collections.unmodifiableMap(variables);\n}\n\n\u002F**\n * 返回当前位置沿作用域链能看到的所有变量（含外层），内层同名变量会覆盖外层的，\n * 这才是调试时\"当前所有可见变量\"应该展示的完整视图\n *\u002F\npublic Map\u003CString, Object> getVisibleVariables() {\n    List\u003CScope> chain = new ArrayList\u003C>();\n    for (Scope scope = this; scope != null; scope = scope.parent) {\n        chain.add(scope);\n    }\n    Map\u003CString, Object> merged = new LinkedHashMap\u003C>();\n    for (int i = chain.size() - 1; i >= 0; i--) {\n        merged.putAll(chain.get(i).variables);\n    }\n    return merged;\n}\n```\n\n## 三、测试代码与执行结果\n\n因为这篇文章的重点是机制本身，没有接入真实的控制台输入，用一个`commands`数组模拟依次下发的调试指令，实际使用时把这里换成读取`Scanner`的用户输入即可：\n\n```java\nprivate static void debugDemo() throws InterruptedException {\n    String expression =\n            \"number i = 0 \\n\" +                    \u002F\u002F 第1行\n                    \"while (i \u003C 3) { \\n\" +          \u002F\u002F 第2行\n                    \"    i = i + 1 \\n\" +             \u002F\u002F 第3行，断点设在这一行\n                    \"    print(i) \\n\" +               \u002F\u002F 第4行\n                    \"} \\n\" +                          \u002F\u002F 第5行\n                    \"print(\\\"done\\\")\";                 \u002F\u002F 第6行\n\n    RuleSetLexer lexer = new RuleSetLexer(CharStreams.fromString(expression));\n    RuleSetParser parser = new RuleSetParser(new CommonTokenStream(lexer));\n    RuleSetParser.ProgContext prog = parser.prog();\n\n    DebugController debugController = new DebugController();\n    debugController.addBreakpoint(3);\n    MyRuleSetVisitor visitor = new MyRuleSetVisitor(debugController);\n\n    \u002F\u002F 解释器放到独立线程里跑，避免阻塞住控制台线程\n    Thread interpreterThread = new Thread(() -> {\n        try {\n            visitor.visit(prog);\n        } finally {\n            debugController.finish();\n        }\n    });\n    interpreterThread.start();\n\n    \u002F\u002F 模拟依次下发的调试指令：第一次命中断点直接continue，第二次改成单步，之后再continue到结束\n    String[] commands = {\"c\", \"n\", \"c\", \"c\"};\n    int commandIndex = 0;\n    while (true) {\n        DebugController.PauseInfo pause = debugController.awaitPause();\n        if (pause.isFinished()) {\n            System.out.println(\"[调试] 程序执行结束\");\n            break;\n        }\n        System.out.println(\"[调试] 暂停在第\" + pause.line + \"行，当前可见变量：\" + pause.scope.getVisibleVariables());\n        String command = commands[commandIndex++];\n        if (\"n\".equals(command)) {\n            System.out.println(\"[调试] 下发指令：n（单步）\");\n            debugController.stepNext();\n        } else {\n            System.out.println(\"[调试] 下发指令：c（继续）\");\n            debugController.resume();\n        }\n    }\n    interpreterThread.join();\n}\n```\n\n执行结果如下：\n\n```\n执行:\nnumber i = 0\nwhile (i \u003C 3) {\n    i = i + 1\n    print(i)\n}\nprint(\"done\")\n\n[调试] 暂停在第3行，当前可见变量：{i=0.0}\n[调试] 下发指令：c（继续）\n1.0\n[调试] 暂停在第3行，当前可见变量：{i=1.0}\n[调试] 下发指令：n（单步）\n[调试] 暂停在第4行，当前可见变量：{i=2.0}\n[调试] 下发指令：c（继续）\n2.0\n[调试] 暂停在第3行，当前可见变量：{i=2.0}\n[调试] 下发指令：c（继续）\n3.0\n\"done\"\n[调试] 程序执行结束\n```\n\n跟着日志走一遍：程序第一次跑到第3行（`i = i + 1`）命中断点暂停，此时`i`还是循环开始时的`0.0`；下发`c`让它继续，`i`自增为`1.0`并打印；第二次命中断点时`i`是`1.0`，这次改成下发`n`（单步），于是只往前走一条语句，停在第4行`print(i)`执行之前，此时`i`已经自增成了`2.0`但还没打印出来；再下发`c`，打印出`2.0`，循环继续，第三次命中断点时`i`是`2.0`；最后一次`c`放行到程序结束，打印`3.0`和`\"done\"`。全程和断点、单步的语义完全对得上。\n\n## 四、遗留的问题\n\n目前的单步只有一种粒度——\"下一条语句\"，不管这条语句是不是函数调用。如果`n`所在的位置紧接着是一次函数调用，现在的实现会直接停在函数体内部的第一条语句上，这其实是通常所说的**Step Into**（进入函数内部）。但调试时经常还需要**Step Over**（把整个函数调用当成一步跳过去，不进入内部）和**Step Out**（跳出当前函数，停在调用它的地方），这两者都需要借助上一篇文章实现的调用栈来判断\"当前跳到哪一层了\"。下一篇文章我们就来实现这两种更精细的单步方式，并且把调用栈信息也展示出来。\n","\u003Cp>上一篇文章实现了自定义函数和调用栈，我们的小型DSL已经是一门功能齐全的编程语言了。从本篇开始，我们尝试实现一个更有意思的能力——\u003Cstrong>调试器\u003C\u002Fstrong>：设置断点、单步执行、暂停时查看变量。这篇文章先实现最基础的部分：断点和单步。注意本篇不涉及.g4语法的任何改动，纯粹是宿主程序（Java）层面新增的能力。\u003C\u002Fp>\n\u003Ch2>一、思路设计\u003C\u002Fh2>\n\u003Cp>我们的解释器是基于\u003Ccode>Visitor\u003C\u002Fcode>模式的树遍历，本质上是一次\"一口气从头跑到尾\"的递归函数调用，天然没有\"暂停点\"。要实现断点和单步，核心问题是：\u003Cstrong>怎么让解释器在执行到某一条语句时暂停下来，等外部指令决定它是继续跑还是再往前挪一步\u003C\u002Fstrong>？\u003C\u002Fp>\n\u003Cp>这里采用的方案是\u003Cstrong>把解释器的执行放到一个独立线程里跑\u003C\u002Fstrong>，在每条语句执行之前\"打卡\"一次：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>如果打卡的这一行命中了断点，或者当前处于单步模式，就把这个线程阻塞住。\u003C\u002Fli>\n \u003Cli>阻塞期间，另一个\"控制台线程\"（在真实场景里就是接收用户输入的线程）可以拿到暂停时的行号和当前作用域，展示给用户看。\u003C\u002Fli>\n \u003Cli>用户输入\u003Ccode>n\u003C\u002Fcode>（单步）或\u003Ccode>c\u003C\u002Fcode>（继续）之后，控制台线程通知被阻塞的解释器线程继续往下走。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>两个线程之间的通信，本质上是两次\"握手\"：解释器线程要让控制台线程知道\"我暂停了\"，控制台线程要让解释器线程知道\"可以继续了\"。这里用两个容量为1的阻塞队列（\u003Ccode>ArrayBlockingQueue\u003C\u002Fcode>）分别表示这两个方向的信号，谁先到谁就等着，不会出现请求丢失的问题，比用\u003Ccode>wait\u002Fnotify\u003C\u002Fcode>手写锁更不容易出错。\u003C\u002Fp>\n\u003Ch2>二、语法实现\u003C\u002Fh2>\n\u003Ch3>1. DebugController：两个线程之间的桥梁\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-java\">public class DebugController {\n\n    private final Set&lt;Integer&gt; breakpoints = ConcurrentHashMap.newKeySet();\n\n    \u002F**\n     * 是否处于单步模式，只对触发暂停后的下一条语句生效，消费一次就会被重置\n     *\u002F\n    private volatile boolean stepMode = false;\n\n    private final BlockingQueue&lt;PauseInfo&gt; pauseSignal = new ArrayBlockingQueue&lt;&gt;(1);\n    private final BlockingQueue&lt;Boolean&gt; resumeSignal = new ArrayBlockingQueue&lt;&gt;(1);\n\n    public void addBreakpoint(int line) {\n        breakpoints.add(line);\n    }\n\n    \u002F**\n     * 由解释器线程调用：每执行一条语句之前检查是否需要暂停\n     *\u002F\n    public void checkpoint(int line, Scope scope) {\n        if (!stepMode &amp;&amp; !breakpoints.contains(line)) {\n            return;\n        }\n        stepMode = false;\n        try {\n            pauseSignal.put(new PauseInfo(line, scope));\n            \u002F\u002F 阻塞在这里，直到控制台线程调用resume或stepNext\n            resumeSignal.take();\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n        }\n    }\n\n    \u002F**\n     * 由解释器线程调用：程序正常结束或者抛出异常终止时调用，发送一个哨兵值告知控制台线程\"结束了\"\n     *\u002F\n    public void finish() {\n        try {\n            pauseSignal.put(PauseInfo.FINISHED);\n        } catch (InterruptedException e) {\n            Thread.currentThread().interrupt();\n        }\n    }\n\n    \u002F**\n     * 由控制台线程调用：阻塞直到解释器线程暂停下来（命中断点\u002F单步\u002F程序结束），返回当时的信息\n     *\u002F\n    public PauseInfo awaitPause() throws InterruptedException {\n        return pauseSignal.take();\n    }\n\n    \u002F**\n     * 由控制台线程调用：让解释器线程继续跑，直到遇到下一个断点或程序结束\n     *\u002F\n    public void resume() {\n        stepMode = false;\n        resumeSignal.offer(true);\n    }\n\n    \u002F**\n     * 由控制台线程调用：只放行到下一条语句就再次暂停\n     *\u002F\n    public void stepNext() {\n        stepMode = true;\n        resumeSignal.offer(true);\n    }\n\n    \u002F**\n     * 解释器每次暂停时的快照：暂停在第几行、当时的作用域是什么\n     *\u002F\n    public static class PauseInfo {\n        static final PauseInfo FINISHED = new PauseInfo(-1, null);\n        public final int line;\n        public final Scope scope;\n        PauseInfo(int line, Scope scope) {\n            this.line = line;\n            this.scope = scope;\n        }\n        public boolean isFinished() {\n            return line == -1;\n        }\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cblockquote>\n \u003Cp>关于\u003Ccode>stepMode\u003C\u002Fcode>只生效一次：\u003Ccode>n\u003C\u002Fcode>（单步）应该只让\"紧接着的下一条语句\"暂停，而不是之后所有语句都暂停，所以\u003Ccode>checkpoint\u003C\u002Fcode>一旦因为\u003Ccode>stepMode\u003C\u002Fcode>触发了暂停，会立刻把它重置为\u003Ccode>false\u003C\u002Fcode>。如果用户接下来想继续单步，需要再次调用\u003Ccode>stepNext()\u003C\u002Fcode>。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch3>2. 打卡点：visitMain\u003C\u002Fh3>\n\u003Cp>前面几篇文章里，\u003Ccode>main\u003C\u002Fcode>语法对应的正是\"一条语句\"（变量定义、赋值、打印、if、while、break、continue……），天然就是解释器执行的最小步进单位，所以打卡点就选在\u003Ccode>visitMain\u003C\u002Fcode>：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F**\n * 调试控制器，为null时表示不启用调试，按普通方式一路执行到底，不产生任何额外开销\n *\u002F\nprivate final DebugController debugController;\n\npublic MyRuleSetVisitor() {\n    this(null);\n}\n\npublic MyRuleSetVisitor(DebugController debugController) {\n    this.debugController = debugController;\n}\n\n\u002F**\n * main对应语法里的每一条语句，是解释器执行的最小步进单位。在真正执行这条语句之前先\"打卡\"一下，\n * 如果启用了调试且命中了断点或处于单步模式，这里会阻塞住，等待调试控制台发出继续\u002F单步指令。\n *\u002F\n@Override\npublic VisitorResult visitMain(RuleSetParser.MainContext ctx) {\n    if (debugController != null) {\n        debugController.checkpoint(ctx.start.getLine(), currentScope);\n    }\n    return visitChildren(ctx);\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>debugController\u003C\u002Fcode>允许传\u003Ccode>null\u003C\u002Fcode>，这样不需要调试的场景（前几篇文章的用法）不会受到任何影响，也不会有额外开销——这是给已有功能\"平滑加装\"新能力的常见做法。\u003C\u002Fp>\n\u003Ch3>3. 查看变量：区分\"自己的变量\"和\"看得见的变量\"\u003C\u002Fh3>\n\u003Cp>暂停时要把变量展示出来，但\u003Ccode>Scope\u003C\u002Fcode>当前只保存\"当前这一层自己的变量\"。如果只展示这一层，很多时候会是空的（比如\u003Ccode>while\u003C\u002Fcode>循环体\u003Ccode>block\u003C\u002Fcode>刚进入时，自己还没有定义任何变量，\u003Ccode>i\u003C\u002Fcode>是在外层定义的）。所以在\u003Ccode>Scope\u003C\u002Fcode>里补充一个方法，沿着作用域链把所有能看到的变量都合并起来，内层同名变量覆盖外层的：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">\u002F**\n * 返回当前这一层作用域自己定义的变量（不含外层），主要给调试器展示\"当前作用域内的变量\"用\n *\u002F\npublic Map&lt;String, Object&gt; getOwnVariables() {\n    return Collections.unmodifiableMap(variables);\n}\n\n\u002F**\n * 返回当前位置沿作用域链能看到的所有变量（含外层），内层同名变量会覆盖外层的，\n * 这才是调试时\"当前所有可见变量\"应该展示的完整视图\n *\u002F\npublic Map&lt;String, Object&gt; getVisibleVariables() {\n    List&lt;Scope&gt; chain = new ArrayList&lt;&gt;();\n    for (Scope scope = this; scope != null; scope = scope.parent) {\n        chain.add(scope);\n    }\n    Map&lt;String, Object&gt; merged = new LinkedHashMap&lt;&gt;();\n    for (int i = chain.size() - 1; i &gt;= 0; i--) {\n        merged.putAll(chain.get(i).variables);\n    }\n    return merged;\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>三、测试代码与执行结果\u003C\u002Fh2>\n\u003Cp>因为这篇文章的重点是机制本身，没有接入真实的控制台输入，用一个\u003Ccode>commands\u003C\u002Fcode>数组模拟依次下发的调试指令，实际使用时把这里换成读取\u003Ccode>Scanner\u003C\u002Fcode>的用户输入即可：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-java\">private static void debugDemo() throws InterruptedException {\n    String expression =\n            \"number i = 0 \\n\" +                    \u002F\u002F 第1行\n                    \"while (i &lt; 3) { \\n\" +          \u002F\u002F 第2行\n                    \"    i = i + 1 \\n\" +             \u002F\u002F 第3行，断点设在这一行\n                    \"    print(i) \\n\" +               \u002F\u002F 第4行\n                    \"} \\n\" +                          \u002F\u002F 第5行\n                    \"print(\\\"done\\\")\";                 \u002F\u002F 第6行\n\n    RuleSetLexer lexer = new RuleSetLexer(CharStreams.fromString(expression));\n    RuleSetParser parser = new RuleSetParser(new CommonTokenStream(lexer));\n    RuleSetParser.ProgContext prog = parser.prog();\n\n    DebugController debugController = new DebugController();\n    debugController.addBreakpoint(3);\n    MyRuleSetVisitor visitor = new MyRuleSetVisitor(debugController);\n\n    \u002F\u002F 解释器放到独立线程里跑，避免阻塞住控制台线程\n    Thread interpreterThread = new Thread(() -&gt; {\n        try {\n            visitor.visit(prog);\n        } finally {\n            debugController.finish();\n        }\n    });\n    interpreterThread.start();\n\n    \u002F\u002F 模拟依次下发的调试指令：第一次命中断点直接continue，第二次改成单步，之后再continue到结束\n    String[] commands = {\"c\", \"n\", \"c\", \"c\"};\n    int commandIndex = 0;\n    while (true) {\n        DebugController.PauseInfo pause = debugController.awaitPause();\n        if (pause.isFinished()) {\n            System.out.println(\"[调试] 程序执行结束\");\n            break;\n        }\n        System.out.println(\"[调试] 暂停在第\" + pause.line + \"行，当前可见变量：\" + pause.scope.getVisibleVariables());\n        String command = commands[commandIndex++];\n        if (\"n\".equals(command)) {\n            System.out.println(\"[调试] 下发指令：n（单步）\");\n            debugController.stepNext();\n        } else {\n            System.out.println(\"[调试] 下发指令：c（继续）\");\n            debugController.resume();\n        }\n    }\n    interpreterThread.join();\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>执行结果如下：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>执行:\nnumber i = 0\nwhile (i &lt; 3) {\n    i = i + 1\n    print(i)\n}\nprint(\"done\")\n\n[调试] 暂停在第3行，当前可见变量：{i=0.0}\n[调试] 下发指令：c（继续）\n1.0\n[调试] 暂停在第3行，当前可见变量：{i=1.0}\n[调试] 下发指令：n（单步）\n[调试] 暂停在第4行，当前可见变量：{i=2.0}\n[调试] 下发指令：c（继续）\n2.0\n[调试] 暂停在第3行，当前可见变量：{i=2.0}\n[调试] 下发指令：c（继续）\n3.0\n\"done\"\n[调试] 程序执行结束\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>跟着日志走一遍：程序第一次跑到第3行（\u003Ccode>i = i + 1\u003C\u002Fcode>）命中断点暂停，此时\u003Ccode>i\u003C\u002Fcode>还是循环开始时的\u003Ccode>0.0\u003C\u002Fcode>；下发\u003Ccode>c\u003C\u002Fcode>让它继续，\u003Ccode>i\u003C\u002Fcode>自增为\u003Ccode>1.0\u003C\u002Fcode>并打印；第二次命中断点时\u003Ccode>i\u003C\u002Fcode>是\u003Ccode>1.0\u003C\u002Fcode>，这次改成下发\u003Ccode>n\u003C\u002Fcode>（单步），于是只往前走一条语句，停在第4行\u003Ccode>print(i)\u003C\u002Fcode>执行之前，此时\u003Ccode>i\u003C\u002Fcode>已经自增成了\u003Ccode>2.0\u003C\u002Fcode>但还没打印出来；再下发\u003Ccode>c\u003C\u002Fcode>，打印出\u003Ccode>2.0\u003C\u002Fcode>，循环继续，第三次命中断点时\u003Ccode>i\u003C\u002Fcode>是\u003Ccode>2.0\u003C\u002Fcode>；最后一次\u003Ccode>c\u003C\u002Fcode>放行到程序结束，打印\u003Ccode>3.0\u003C\u002Fcode>和\u003Ccode>\"done\"\u003C\u002Fcode>。全程和断点、单步的语义完全对得上。\u003C\u002Fp>\n\u003Ch2>四、遗留的问题\u003C\u002Fh2>\n\u003Cp>目前的单步只有一种粒度——\"下一条语句\"，不管这条语句是不是函数调用。如果\u003Ccode>n\u003C\u002Fcode>所在的位置紧接着是一次函数调用，现在的实现会直接停在函数体内部的第一条语句上，这其实是通常所说的\u003Cstrong>Step Into\u003C\u002Fstrong>（进入函数内部）。但调试时经常还需要\u003Cstrong>Step Over\u003C\u002Fstrong>（把整个函数调用当成一步跳过去，不进入内部）和\u003Cstrong>Step Out\u003C\u002Fstrong>（跳出当前函数，停在调用它的地方），这两者都需要借助上一篇文章实现的调用栈来判断\"当前跳到哪一层了\"。下一篇文章我们就来实现这两种更精细的单步方式，并且把调用栈信息也展示出来。\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},"2101631008783122433","antlr4-09-debugger-step-over","Antlr4系列⑨：调试器进阶之Step Over与条件断点","Step Into\u002FOver\u002FOut、调用栈展示、条件断点",5666,15,"2026-09-20T19:14:22",{"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},"2101635146136862722","antlr4-10-arrays","Antlr4系列⑩：支持数组","array类型、字面量、下标读写、len",5036,13,"2026-09-20T19:30:48",{"id":50,"slug":51,"title":52,"summary":53,"coverUrl":20,"categoryId":21,"status":22,"topFlag":4,"viewCount":4,"likeCount":4,"wordCount":54,"readingMinutes":47,"publishedAt":55},"2101608263357743105","antlr4-05-loops","Antlr4系列⑤：支持循环语句","while循环、break\u002Fcontinue（用异常做控制流跳转）",4801,"2026-09-20T17:44:00",{"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},"2101619933819879425","antlr4-07-functions-and-call-stack","Antlr4系列⑦：自定义函数与调用栈","function定义\u002F调用、递归、词法作用域、调用栈防死递归",6508,17,"2026-09-20T18:33:25",{"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},{"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>上一篇文章实现了自定义函数和调用栈，我们的小型DSL已经是一门功能齐全的编程语言了。从本篇开始，我们尝试实现一个更有意思的能力——\u003Cstrong>调试器\u003C\u002Fstrong>：设置断点、单步执行、暂停时查看变量。这篇文章先实现最基础的部分：断点和单步。注意本篇不涉及.g4语法的任何改动，纯粹是宿主程序（Java）层面新增的能力。\u003C\u002Fp>\n\u003Ch2 id=\"toc-1\">一、思路设计\u003C\u002Fh2>\n\u003Cp>我们的解释器是基于\u003Ccode>Visitor\u003C\u002Fcode>模式的树遍历，本质上是一次\"一口气从头跑到尾\"的递归函数调用，天然没有\"暂停点\"。要实现断点和单步，核心问题是：\u003Cstrong>怎么让解释器在执行到某一条语句时暂停下来，等外部指令决定它是继续跑还是再往前挪一步\u003C\u002Fstrong>？\u003C\u002Fp>\n\u003Cp>这里采用的方案是\u003Cstrong>把解释器的执行放到一个独立线程里跑\u003C\u002Fstrong>，在每条语句执行之前\"打卡\"一次：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>如果打卡的这一行命中了断点，或者当前处于单步模式，就把这个线程阻塞住。\u003C\u002Fli>\n \u003Cli>阻塞期间，另一个\"控制台线程\"（在真实场景里就是接收用户输入的线程）可以拿到暂停时的行号和当前作用域，展示给用户看。\u003C\u002Fli>\n \u003Cli>用户输入\u003Ccode>n\u003C\u002Fcode>（单步）或\u003Ccode>c\u003C\u002Fcode>（继续）之后，控制台线程通知被阻塞的解释器线程继续往下走。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>两个线程之间的通信，本质上是两次\"握手\"：解释器线程要让控制台线程知道\"我暂停了\"，控制台线程要让解释器线程知道\"可以继续了\"。这里用两个容量为1的阻塞队列（\u003Ccode>ArrayBlockingQueue\u003C\u002Fcode>）分别表示这两个方向的信号，谁先到谁就等着，不会出现请求丢失的问题，比用\u003Ccode>wait\u002Fnotify\u003C\u002Fcode>手写锁更不容易出错。\u003C\u002Fp>\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:#D73A49;--shiki-dark:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\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\">\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\"> Set&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Integer\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> breakpoints \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> ConcurrentHashMap.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">newKeySet\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\">     *\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\"> volatile\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> boolean\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> stepMode \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\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\">\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\"> BlockingQueue&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">PauseInfo\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> pauseSignal \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\"> ArrayBlockingQueue&#x3C;>(\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">1\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    private\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> BlockingQueue&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">Boolean\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> resumeSignal \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\"> ArrayBlockingQueue&#x3C;>(\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">1\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\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\"> addBreakpoint\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">int\u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\"> line\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        breakpoints.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">add\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\">    }\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\">     *\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:#E36209;--shiki-dark:#FFAB70\"> line\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, Scope \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">scope\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\"> (\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">!\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">stepMode \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">&#x26;&#x26;\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> !\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">breakpoints.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">contains\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\">            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:#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:#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, scope));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">            \u002F\u002F 阻塞在这里，直到控制台线程调用resume或stepNext\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:#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:#D73A49;--shiki-dark:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> void\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> finish\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() {\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\">(PauseInfo.FINISHED);\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:#6A737D;--shiki-dark:#6A737D\">    \u002F**\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">     * 由控制台线程调用：阻塞直到解释器线程暂停下来（命中断点\u002F单步\u002F程序结束），返回当时的信息\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#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:#24292E;--shiki-dark:#E1E4E8\"> PauseInfo \u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">awaitPause\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">() \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">throws\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> InterruptedException {\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\"> pauseSignal.\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>\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\">     *\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\"> resume\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:#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\">        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>\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\"> stepNext\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:#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\">        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>\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\"> static\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> class\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> PauseInfo\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">        static\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> PauseInfo FINISHED \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\"> PauseInfo\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">-\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">1\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, \u003C\u002Fspan>\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\">        public\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\"> line;\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\"> final\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> Scope scope;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">        PauseInfo\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">int\u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\"> line\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">, Scope \u003C\u002Fspan>\u003Cspan style=\"color:#E36209;--shiki-dark:#FFAB70\">scope\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\">.line \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> line;\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\">.scope \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> scope;\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\">        public\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> boolean\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> isFinished\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\"> line \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">==\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> -\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">1\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cblockquote>\n \u003Cp>关于\u003Ccode>stepMode\u003C\u002Fcode>只生效一次：\u003Ccode>n\u003C\u002Fcode>（单步）应该只让\"紧接着的下一条语句\"暂停，而不是之后所有语句都暂停，所以\u003Ccode>checkpoint\u003C\u002Fcode>一旦因为\u003Ccode>stepMode\u003C\u002Fcode>触发了暂停，会立刻把它重置为\u003Ccode>false\u003C\u002Fcode>。如果用户接下来想继续单步，需要再次调用\u003Ccode>stepNext()\u003C\u002Fcode>。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch3 id=\"toc-4\">2. 打卡点：visitMain\u003C\u002Fh3>\n\u003Cp>前面几篇文章里，\u003Ccode>main\u003C\u002Fcode>语法对应的正是\"一条语句\"（变量定义、赋值、打印、if、while、break、continue……），天然就是解释器执行的最小步进单位，所以打卡点就选在\u003Ccode>visitMain\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\"> * 调试控制器，为null时表示不启用调试，按普通方式一路执行到底，不产生任何额外开销\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\"> DebugController debugController;\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\"> MyRuleSetVisitor\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\">(\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:#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:#6F42C1;--shiki-dark:#B392F0\"> MyRuleSetVisitor\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(DebugController debugController) {\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\">.debugController \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> debugController;\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\"> * main对应语法里的每一条语句，是解释器执行的最小步进单位。在真正执行这条语句之前先\"打卡\"一下，\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:#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\">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:#24292E;--shiki-dark:#E1E4E8\">        debugController.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">checkpoint\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\">(), 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\u003Cp>\u003Ccode>debugController\u003C\u002Fcode>允许传\u003Ccode>null\u003C\u002Fcode>，这样不需要调试的场景（前几篇文章的用法）不会受到任何影响，也不会有额外开销——这是给已有功能\"平滑加装\"新能力的常见做法。\u003C\u002Fp>\n\u003Ch3 id=\"toc-5\">3. 查看变量：区分\"自己的变量\"和\"看得见的变量\"\u003C\u002Fh3>\n\u003Cp>暂停时要把变量展示出来，但\u003Ccode>Scope\u003C\u002Fcode>当前只保存\"当前这一层自己的变量\"。如果只展示这一层，很多时候会是空的（比如\u003Ccode>while\u003C\u002Fcode>循环体\u003Ccode>block\u003C\u002Fcode>刚进入时，自己还没有定义任何变量，\u003Ccode>i\u003C\u002Fcode>是在外层定义的）。所以在\u003Ccode>Scope\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\"> *\u002F\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\"> Map\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">String, Object\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">>\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> getOwnVariables\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\"> Collections.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">unmodifiableMap\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(variables);\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\"> * 这才是调试时\"当前所有可见变量\"应该展示的完整视图\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:#24292E;--shiki-dark:#E1E4E8\"> Map\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">&#x3C;\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">String, Object\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">>\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\"> getVisibleVariables\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\">Scope\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> chain \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:#D73A49;--shiki-dark:#F97583\">    for\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (Scope scope \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\"> this\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">; scope \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\">; scope \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> scope.parent) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        chain.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">add\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(scope);\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\">    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\">Object\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">> merged \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\"> LinkedHashMap&#x3C;>();\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:#24292E;--shiki-dark:#E1E4E8\"> chain.\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:#005CC5;--shiki-dark:#79B8FF\"> 1\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\">--\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        merged.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">putAll\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(chain.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">get\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(i).variables);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    return\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> merged;\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\u003Ch2 id=\"toc-6\">三、测试代码与执行结果\u003C\u002Fh2>\n\u003Cp>因为这篇文章的重点是机制本身，没有接入真实的控制台输入，用一个\u003Ccode>commands\u003C\u002Fcode>数组模拟依次下发的调试指令，实际使用时把这里换成读取\u003Ccode>Scanner\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:#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\"> debugDemo\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>\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\">                    \"while (i &#x3C; 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 第2行\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>\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\">                    \"    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 第4行\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 第5行\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">                    \"print(\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">done\u003C\u002Fspan>\u003Cspan style=\"color:#005CC5;--shiki-dark:#79B8FF\">\\\"\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">)\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">;                 \u003C\u002Fspan>\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">\u002F\u002F 第6行\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    RuleSetLexer lexer \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\">(expression));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    RuleSetParser parser \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\">(lexer));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    RuleSetParser.ProgContext prog \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> parser.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">prog\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\">addBreakpoint\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\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    MyRuleSetVisitor visitor \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\"> MyRuleSetVisitor\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(debugController);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D;--shiki-dark:#6A737D\">    \u002F\u002F 解释器放到独立线程里跑，避免阻塞住控制台线程\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    Thread interpreterThread \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\"> Thread\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\"> {\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\">            visitor.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">visit\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(prog);\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\">            debugController.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">finish\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\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    interpreterThread.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">start\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\u002F 模拟依次下发的调试指令：第一次命中断点直接continue，第二次改成单步，之后再continue到结束\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    String\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">[] commands \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\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\">\"n\"\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\">\"c\"\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\"> commandIndex \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\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">    while\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\">        DebugController.PauseInfo pause \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\">awaitPause\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\"> (pause.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">isFinished\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">()) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"[调试] 程序执行结束\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">            break\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\">        System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"[调试] 暂停在第\"\u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\"> +\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> pause.line \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\"> pause.scope.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">getVisibleVariables\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">());\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">        String command \u003C\u002Fspan>\u003Cspan style=\"color:#D73A49;--shiki-dark:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> commands[commandIndex\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:#D73A49;--shiki-dark:#F97583\">        if\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> (\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"n\"\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">equals\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(command)) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#032F62;--shiki-dark:#9ECBFF\">\"[调试] 下发指令：n（单步）\"\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\">stepNext\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\">else\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\"> {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">            System.out.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">println\u003C\u002Fspan>\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#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\">            debugController.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">resume\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\">\u003Cspan style=\"color:#24292E;--shiki-dark:#E1E4E8\">    interpreterThread.\u003C\u002Fspan>\u003Cspan style=\"color:#6F42C1;--shiki-dark:#B392F0\">join\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>执行:\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>number i = 0\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>while (i &#x3C; 3) {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    i = i + 1\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>    print(i)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>print(\"done\")\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[调试] 暂停在第3行，当前可见变量：{i=0.0}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[调试] 下发指令：c（继续）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>1.0\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[调试] 暂停在第3行，当前可见变量：{i=1.0}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[调试] 下发指令：n（单步）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[调试] 暂停在第4行，当前可见变量：{i=2.0}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[调试] 下发指令：c（继续）\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>2.0\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan>[调试] 暂停在第3行，当前可见变量：{i=2.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>\"done\"\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>跟着日志走一遍：程序第一次跑到第3行（\u003Ccode>i = i + 1\u003C\u002Fcode>）命中断点暂停，此时\u003Ccode>i\u003C\u002Fcode>还是循环开始时的\u003Ccode>0.0\u003C\u002Fcode>；下发\u003Ccode>c\u003C\u002Fcode>让它继续，\u003Ccode>i\u003C\u002Fcode>自增为\u003Ccode>1.0\u003C\u002Fcode>并打印；第二次命中断点时\u003Ccode>i\u003C\u002Fcode>是\u003Ccode>1.0\u003C\u002Fcode>，这次改成下发\u003Ccode>n\u003C\u002Fcode>（单步），于是只往前走一条语句，停在第4行\u003Ccode>print(i)\u003C\u002Fcode>执行之前，此时\u003Ccode>i\u003C\u002Fcode>已经自增成了\u003Ccode>2.0\u003C\u002Fcode>但还没打印出来；再下发\u003Ccode>c\u003C\u002Fcode>，打印出\u003Ccode>2.0\u003C\u002Fcode>，循环继续，第三次命中断点时\u003Ccode>i\u003C\u002Fcode>是\u003Ccode>2.0\u003C\u002Fcode>；最后一次\u003Ccode>c\u003C\u002Fcode>放行到程序结束，打印\u003Ccode>3.0\u003C\u002Fcode>和\u003Ccode>\"done\"\u003C\u002Fcode>。全程和断点、单步的语义完全对得上。\u003C\u002Fp>\n\u003Ch2 id=\"toc-7\">四、遗留的问题\u003C\u002Fh2>\n\u003Cp>目前的单步只有一种粒度——\"下一条语句\"，不管这条语句是不是函数调用。如果\u003Ccode>n\u003C\u002Fcode>所在的位置紧接着是一次函数调用，现在的实现会直接停在函数体内部的第一条语句上，这其实是通常所说的\u003Cstrong>Step Into\u003C\u002Fstrong>（进入函数内部）。但调试时经常还需要\u003Cstrong>Step Over\u003C\u002Fstrong>（把整个函数调用当成一步跳过去，不进入内部）和\u003Cstrong>Step Out\u003C\u002Fstrong>（跳出当前函数，停在调用它的地方），这两者都需要借助上一篇文章实现的调用栈来判断\"当前跳到哪一层了\"。下一篇文章我们就来实现这两种更精细的单步方式，并且把调用栈信息也展示出来。\u003C\u002Fp>",[83,87,90,94,97,100,103],{"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. 打卡点：visitMain",{"id":98,"text":99,"level":93},"toc-5","3. 查看变量：区分\"自己的变量\"和\"看得见的变量\"",{"id":101,"text":102,"level":86},"toc-6","三、测试代码与执行结果",{"id":104,"text":105,"level":86},"toc-7","四、遗留的问题"]