fix(代码规则): 给 C 题配的规则一半是哑弹,C++ 题根本配不了
Some checks failed
Deploy / deploy (push) Has been cancelled

后台的节点下拉是一张 C/Python 混在一起的 15 条表,整份铺给每种语言。给 C 题选到
只有 Python 有的 list_comprehension、f-string,规则存得进去,判题时拿裸名去比节点
类型,C 的语法树里永远不存在它——「必须使用列表推导式」永远失败、「不能使用
f-string」永远通过,两头都不报错,只有学生受着。反过来 mappings 支持的 do_while、
switch、struct、include 在表里没有,编辑器根本选不到。

标签表改成按语言分组,和判题机 mappings 的键集逐条对齐;保存时校验规则与语言是否
匹配,不匹配给中文提示。C 的 target 从 8 个可用变成 14 个。

C++ 接上了 tree-sitter-cpp,386 道 C++ 题从此能配规则。它继承 tree-sitter-c 的语法,
C 那 14 个 target 实测全部通用,另加范围 for、类定义、try-catch、throw、namespace、
模板、lambda、using 共 22 个。调用形态和 C/Python 都不同,一并处理:a.push_back()
和 p->push_back() 是 call_expression + field_expression,不是 Python 的 attribute;
std::sort(...) 的 function 是 qualified_identifier 而不是 identifier,所以额外比一次
:: 末段,否则学生写没写 using namespace std 会得到不同判定。

一起收掉的几处:

- Java/Golang/JavaScript 配的规则一条都不会跑,题目页却照常把它们渲染成「要求」
  挂给学生看。现在后台不给这些语言开 tab,下发给学生的要求也按语言过滤。
- 「出现次数」不填数字存下来是一条恒真规则,描述还退化成光秃秃一个「for 循环」。
  切换引擎时给默认值,保存时拦下,读取时整条丢弃。
- 次数规则失败只说「if 条件 出现 2 次 ✗」,学生不知道自己写了几次,补上「当前 N 次」。
  旧栈的引擎其实算了这个数,但 checker 只取 describe,算完就扔。
- must_have_nesting 的文案没走标签表,学生看到的是「必须使用 for_loop 嵌套」。
- 运算符文案给的是逻辑名,C 题的学生看到「必须使用 and 运算符」,而 C 里写的是 &&。

语义校验放在 astRulesError() 而不是 zod 的 refine 上:astRulesSchema 同时用于读后台
题目详情,在读路径上抛错会让历史脏数据把整个题目详情打不开。保存前先 pickAstRules()
剔除够不着的分组再校验,否则早年配过 C++ 规则的题会把老师锁死——tab 里看不到那组
规则,保存却被拦下。

生产库那 17 条规则(全是 Python3 的 must_exist_node / count_node)行为不变,逐条实跑
核对过。C++ 的 22 个节点 target、25 个运算符也逐个跑了,没有恒假的哑弹。改了带 wasm
内嵌的 ast.ts,dev 和编译两种形态都验过。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-27 11:53:37 -06:00
parent 280443c892
commit f38444c97a
9 changed files with 444 additions and 94 deletions

View File

