第 39 章 / 共 58 章 卷八 工程与原理

错误诊断与调试

前 38 章你见过的每一条报错都不是偶然——它们分属清晰的四层防线:编译期、链接期、运行期、进程级。本章把全书散落各章的错误现象收拢成一张地图:诊断格式怎么读、parse 为什么报一次就停而 sem 能一次报一串、exit 3/4 为什么一个字符都不打、exit 127 到底有多少种成因。学完你面对任何报错的第一反应不再是"完了",而是"这是第几层的问题"。

本章导读

调试能力 = 诊断阅读能力 × 定位方法论。本章先解决"读":xlang 的每条诊断都带位置(file:line:col)、阶段标签([parse]/[sem])和消息,--json 把它们变成机器可读的三字段结构;运行时错误则要认三种形态——会说话的异常(uncaught XxxError: 消息)、沉默的 fatal(exit 3/4 一个字符都不打)、和根本不像语言错误的进程级死亡(exit 127)。再解决"找":编译期问题用第 37 章的管线取证法(check 定阶段、ir 看语义、--dump-dir 拿六件套),运行期问题用最小复现、二分注释、防御性断言三板斧。本章不写调试器——dbg/dap 已从正式 CLI 删除(39.10 如实说明官方口径与替代方案)。读完后建议把 39.6 的三形态对照表和深入细节 3 的退出码全景表打印出来贴在手边,它们是你在 xlang 世界里的急诊分诊台。

本章学习目标

掌握四层防线模型并能对任何报错归类;熟练阅读 file:line:col + [阶段] 诊断格式与 --json 三字段结构(phase 三态 checked/parse/sem);理解 parse 报一次停与 sem 收集多错的设计差异及级联诊断"先修第一条"原则;背下运行期三形态对照(可捕获异常 exit 1 带消息 / 静默 fatal exit 3 越界、exit 4 as 失败与 null 调用 / 进程级 exit 127 的多种成因);知道 uncaught 只报 throw 点、无调用栈的现实与三种补偿手段;掌握管线取证调试法(check→ir→dump-dir→save-temps);会用最小复现、二分注释法、防御性断言、println 打点;了解 dbg/dap 删除的官方口径与当前调试替代方案。

39.1 四层防线总览

一个 xlang 程序从出生到运行要过四道关,每一关的错误形态、诊断能力、排查工具完全不同——先建立全景,再逐层深入:

层关卡典型错误诊断形态排查工具
① 编译期lex → parse → sem语法错误、类型不匹配、未声明标识符file:line:col: [阶段] 消息;parse 报一次停,sem 收集多个xl check(--json)、xl lex/parse/ir
② 链接期TCC 编译生成 Cextern 符号缺失、ABI 位宽冲突tcc: error: unresolved reference to '...'(带 __xlvfs__ 路径)cgen --dump-dir、--save-temps
③ 运行期程序执行中throw、除零、断言失败(可捕获);越界、as 失败、null 调用(fatal)异常:行:列: uncaught XxxError: 消息(exit 1);fatal:静默(exit 3/4)try/catch、assert、println 打点
④ 进程级OS 加载与进程生命缺 DLL、栈溢出崩溃、所有权重复释放完全静默,exit 127成因二分法(39.6)

关键认知:越往下层,诊断越少,预防越重要。①层有精确到列的诊断和错误恢复;②层有符号名可查;③层一半会说话一半沉默;④层只剩一个退出码。高手的工作流是把问题尽量堵在上层——类型标注让 sem 替你把关(①),assert 在状态变坏的第一现场报警(③),防御性检查让 fatal 无机可乘(③④)。

39.2 诊断格式与 --json

编译期诊断的统一格式四段式:文件:行:列: [阶段] 消息——

e_sem.xl:2:13: [sem] 初始化表达式类型和声明类型不匹配: target int, value string
e_sem.xl:3:13: [sem] undeclared identifier 'm'

