
peg.js 中 `varname = [a-z0-9]+` 实际会错误匹配 `[`、`]` 等 ascii 符号,根源在于字符范围 `[a-z]` 并非等价于 `[a-za-z]`,而是包含 `z` 到 `a` 之间的所有字符(如 `[`, `\`, `]`, `^`, `_`, `` ` ``),需改用 `[a-za-z0-9]` 或更安全的 `[a-z0-9]i`。
在使用 PEG.js 构建自定义语言解析器时,一个看似微小的正则字符类写法错误,可能导致严重语义偏差——正如本例中 test["foobar"] 被错误解析为变量名 test[,进而触发 "Variable 'test[' does not exist." 的报错。
根本原因在于:[A-z] 不是合法的字母范围表示法。
ASCII 编码中,大写字母 A–Z 对应十进制 65–90,小写字母 a–z 对应 97–122;而 Z(90)与 a(97)之间存在 6 个非字母字符:[ \ ] ^ _(即 ASCII 91–96)。因此 [A-z] 实际等价于 [A-Z\[\]^_a-z],其中就包含了左方括号[和右方括号]。当解析器尝试匹配test["foobar"]时,Varname规则贪婪匹配test[(因为[属于[A-z0-9]范围),导致后续'["foobar"]'无法被Getvar` 的路径部分消费,最终失败。
✅ 正确写法(推荐):
使用 不区分大小写的字符类 [A-Z0-9]i,既简洁又语义清晰,且完全避免 ASCII 间隙问题:
Varname "variable name"
= [A-Z0-9]i+ {
const name = text();
if (!/[A-Za-z]/.test(name)) {
error(`Variable name must contain at least one letter. (reading '${name}')`);
}
return name;
}⚠️ 注意事项:
- i 修饰符使 [A-Z0-9] 同时匹配大小写字母和数字,等效于 [A-Za-z0-9],但更安全(无范围歧义);
- 原规则中 /[A-z]+/.test(text()) 同样受 [A-z] 陷阱影响,必须同步修正为 /[A-Za-z0-9]+/ 或 /^[A-Z0-9]+$/i;
- 在 Getvar 规则末尾添加 _(跳过结尾空白)可提升鲁棒性,防止因空格导致路径解析中断:
Getvar
= name:Varname path:('[' _ exp:(String / Integer) _ ']' { return exp; })* _ {
let rt = glob[name];
if (rt === undefined && name !== 'undefined' && name !== 'null') {
error(`Variable '${name}' does not exist.`);
}
for (const p of path) {
rt = rt?.[p]; // 可选链防 TypeError
}
return rt;
}? 总结:
PEG.js 的字符类 [X-Y] 严格按 ASCII 码值解释范围,绝不可写作 [A-z] 代替 [A-Za-z]。始终优先使用明确范围(如 [A-Za-z])或带 i 修饰符的规范写法(如 [A-Z0-9]i)。该原则适用于所有基于 ASCII 字符集的解析器,是编写可靠语法的基础防线。