@@ -99,26 +99,156 @@ export const astRuleSchema = z.object({
export const astRulesSchema = z.record(z.string(), z.array(astRuleSchema))
/**
* 节点类型的中文名。后台编辑器的下拉选项、后端生成要求文案都要用它 ——
* 原来在 AstRulesEditor.vue下拉 options和 ProblemContent.vue
* NODE_TARGET_LABELS各存了一份同样的 15 条
* 每种语言支持哪些节点 target以及它的中文名。原来这里是一张 15 条的混合表,
* C 和 Python 的节点混在一起铺成后台下拉(更早之前 AstRulesEditor.vue 和
* ProblemContent.vue 还各手抄了一份)
*
* **键必须和 judge/ast.ts 的 mappings 逐一对齐** —— 那边是 target → tree-sitter
* 节点类型,这边是 target → 中文名。少一边就是静默故障:老师给 C 题选到只有
* Python 有的 `list_comprehension`,判题机 `mapping[target] ?? target` 拿裸名去比
* 节点类型C 的语法树里永远不存在它,于是「必须使用列表推导式」永远失败、
* 「不能使用 f-string」永远通过两头都不报错。
*/
export const AST_NODE_TARGET_LABELS: Record<string, string> = {
for_loop: "for 循环",
while_loop: "while 循环",
if_statement: "if 条件",
else_clause: "else 子句",
function_definition: "函数定义",
return: "return 语句",
break: "break 语句",
continue: "continue 语句",
list_comprehension: "列表推导式",
list_literal: "列表",
dict_literal: "字典",
set_literal: "集合",
f_string: "f-string",
try_except: "try-except",
class_definition: "类定义",
export const AST_NODE_TARGETS_BY_LANGUAGE: Record<string, Record<string, string>> = {
C: {
for_loop: "for 循环",
while_loop: "while 循环",
do_while: "do-while 循环",
if_statement: "if 条件",
else_clause: "else 子句",
switch_statement: "switch 语句",
case_statement: "case 分支",
break: "break 语句",
continue: "continue 语句",
return: "return 语句",
function_definition: "函数定义",
assignment: "赋值语句",
struct: "结构体",
include: "#include 指令",
},
// tree-sitter-cpp 继承 tree-sitter-c 的语法C 那 14 条 target 在 C++ 树里
// 逐个实测通用所以这张表是「C 的全集 + C++ 独有的几条」
"C++": {
for_loop: "for 循环",
range_for_loop: "范围 for 循环",
while_loop: "while 循环",
do_while: "do-while 循环",
if_statement: "if 条件",
else_clause: "else 子句",
switch_statement: "switch 语句",
case_statement: "case 分支",
break: "break 语句",
continue: "continue 语句",
return: "return 语句",
function_definition: "函数定义",
class_definition: "类定义",
struct: "结构体",
assignment: "赋值语句",
include: "#include 指令",
try_except: "try-catch",
throw: "throw 语句",
namespace: "namespace 定义",
template: "模板定义",
lambda: "lambda 表达式",
using: "using 声明",
},
Python3: {
for_loop: "for 循环",
while_loop: "while 循环",
if_statement: "if 条件",
elif_clause: "elif 子句",
else_clause: "else 子句",
break: "break 语句",
continue: "continue 语句",
return: "return 语句",
function_definition: "函数定义",
class_definition: "类定义",
assignment: "赋值语句",
try_except: "try-except",
with_statement: "with 语句",
import: "import 语句",
import_from: "from-import 语句",
list_comprehension: "列表推导式",
list_literal: "列表",
dict_literal: "字典",
set_literal: "集合",
f_string: "f-string",
},
}
/**
* 运算符 target → 该语言里的实际写法。逻辑名 `and` / `or` / `not` 在 C 里写作
* `&&` / `||` / `!`,判题机按 mappings 翻译,文案这边也得翻 —— 否则 C 题的学生
* 看到的要求是「必须使用 and 运算符」,而 C 里根本没有 `and` 这个词。
*
* 恒等的那些条目(`+`、`==` …)判题机的 mappings 里已经删掉了,走 `?? target`
* 回落到同一个值;这里保留完整列表是因为它同时是后台下拉的选项来源。
*/
export const AST_OPERATOR_TARGETS_BY_LANGUAGE: Record<string, Record<string, string>> = {
C: {
"+": "+", "-": "-", "*": "*", "/": "/", "%": "%",
"+=": "+=", "-=": "-=", "*=": "*=", "/=": "/=", "%=": "%=",
"++": "++", "--": "--",
"==": "==", "!=": "!=", ">": ">", ">=": ">=", "<": "<", "<=": "<=",
and: "&&", or: "||", not: "!",
"&": "&", "|": "|",
},
// `<<` / `>>` 对 C++ 主要是 cout/cin 的流运算符(位移是同一个 token
"C++": {
"+": "+", "-": "-", "*": "*", "/": "/", "%": "%",
"+=": "+=", "-=": "-=", "*=": "*=", "/=": "/=", "%=": "%=",
"++": "++", "--": "--",
"==": "==", "!=": "!=", ">": ">", ">=": ">=", "<": "<", "<=": "<=",
and: "&&", or: "||", not: "!",
"&": "&", "|": "|", "<<": "<<", ">>": ">>",
},
Python3: {
"+": "+", "-": "-", "*": "*", "/": "/", "//": "//", "%": "%", "**": "**",
"+=": "+=", "-=": "-=", "*=": "*=", "/=": "/=", "%=": "%=",
"==": "==", "!=": "!=", ">": ">", ">=": ">=", "<": "<", "<=": "<=",
and: "and", or: "or", not: "not",
"&": "&", "|": "|",
},
}
/**
* 判题机真正能做 AST 检查的语言。设计文档写的是"支持全部 6 种语言",落地的只有
* 这两种 —— 其余语言 judge/ast.ts 的 loadLanguage 返回 null规则一条都不会跑。
* 所以后台不给别的语言开 tab题目页也不把它们的规则展示成「要求」。
*/
export const AST_SUPPORTED_LANGUAGES = Object.keys(AST_NODE_TARGETS_BY_LANGUAGE)
/** 全语言的节点中文名并集,只给拿不到语言的场合做回落。有语言一律走 astNodeLabel() */
export const AST_NODE_TARGET_LABELS: Record<string, string> = Object.assign(
{},
...Object.values(AST_NODE_TARGETS_BY_LANGUAGE),
)
export function astNodeLabel(target: string, language?: string): string {
const table = language ? AST_NODE_TARGETS_BY_LANGUAGE[language] : undefined
return table?.[target] ?? AST_NODE_TARGET_LABELS[target] ?? target
}
export function astOperatorLabel(target: string, language?: string): string {
return (language ? AST_OPERATOR_TARGETS_BY_LANGUAGE[language]?.[target] : undefined) ?? target
}
/**
* count_* 引擎至少要有一个数字约束,否则这条规则恒真 —— rangePassed 三个字段全
* undefined 直接返回 true描述也退化成光秃秃一个「for 循环」。编辑器切到
* 「出现次数」时会清掉 exact/min/max老师不填数字就会存出这种规则。
*
* 读取路径(判题、题目页要求)用它把这种规则整条丢掉,而不是让 schema 校验失败:
* astRulesSchema 同时用于**读**后台题目详情,在那儿抛错会让整个详情打不开。
*/
export function astRuleIsMeaningful(rule: {
engine: string
exact?: number
min?: number
max?: number
}): boolean {
if (!rule.engine.startsWith("count")) return true
return rule.exact !== undefined || rule.min !== undefined || rule.max !== undefined
}
/**