消息文本中英文混合是当前现实(初始化错误用中文,undeclared identifier 用英文)——写工具匹配诊断时请按结构(位置+阶段)解析,别按消息文本匹配。

xl check --json 把诊断结构化,三个顶层字段(实测三种结果):

$ xl check app.xl --json
{"ok":true,"phase":"checked","diagnostics":[]}
$ xl check e_parse.xl --json
{"ok":false,"phase":"parse","diagnostics":[{"file":"e_parse.xl","line":4,"column":4,"message":"语法错误"}]}
$ xl check e_sem.xl --json
{"ok":false,"phase":"sem","diagnostics":[{"file":"e_sem.xl","line":2,"column":13,"message":"初始化表达式类型和声明类型不匹配: target int, value string"},{"file":"e_sem.xl","line":3,"column":13,"message":"undeclared identifier 'm'"}]}

phase 是机器分诊的核心字段,实测三态:checked(全部通过)、parse(语法阶段倒下)、sem(语义阶段倒下)。注意词法层的错误也归在 parse 阶段——39.3 会看到这个归并的原因。diagnostics 数组的元素永远带 file/line/column/message 四字段,编辑器插件据此画波浪线即可,不需要理解消息内容。

39.3 lex/parse:报一次停

先看一个反直觉实测:@ 这种"非法字符"在 xlang 里根本不触发词法错误——

$ cat e_lex.xl
func main(...)
    int n = 10 @ 5
end
$ xl lex e_lex.xl
   ……
   2:13  INT              10
   2:16  AT               @        ← @ 被切成合法 AT token
   2:18  INT              5
   ……
# exit 0 —— lex 阶段毫无意见
$ xl check e_lex.xl
e_lex.xl:2:16: [parse] 语法错误   ← 是 parse 不认 AT

xlang 的 lexer 对单字符几乎来者不拒(@、$ 都有 token 名),真正的词法错误只剩少数形态——最典型是未闭合的字符串,它产生专门的 ERROR token:

$ xl lex e_str.xl
   ……
   2:16  ERROR            "unclosed  # 未闭合的字符串
   ……
cannot dump tokens: e_str.xl
# exit 1 —— token 流照打,收尾报错
$ xl check e_str.xl
e_str.xl:2:16: [parse] 未闭合的字符串

所以 phase 字段没有 "lex" 值的原因就清楚了:词法问题要么不存在(被切成合法 token,留给 parse 拒),要么以 ERROR token 形式进入 parse 被拒——诊断上统一记 parse 阶段。

parse 的脾气是"报一次就停"。语法树断了就续不上,继续分析只会产生垃圾诊断,所以 parse 永远只报第一条(上面 JSON 样本里 diagnostics 只有一项)。推论:修 parse 错误永远从报出来的第一条修起,修完重跑——后面的"错误"很可能只是第一条的连锁反应。

39.4 sem:收集多个错误

语义阶段相反——它走遍整棵树,把能收集的错误一次性报完(实测一个 4 行错误的文件报出 5 条):

$ cat e_multi.xl
func main(...)
    int a = "x"
    int b = "y"
    foo()
    int c = a + "z"
end
$ xl check e_multi.xl
e_multi.xl:2:13: [sem] 初始化表达式类型和声明类型不匹配: target int, value string
e_multi.xl:3:13: [sem] 初始化表达式类型和声明类型不匹配: target int, value string
e_multi.xl:4:5: [sem] undeclared identifier 'foo'
e_multi.xl:5:15: [sem] 字符串拼接两侧都必须是 string
e_multi.xl:5:15: [sem] 初始化表达式类型和声明类型不匹配: target int, value string

看第 5 行:一个 a + "z" 触发两条诊断——先报"拼接两侧必须都是 string",随后报"初始化类型不匹配"。第二条是第一条的级联:拼接失败后结果类型无法确定,按 int 继续检查自然又不匹配。由此得到本章最重要的修复纪律:级联诊断,先修第一条。5 条诊断的真实问题只有 4 个,修完第 5 行的拼接类型,第二条初始化不匹配自动消失。一堆报错扑面而来时别慌——按行号排序从第一条修,每修一条重跑 check,诊断数量常常应声减半。

