后台的节点下拉是一张 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:
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user