refactor(AST): 两张 target 表合成一张,加节点类型漏配在结构上不再可能
契约的 AST_NODE_TARGETS_BY_LANGUAGE 是 target → 中文名,judge/ast.ts 的 mappings 是
target → tree-sitter 节点类型,同一批键分在两个包里,靠一句「两边必须同增同减」的注释
维持。只加一边是静默错判:老师给 C 题选到只有 Python 有的 list_comprehension,判题机
拿裸名去比节点类型,C 的语法树里永远不存在它,于是「必须使用列表推导式」永远失败、
「不能使用 f-string」永远通过,两头都不报错,只有学生受着。
现在一个 target 一条 { label, node }:label 给后台下拉和题目页,node 给判题机。加 target
而漏配节点类型在结构上就不可能了。judge/ast.ts 的 mappings 整张删掉,解析统一走契约的
astTargetNodeType()(节点查 node,运算符查运算符表 —— 那张表的值本身就是要比的 token,
判题机原来抄的 and→&& 三条取值逐个相同,纯属重复)。
顺带把同一份数据的四份拷贝收成一份:C 的 14 条原来在契约和判题机里各抄了两遍
(C 一份、C++ 一份),现在 C++ 逐条引用 C_NODE_TARGETS;运算符表的 C++ 改成
{ ...C_OPERATOR_TARGETS, "<<", ">>" }。C++ 那几条仍逐条列出而不是 spread,是为了保住
下拉框的显示顺序(C++ 独有的几条插在中间)。
## 验证
行为零变化,是逐个 target 机械比对过的:把 HEAD 版的两张表原样取出来,对三种语言的
全部 target 比对「label / 运算符文案 / tree-sitter 解析结果 / 下拉框顺序」四项 ——
C 37 个、C++ 47 个、Python3 43 个,全部一致,键集与顺序也一致。
实跑:给题目 1004 配两条 Python3 规则(必须有 for 循环、不能用 f-string),交一发没有
for 循环的正确答案,判成 AST_CHECK_FAILED(10),statistic_info.ast_results 为
「必须使用 for 循环 / 不通过」「不能使用 f-string / 通过」—— label 与 node 两半都走到了。
再交一发带 for 循环的,判成 ACCEPTED(0)。
tsc、vue-tsc、vite build、单二进制编译均通过。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012j1vgeDqay8wKCh8dPgPcH
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
astNodeLabel,
|
||||
astOperatorLabel,
|
||||
astRuleIsMeaningful,
|
||||
astTargetNodeType,
|
||||
astRuleSchema,
|
||||
AST_SUPPORTED_LANGUAGES,
|
||||
type AstRequirement,
|
||||
@@ -31,91 +32,11 @@ export interface AstResult {
|
||||
actual?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* target → tree-sitter 节点类型。恒等的条目(`+`、`==` 这些运算符)不列,
|
||||
* 走 `mapping[target] ?? target` 回落。
|
||||
*
|
||||
* **这里的键集是契约 AST_NODE_TARGETS_BY_LANGUAGE 的另一半**,两边必须同增同减:
|
||||
* 那边决定后台下拉能选什么,这边决定判题机认得什么。只加一边就是静默错判。
|
||||
*/
|
||||
const mappings: Record<string, Record<string, string>> = {
|
||||
C: {
|
||||
for_loop: "for_statement",
|
||||
while_loop: "while_statement",
|
||||
do_while: "do_statement",
|
||||
if_statement: "if_statement",
|
||||
else_clause: "else_clause",
|
||||
break: "break_statement",
|
||||
continue: "continue_statement",
|
||||
function_definition: "function_definition",
|
||||
return: "return_statement",
|
||||
switch_statement: "switch_statement",
|
||||
case_statement: "case_statement",
|
||||
assignment: "assignment_expression",
|
||||
struct: "struct_specifier",
|
||||
include: "preproc_include",
|
||||
and: "&&",
|
||||
or: "||",
|
||||
not: "!",
|
||||
},
|
||||
"C++": {
|
||||
// C 的那 14 条原样通用(tree-sitter-cpp 继承 tree-sitter-c 的语法)
|
||||
for_loop: "for_statement",
|
||||
while_loop: "while_statement",
|
||||
do_while: "do_statement",
|
||||
if_statement: "if_statement",
|
||||
else_clause: "else_clause",
|
||||
break: "break_statement",
|
||||
continue: "continue_statement",
|
||||
function_definition: "function_definition",
|
||||
return: "return_statement",
|
||||
switch_statement: "switch_statement",
|
||||
case_statement: "case_statement",
|
||||
assignment: "assignment_expression",
|
||||
struct: "struct_specifier",
|
||||
include: "preproc_include",
|
||||
// C++ 独有
|
||||
range_for_loop: "for_range_loop",
|
||||
class_definition: "class_specifier",
|
||||
try_except: "try_statement",
|
||||
throw: "throw_statement",
|
||||
namespace: "namespace_definition",
|
||||
template: "template_declaration",
|
||||
lambda: "lambda_expression",
|
||||
using: "using_declaration",
|
||||
and: "&&",
|
||||
or: "||",
|
||||
not: "!",
|
||||
},
|
||||
Python3: {
|
||||
for_loop: "for_statement",
|
||||
while_loop: "while_statement",
|
||||
if_statement: "if_statement",
|
||||
else_clause: "else_clause",
|
||||
elif_clause: "elif_clause",
|
||||
break: "break_statement",
|
||||
continue: "continue_statement",
|
||||
function_definition: "function_definition",
|
||||
return: "return_statement",
|
||||
try_except: "try_statement",
|
||||
with_statement: "with_statement",
|
||||
list_comprehension: "list_comprehension",
|
||||
list_literal: "list",
|
||||
dict_literal: "dictionary",
|
||||
set_literal: "set",
|
||||
f_string: "format_string",
|
||||
import: "import_statement",
|
||||
import_from: "import_from_statement",
|
||||
assignment: "assignment",
|
||||
class_definition: "class_definition",
|
||||
},
|
||||
}
|
||||
|
||||
let initPromise: Promise<void> | undefined
|
||||
const languages = new Map<string, Language>()
|
||||
|
||||
async function loadLanguage(language: string) {
|
||||
if (!(language in mappings)) return null
|
||||
if (!AST_SUPPORTED_LANGUAGES.includes(language)) return null
|
||||
// locateFile 指到内嵌的 tree-sitter.wasm:emscripten 默认按脚本所在目录找,
|
||||
// 单二进制里那个目录是 /$bunfs/root,它自己找不着
|
||||
if (!initPromise) initPromise = Parser.init({ locateFile: () => treeSitterWasmPath })
|
||||
@@ -240,7 +161,7 @@ export function astRequirements(value: unknown): AstRequirements | null {
|
||||
/**
|
||||
* AST 规则的语义校验。zod 只管形状(engine 在枚举里、min 是整数),管不了
|
||||
* 「给 C 题选了只有 Python 才有的 list_comprehension」这类组合 —— 那种规则存得进去,
|
||||
* 判题时 `mapping[target] ?? target` 拿裸名去比节点类型,永远失败或永远通过,
|
||||
* 判题时 astTargetNodeType() 找不到就回落成裸名去比节点类型,永远失败或永远通过,
|
||||
* 两头都不报错,只有学生受着。
|
||||
*
|
||||
* 放这儿而不是 astRulesSchema 的 refine 上:那个 schema 同时用于**读**后台题目详情,
|
||||
@@ -345,14 +266,9 @@ function methodCalls(root: Node, target: string, language: string) {
|
||||
})
|
||||
}
|
||||
|
||||
function evaluateRule(
|
||||
root: Node,
|
||||
rule: AstRule,
|
||||
language: string,
|
||||
mapping: Record<string, string>,
|
||||
): AstResult | null {
|
||||
function evaluateRule(root: Node, rule: AstRule, language: string): AstResult | null {
|
||||
const target = rule.target ?? ""
|
||||
const nodeType = mapping[target] ?? target
|
||||
const nodeType = astTargetNodeType(target, language)
|
||||
|
||||
switch (rule.engine) {
|
||||
case "must_exist_node":
|
||||
@@ -409,8 +325,8 @@ function evaluateRule(
|
||||
case "must_have_nesting": {
|
||||
const outer = rule.outer ?? ""
|
||||
const inner = rule.inner ?? ""
|
||||
const outerType = mapping[outer] ?? outer
|
||||
const innerType = mapping[inner] ?? inner
|
||||
const outerType = astTargetNodeType(outer, language)
|
||||
const innerType = astTargetNodeType(inner, language)
|
||||
const passed = collectNodes(root, outerType).some((node) =>
|
||||
node.children.some((child) => hasNode(child, innerType)),
|
||||
)
|
||||
@@ -437,10 +353,9 @@ export async function checkAst(
|
||||
const tree = parser.parse(code)
|
||||
if (!tree) return { passed: true, results: [] }
|
||||
try {
|
||||
const mapping = mappings[language] ?? {}
|
||||
const results = rules
|
||||
.filter(astRuleIsMeaningful)
|
||||
.map((rule) => evaluateRule(tree.rootNode, rule, language, mapping))
|
||||
.map((rule) => evaluateRule(tree.rootNode, rule, language))
|
||||
.filter((result): result is AstResult => result !== null)
|
||||
return { passed: results.every((result) => result.passed), results }
|
||||
} finally {
|
||||
|
||||
@@ -71,7 +71,7 @@ const ENGINE_OPTIONS: SelectOption[] = [
|
||||
// (或者反过来,「不能使用 f-string」永远通过),两头都不报错。
|
||||
function nodeTargetOptions(lang: string): SelectOption[] {
|
||||
return Object.entries(AST_NODE_TARGETS_BY_LANGUAGE[lang] ?? {}).map(
|
||||
([value, label]) => ({ label, value }),
|
||||
([value, entry]) => ({ label: entry.label, value }),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ function getTargetLabel(
|
||||
engine: string,
|
||||
target: string,
|
||||
): string | undefined {
|
||||
if (isNodeEngine(engine)) return AST_NODE_TARGETS_BY_LANGUAGE[lang]?.[target]
|
||||
if (isNodeEngine(engine)) return AST_NODE_TARGETS_BY_LANGUAGE[lang]?.[target]?.label
|
||||
// 运算符不写 label:判题结果的文案按语言翻译(astOperatorLabel),
|
||||
// 存一个固定 label 反而会把 C 的 && 钉死成 and
|
||||
return undefined
|
||||
|
||||
Reference in New Issue
Block a user