Files
OJ2/docs/ast-rules.md
T
xuyueandClaude Opus 5.5 55bdf317f1
Deploy / deploy (push) Successful in 55s
fix(语法要求): 「自定义函数」不再把 main 算进去,运算符不再把取地址、指针、模板尖括号当成用了
- C / C++ 的函数定义排掉 main,下拉改叫「自定义函数」。原来「必须使用」永远通过、「不能使用」永远失败,
  出题页挑战候选里 C 的「用自定义函数再写一遍」也因此一直挖不出来
- 运算符只认二元运算 / 复合赋值 / 自增自减里的 token,一元只认取反。scanf 的 &、int *p、vector<int>、
  *args、import *、**kw、not in、is not、case 1 | 2 不再算「用了」
- Python 的 math.sqrt(4) 算调用了 sqrt()(只认 import 进来的模块)
- Python 的「赋值语句」连 s += 1 一起算,和 C 一个口径
- 保存时拦下负数次数、至少比至多还多的次数规则

现有 19 道题的规则都不受影响:1265 条真实提交重判,结论和改之前一致。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 13:29:57 +08:00

69 lines
4.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AST 代码规则
规矩在 `CLAUDE.md`「AST 代码规则」一节。这里是加语言、加 target 时要一起看的细节。
## 为什么会有 `check:ast`
契约的 `AST_NODE_TARGETS_BY_LANGUAGE` 是**唯一**一张表,一个 target 一条
`{ label, node }`:`label` 给后台下拉和题目页,`node` 给判题机比 tree-sitter 节点类型。
运算符表 `AST_OPERATOR_TARGETS_BY_LANGUAGE` 一份两用(它的值既是文案又是要比的 token)。
判题机侧没有第二张表,解析统一走契约的 `astTargetNodeType()`,所以**加 target 而漏配节点
类型在结构上不可能**。
但**配错**仍然可能,而且完全静默:节点类型对不上就是一个都收不到,于是「必须使用 X」永远
失败、「不能使用 X」永远通过,两头不报错,只有学生受着。
```bash
bun run --filter '@oj2/api' check:ast # 每个 target 的 node 在语法里是否真实存在
```
**升级 `tree-sitter-*` 依赖之后一定要跑一次** —— 语法改节点名是常事,后果全静默。
加这个检查那天,56 个 target 里就抓出一个:`f_string` 一直配的是 `format_string`,
而这个版本的 tree-sitter-python 根本没有这种节点(f-string 是 `string` 里带
`interpolation`),所以「不能使用 f-string」从上线起就没生效过。
它只验节点类型**存在**,不验语义对不对(把 `while_loop` 配成 `for_statement` 这种两个都
存在,机器看不出来),语义那层还是得实跑。
## 光比节点类型不够的几处
下面几处都是光比类型就会静默判错的,判题机(`judge/ast.ts`)额外看了一眼上下文:
- **C / C++ 的函数定义不算 `main`**(`nodeMatch`)。每个 C 程序都有 main,算进去的话「必须使用」
永远通过、「不能使用」永远失败。所以 C / C++ 的下拉叫「自定义函数」。
- **Python 的「赋值语句」连 `s += 1` 一起算**(`nodeMatch`),和 C 一个口径 —— C 的
`assignment_expression` 本来就包括复合赋值,tree-sitter-python 却把它拆成了 `augmented_assignment`。
- **运算符只认真在做运算的 token**(`operatorMatch`),看父节点是不是二元运算 / 复合赋值 / 自增自减,
一元的只认取反。不看的话 `scanf("%d", &n)` 算用了 `&`、`int *p` 算 `*`、`vector<int>` 算 `>`、
`*args` 和 `from m import *` 算 `*`、`not in` / `is not` 算 `not`。负号 `-x` 不算用了减号。
- **Python 的 `math.sqrt(4)` 算调用了 `sqrt()`**(`functionCalls`),只认 `import` 进来的模块名;
对象上的 `s.count()` 仍归「调用方法」。
改这几处之后,拿真实提交回放一遍:对着挂了语法要求的题,把 AC 和「语法检查没通过」的提交重新
`checkAst`,看结论变了多少条、变的是不是该变的。
## 只有三种语言真的会跑
判题机只认 `AST_SUPPORTED_LANGUAGES`(C / C++ / Python)。别的语言配了规则一条都不会跑,
所以后台不给它们开 tab,题目页也不把它们的规则展示成「要求」——
**看得见却不检查**比没有更糟。
## C++ 不是「C 加几条」那么简单
C++ 的语法表是「C 的全集 + C++ 独有的几条」,因为 tree-sitter-cpp 继承 tree-sitter-c,
C 那 14 个 target 在 C++ 树里逐个实测通用。但**调用形态两者不同**,加语言时必须一起看:
- `a.push_back()` 和 `p->push_back()` 在 C++ 都是 `call_expression` + `field_expression`,
不是 Python 的 `attribute`;
- `std::sort(...)` 的 function 是 `qualified_identifier` 而不是 `identifier`,所以
`functionCalls` 对 C++ 额外比一次 `::` 末段 —— 否则学生写了 `using namespace std` 与否
会得到不同的判定结果。
## 规则的语义校验为什么不在 zod 上
在 `astRulesError()`,不在 `astRulesSchema` 的 refine 上:那个 schema 同时用于**读**后台
题目详情,在读路径上抛错会让历史脏数据把整个题目详情打不开(同 `docs/contract.md` 那套教训)。
同理,保存前先 `pickAstRules()` 剔除够不着的分组再校验,否则早年配过 C++ 规则的题会把老师
锁死 —— tab 里看不到那组规则,保存却被拦下。