sem 还是编译期防线的最后一道闸,很多"运行时灾难"在这里就被拦下——实测把 null 直接赋给 string:

r_null.xl:2:16: [sem] 初始化表达式类型和声明类型不匹配: target string, value null

这类错误根本活不到运行期。类型标注写得越认真,sem 替你拦截的就越多——这就是第 4 章"建议显式标注类型"的深层回报。

39.5 check 与 CI 集成

xl check 是为自动化而生的命令:不运行、不生成、只诊断。它与 CI 的契约就三条(均实测):

契约内容CI 用法
退出码通过 0,任何阶段失败 1xl check src/*.xl || exit 1 门禁
stdout 文本通过打 ok,失败打诊断清单直接进 CI 日志
--json{"ok","phase","diagnostics":[{file,line,column,message}]}解析后转 JUnit/annotations 上报

一个最小但够用的 CI 流水线片段:

ci-check.sh —— 全量检查 + 失败即停
for f in src/*.xl; do
    xl check "$f" --json > "reports/$(basename $f .xl).json" || FAILED=1
done
[ -n "$FAILED" ] && exit 1

每个文件一份 JSON 诊断存档,phase 字段还能做精细策略:比如 parse 失败算阻塞(代码根本写错了),sem 里某些已知历史问题可以暂时豁免——机器可读的意义就是策略可编程。嵌入场景同样能力在 xlcCheck(第 38 章 38.10),IDE 保存即检查走那条路。

39.6 运行期三形态对照

编译通过后,错误的画风突变——先认全三种形态(全部本章实测):

形态一:会说话的异常(exit 1)。throw、除零、断言失败这类可捕获错误,没人 catch 时运行时在 stderr 打一行 uncaught 诊断:

$ xl run r_throw.xl
before throw
tcc: __xlvfs__/module.c:449: warning: 'return' with no value
3:5: uncaught Error: boom happened
# exit 1
$ xl run r_divzero.xl
before
5:22: uncaught ZeroDivisionError: integer division by zero
$ xl run r_assert.xl
3:5: uncaught AssertionError: n should be 42

三个细节:行:列: 前缀不带文件名(多文件工程里要结合上下文判断);uncaught 时 TCC 会附带一条 warning: 'return' with no value 噪音(深入细节 2 解释,可忽略);诊断全在 stderr,程序自身输出在 stdout,2>nul 可以只看正常输出。try/catch 接住则完全无害(caught: boom happened / recovered,exit 0)。

形态二:沉默的 fatal(exit 3 / 4)。数组越界、as 失败、null 调方法——程序逻辑已错的信号,运行时选择立即终止,一个字符的诊断都不打:

$ xl run r_oob.xl       # arr[10],数组只有 3 个元素
before
# exit 3 —— "after" 永远没机会打印,也没有任何错误消息
$ xl run r_asfail.xl    # Animal 实例 as Dog
before
# exit 4 —— 同样沉默
$ xl run r_nullcall.xl  # Point p = null 然后 p.move(1)
before
# exit 4 —— null 调用与 as 失败同码

try 兜不住它们(第 10 章已定论,本章复核):try/catch 包越界,catch 块根本不执行,照样 exit 3。排查手段只能靠 39.9 的方法论(println 二分 + assert 前移)。

形态三:进程级死亡(exit 127)。程序根本没好好跑起来或中途崩溃,至少四种成因(全部实测过):

exit 127 成因出处识别线索
缺 DLL(--loadlib 依赖 / xl.dll 未随宿主分发)第 37/38 章程序启动即死,一行输出都没有
栈溢出(无限递归)本章实测:dive(n+1) 自递归"before" 打印后崩溃
传参 xvalue 宿主重复 unref第 38 章坑 2脚本输出完整,收尾时死
Queue<var> 声明即段错误等编译器缺陷第 29 章特定语言构造触发,最小复现可定位

127 的排查心法:它从不是 xlang 语言层的错误——别去查语法,先按上表对号成因。三形态合起来的分诊口诀:有消息看消息(形态一),没消息看退出码(3/4 查 fatal 点,127 查进程环境)。

39.7 异常定位与无栈现实

uncaught 诊断的行号是 throw 点,不是出错逻辑的发源地——实测三层调用链:

$ cat r_prop.xl
func inner()
    throw "deep error"
end
func outer()
    inner()
end
func main(...)
    outer()
end
$ xl run r_prop.xl
2:5: uncaught Error: deep error
# 报的是第 2 行 inner 里的 throw,没有 outer/main 的调用链

当前版本没有调用栈输出——throw 穿过 outer、main 的轨迹被吃掉了。小项目无所谓,大项目里一个底层工具函数被几十处调用时,"谁把坏数据传进来的"才是你真正要问的。三种补偿手段:

手段一,分段 catch 补上下文。在关键中层函数 catch 后重新 throw 带上下文的分类错误(第 10 章重抛模式):底层报"deep error",中层包装成 throw FileError "loadConfig: deep error"——消息链代替调用栈。

手段二,日志打点。log 模块(第 36 章)在中层入口打 info 级"进入 outer, 参数=...",uncaught 后按日志回溯路径——比反复改代码加 println 体面。

手段三,防御性断言前移。在函数入口 assert 参数不变量(39.9 展开),让错误在最近现场爆炸——断言消息里写明期望与实际,比 uncaught 的裸行号信息量大得多。

39.8 管线取证调试法

怀疑"编译器理解错了我的代码"或错误发生在链接期时,启用第 37 章建立的取证工具链。系统化后的排查动线:

取证动线(自上而下,哪层现形停在哪)
① xl check --json        → phase 字段直接告诉你是 parse 还是 sem 的问题
② xl parse f.xl          → 语法树长什么样?优先级、结构是否符合预期
③ xl ir f.xl             → 语义加工后什么样?隐式转换、注入物是否如预期
④ xl cgen f.xl --dump-dir dump   → 六件套全量取证(tokens/ast/xlir/abi/c/h)
⑤ xl run f.xl --save-temps       → 拿到正在被执行的那份 module.c 原文

链接期错误是这套工具的主战场。第 37 章的 unresolved reference 案例完整走一遍动线:tcc: tcc: error: unresolved reference to 'c_add3'——报错来自 TCC(②层),说明 xlang 侧全部通过;报错里的符号名 c_add3 是你 extern 声明的名字;cgen --emit 看生成 C 里这个符号的声明形态对不对;最后确认 --loadlib/--link 是否给了符号的"尸体"。报错路径里的 __xlvfs__/ 前缀(如 csrc_0.c)提醒你:这份 C 是动态 VFS 里的生成物,不是磁盘文件,要看原文请 --save-temps 导出。

ABI 位宽冲突同样靠 cgen 取证:第 37 章的 incompatible types for redefinition(xlang int 64 位 vs C int 32 位)在生成 C 里一眼可见——声明是 long long helper_triple_plus1(long long),定义是 int helper_triple_plus1(int),对不上就是位宽写错(int32 修正)。凡是"编译过了但链接炸"的问题,答案都写在生成 C 里。

39.9 实用调试技巧

技巧一:最小复现。遇到诡异行为,先把程序砍到不能再砍——本书第 29 章定位 Queue<var> 段错误、本章定位多线程 string() 空串,用的都是这个套路:原程序几百行 → 砍掉无关功能 → 砍掉一半代码看问题是否还在 → 最终得到一个十几行、任何人拿去都能复现的文件。最小复现既是报 bug 的礼仪,也是最快的定位器——问题往往就在砍掉某一块的瞬间自己现形。

技巧二:二分注释法。fatal 静默死亡(exit 3/4)没有行号时,用注释做二分查找:把 main 后半段整段注释掉,程序不崩了,说明 fatal 在后半段;恢复一半再试,几次二分即可锁定到行。配合 println 打点更精确:

fatal 定位:打点 + 二分
func main(...)
    println("checkpoint A")     // 打出来了吗?
    stepOne()
    println("checkpoint B")     // 到这就断了 → fatal 在 stepOne
    stepTwo()
    println("checkpoint C")
end

最后一个打印出来的 checkpoint 与下一个之间的代码就是案发区间。exit 3 查数组/容器下标,exit 4 查 as 转换与可能为 null 的对象——按码索骥比逐行读快得多。

技巧三:防御性断言。把"这里应该成立"的每一个假设写成 assert,错误就会在第一现场爆炸而不是污染到下游:

把 fatal 扼杀在摇篮
func average(array<int> xs) as float
    assert xs.length > 0, "average: empty array"
    int total = 0
    for x in xs
        total += x
    end
    return total / float(xs.length)
end

assert 失败是可捕获、带消息、带你写的现场描述的 AssertionError(exit 1)——比越界静默 exit 3 友好一百倍。入口断言参数、循环断言不变量、返回前断言结果范围,断言密度就是你的调试安全网密度。

技巧四:println 打点的两个现代注意。一,多线程/嵌入宿主场景别用 string() 显式转换做打点(第 38 章实测它返回空串),用 println 多参直打 println("v =", v);二,打点输出量大时 xl run app.xl 2>nul 可以临时屏蔽 uncaught 的 TCC 噪音只留 stdout。

39.10 调试器现状

必须如实相告:当前版本没有断点调试器。docs/命令行设计.md §6 的官方口径(原文照录):"首个发布版本不提供 dbg、dap 或独立 xldbg.exe。此前的 probe 与 DAP smoke 不能执行真实断点控制,也不能提供真实栈帧和变量,因此已从正式 CLI 删除,不能作为兼容命令或隐藏回退重新进入。"

删除而不是留个半成品,是对调试工具的基本要求——一个不能真正暂停、看栈、读变量的"调试器"比没有更害人。官方对回归的条件同样明确:调试器必须完成真实被调试进程、暂停/恢复、调用栈、作用域、变量快照和 DAP 映射后才可重新设计公开命令,且不得进入 xl.dll 运行版。

所以本章前九节就是你的"当前调试器全家桶":check --json 是静态分析器,管线取证是透视仪,assert 是断点,println/log 是手表,最小复现与二分法是手术刀。它们组合起来覆盖了断点调试器九成以上的日常用途。等官方调试器落地,这一章教的诊断分类与定位方法论依然是基本功——调试器只是让同样的认知有了更快的交互。

深入细节

深入细节 1:为什么 parse 报一次停、sem 能收集多个

两个阶段的错误恢复成本天差地别。parse 阶段的输入是线性 token 流,一个 token 对不上,后续 token 的解读全部失真——继续分析只会制造连锁垃圾诊断(想想少了个 end 之后每一行都"错"),所以 lemon 解析器选择第一次失败就停,把唯一可信的诊断交给你。sem 阶段的输入已经是结构完整的语法树,每个节点可以独立检查:第 2 行的初始化类型错了,不影响第 3 行的标识符解析——遍历继续,诊断攒齐。代价是级联:一个根因可能衍生多条诊断(39.4 的 5 条诊断 4 个问题),于是有了"先修第一条"的纪律。理解了这个机制,你就不会抱怨"为什么只报一个错"或"为什么报这么多"——两种策略都是当时当地的最优解。

深入细节 2:TCC warning 噪音的来历

uncaught 异常时总会伴生一条 tcc: __xlvfs__/module.c:449: warning: 'return' with no value——它不是你的代码的问题。异常穿透 main 时,生成的 C 代码沿异常路径返回而无返回值,TCC 对这个生成代码的形态照章提出警告。实测规律:正常退出无此警告,catch 住无此警告,只有 uncaught 时出现,且固定指向 __xlvfs__/module.c(动态 VFS 里的生成 C,不是你写的文件)。看到它只需明白一件事:它是 uncaught 的伴随症状,不是独立问题,真正的诊断永远在它下面那行 uncaught XxxError 里。顺手的事实:诊断全走 stderr(xlGetLastError 同款通道),stdout 只装程序自己的输出——管道分流时 1>out.txt 2>err.txt 可以把两者干净分离。

深入细节 3:退出码全景表

全书实测到的退出码汇总,按值分诊:

退出码含义实测出处
0正常(含 check 通过、脚本 catch 住异常)各章
1编译期诊断失败(check/parse/sem);uncaught 可捕获异常(Error/ZeroDivision/AssertionError…);CLI 参数错误(未知命令、-shared 缺 -o 等)第 10/37/39 章
2impdef 输入缺失等工具参数错误第 37 章
3fatal:数组/容器越界——静默无消息第 10/39 章
4fatal:as 转换失败、null 调方法——静默无消息第 10/34/39 章
N(脚本自定)main return N 透传(run/build/嵌入三路一致)第 37/38 章
127进程级:缺 DLL / 栈溢出崩溃 / xvalue 重复释放 / 编译器缺陷段错误第 29/37/38/39 章
139段错误(Linux 信号口径;Queue<var> 声明等编译器缺陷)第 29 章

用法:CI 里对 exit code 分类统计——1 是"代码要修",3/4 是"逻辑有 bug",127/139 是"环境或编译器问题",脚本自定义码(建议 ≥10 避开语言保留区间)是"业务结果"。这张表随版本演进可能扩充,以最新实测为准。

⚠️ 常见错误

⚠️ 坑 1:把 [parse] 语法错误当拼写问题逐字符找
e_lex.xl:2:16: [parse] 语法错误

原因:报错位置 2:16 是 parse 不认的那个 token 的位置,但 token 本身可能是合法词法单位——@ 被切成 AT token,词法毫无意见,语法层面 `10 @ 5` 不是合法表达式。逐字符盯拼写会漏掉结构问题。正确做法:xl lex 看 token 流确认词法无误后,检查该位置前后的语法结构(运算符是否成对、end 是否缺失、括号是否平衡)。

⚠️ 坑 2:从最后一条诊断开始修
e_multi.xl:5:15: [sem] 字符串拼接两侧都必须是 string
e_multi.xl:5:15: [sem] 初始化表达式类型和声明类型不匹配: target int, value string

原因:第二条是第一条的级联——拼接失败后类型推导塌陷,初始化自然不匹配。先修第二条无从下手(类型"看起来"没错)。正确做法:永远按行号从第一条修起,修一条重跑一次 check,级联诊断自动消失。

⚠️ 坑 3:指望 try/catch 兜越界和 as 失败
try
    println(string(arr[10]))
catch e
    println("caught")   // 永远不会执行
end
# exit 3 —— 静默

原因:越界(exit 3)、as 失败与 null 调用(exit 4)是 fatal 不是异常,运行时直接终止进程,catch 根本来不及参与。正确做法:事前防御——访问前查 length、as 前用 is 判断、调用前查 null;或用 assert 把 fatal 变成可诊断的 AssertionError。

⚠️ 坑 4:把 exit 127 当语言错误查语法
$ xl run app.xl
# 静默,exit 127

原因:127 是进程级问题——缺 DLL、栈溢出、xvalue 重复释放、编译器缺陷段错误,与 xlang 语法语义无关,翻代码查语法纯属南辕北辙。正确做法:按 39.6 的成因表对号:一行输出都没有→查 DLL;输出打到一半→查递归深度与所有权;特定构造必现→最小复现报编译器问题。

⚠️ 坑 5:多线程/嵌入宿主里用 string() 打点
    println("v = " + string(v))   // 多线程宿主下输出 "v = "(空串)

原因:第 38 章实测边界——多 context 并发时 string() 转换路径返回空串,打点信息全丢,还以为变量是空的。正确做法:打点用 println 多参直打 println("v =", v)(已实测正常);单线程 CLI 场景不受影响。

本章小结

主题要点
四层防线编译期(诊断最全)→ 链接期(查符号)→ 运行期(异常说话 fatal 沉默)→ 进程级(只剩退出码);越往下预防越重要
诊断格式file:line:col: [阶段] 消息;--json 三字段 ok/phase/diagnostics,phase 三态 checked/parse/sem;消息中英混合按结构解析
parse报一次停;@ 等"非法字符"是合法 token 由 parse 拒;ERROR token 承载未闭合字符串
sem收集多个错误;级联诊断先修第一条;类型标注让 sem 拦截运行期灾难
CIcheck 退出码 0/1 门禁;--json 按文件存档;嵌入侧对应 xlcCheck
运行期三形态uncaught XxxError 带行号(exit 1)/ fatal 越界 3、as 与 null 4 全静默 / 进程级 127 四种成因;TCC warning 是 uncaught 伴生噪音
异常定位uncaught 只报 throw 点无调用栈;补偿:分段 catch 补上下文、log 打点、防御性断言前移
取证法check 定阶段 → parse 看树 → ir 看语义 → dump-dir 六件套 → save-temps 拿 module.c;链接期问题答案在生成 C 里
技巧最小复现、二分注释+checkpoint 打点、assert 防御网、多线程 println 直打
调试器dbg/dap 已删除(不能真断点真栈帧);回归需完整调试能力;当前替代=本章前九节

复习题

  1. 四层防线各层的典型错误与诊断形态分别是什么?为什么越往下层越要靠预防?
  2. --json 诊断的 phase 字段有哪三个实测值?词法错误归在哪个阶段,为什么?
  3. parse 与 sem 的错误报告策略有何不同?面对 5 条 sem 诊断应该按什么顺序修,为什么?
  4. exit 1、3、4、127 各自对应什么错误形态?try/catch 能兜住哪些?
  5. uncaught 诊断的行号指向哪里、缺什么信息?写出三种补偿手段。当前版本为什么没有调试器?

编程练习

练习 1:诊断分诊台

故意制造六个错误文件:①含 @ 的表达式、②未闭合字符串、③int 赋字符串、④调用未声明函数、⑤extern 缺 --loadlib、⑥数组越界。对每个文件运行合适的命令收集真实诊断,填一张分诊表:阶段(parse/sem/链接/运行)、退出码、有无消息、第一嫌疑位置。最后用 xl lex 验证 ①② 的 token 流差异(AT vs ERROR)。

参考思路:39.3/39.4/39.6 的实测样本就是标准答案;⑥的"诊断"就是沉默本身——如实记录"无任何输出,exit 3"。

练习 2:级联错误大扫除

写一个含 5 处语义错误的程序(类型不匹配、未声明标识符、拼接两侧非 string、返回类型错误、未知异常分类 throw NotExistKind),xl check 收集全部诊断;然后严格按"先修第一条"纪律逐条修复,每修一条重跑 check 记录诊断数变化,画出收敛曲线;观察哪一条修复消除了最多的级联诊断。

参考思路:39.4 的 e_multi.xl 是模板;返回类型错误示例——声明 as int 却 return "text";收敛通常是超线性的。

练习 3:fatal 悬案侦破

写一个 30 行左右的程序,在某处埋一个数组越界(比如循环条件写成 i <= arr.length)。运行观察静默 exit 3;然后只用 checkpoint 打点法(println A/B/C/D 四个点)不读代码逻辑,把越界定位到函数级;再用一次二分把范围缩到 5 行内;最后在数组访问前加 assert i < arr.length 把 fatal 变成带消息的 AssertionError,验证 exit 码从 3 变 1 且能看到你写的现场描述。

参考思路:39.9 技巧二的完整演练;体会 assert 把"沉默的 fatal"变成"会说话的异常"的价值——这就是防御性断言的全部意义。

下一章第 40 章 编译管线原理(撰写中)