后台的节点下拉是一张 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:
27
CLAUDE.md
27
CLAUDE.md
@@ -102,6 +102,33 @@ dev 直接起不来。
|
||||
这些整数是**落库的值**:12 万条历史提交的 `submission.result` 就是它们,判题沙箱回的也是
|
||||
这套编码,所以只能新增、不能改已有的含义。题目表情 reaction 的语义 key 同理。
|
||||
|
||||
### AST 代码规则有两张表,必须同增同减
|
||||
|
||||
契约的 `AST_NODE_TARGETS_BY_LANGUAGE`(target → 中文名)决定后台下拉能选什么,
|
||||
`apps/api/src/judge/ast.ts` 的 `mappings`(target → tree-sitter 节点类型)决定判题机
|
||||
认得什么。**加节点类型时两边都要加**,运算符表 `AST_OPERATOR_TARGETS_BY_LANGUAGE` 同理。
|
||||
|
||||
只加一边是**静默错判**:判题机 `mapping[target] ?? target` 拿裸名去比节点类型,
|
||||
C 的语法树里永远不存在 `list_comprehension`,于是「必须使用列表推导式」永远失败、
|
||||
「不能使用 f-string」永远通过,两头都不报错,只有学生受着。原来那张表是 C/Python
|
||||
混在一起的 15 条,整份铺成下拉,给 C 题也能选到 Python 专有节点——就是这么来的。
|
||||
|
||||
判题机只认 `AST_SUPPORTED_LANGUAGES` 里的语言(C / C++ / Python3)。别的语言配了规则
|
||||
一条都不会跑,所以后台不给它们开 tab,题目页也不把它们的规则展示成「要求」——
|
||||
**看得见却不检查**比没有更糟。
|
||||
|
||||
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` 与否会得到不同的判定结果。
|
||||
|
||||
规则的语义校验在 `astRulesError()`,不在 zod 的 refine 上:`astRulesSchema` 同时用于
|
||||
**读**后台题目详情,在读路径上抛错会让历史脏数据把整个题目详情打不开。同理,保存前
|
||||
先 `pickAstRules()` 剔除够不着的分组再校验,否则早年配过 C++ 规则的题会把老师锁死
|
||||
——tab 里看不到那组规则,保存却被拦下。
|
||||
|
||||
### 比赛只有 ACM 模式
|
||||
|
||||
没有 OI。上一代残留的 OI 分支在阶段 0 已经砍掉,不要"顺手补回来"。
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"postgres": "^3.4.9",
|
||||
"sql.js": "^1.14.2",
|
||||
"tree-sitter-c": "^0.24.1",
|
||||
"tree-sitter-cpp": "^0.23.4",
|
||||
"tree-sitter-python": "^0.25.0",
|
||||
"web-tree-sitter": "^0.26.13",
|
||||
"zod": "^4.4.3"
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import {
|
||||
AST_NODE_TARGETS_BY_LANGUAGE,
|
||||
AST_OPERATOR_TARGETS_BY_LANGUAGE,
|
||||
astNodeLabel,
|
||||
astOperatorLabel,
|
||||
astRuleIsMeaningful,
|
||||
astRuleSchema,
|
||||
AST_NODE_TARGET_LABELS,
|
||||
AST_SUPPORTED_LANGUAGES,
|
||||
type AstRequirement,
|
||||
type AstRequirements,
|
||||
type AstRule,
|
||||
type AstRules,
|
||||
} from "@oj2/contract"
|
||||
import { Language, Parser, type Node } from "web-tree-sitter"
|
||||
// 语法 wasm 内嵌成资源。原来是 `Bun.resolveSync(pkg + "/" + name, import.meta.dir)`,
|
||||
// 编译成单二进制后 import.meta.dir 是 /$bunfs/root,解析不到 node_modules。见 vendor/jieba.ts
|
||||
import cWasmPath from "tree-sitter-c/tree-sitter-c.wasm" with { type: "file" }
|
||||
import cppWasmPath from "tree-sitter-cpp/tree-sitter-cpp.wasm" with { type: "file" }
|
||||
import pythonWasmPath from "tree-sitter-python/tree-sitter-python.wasm" with { type: "file" }
|
||||
// web-tree-sitter 自己的运行时 wasm,Parser.init() 要用
|
||||
import treeSitterWasmPath from "web-tree-sitter/web-tree-sitter.wasm" with { type: "file" }
|
||||
@@ -20,8 +27,17 @@ export type { AstRule } from "@oj2/contract"
|
||||
export interface AstResult {
|
||||
description: string
|
||||
passed: boolean
|
||||
/** count_* 引擎实际数到的次数。失败时前端拿它补一句「当前 N 次」 */
|
||||
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",
|
||||
@@ -42,6 +58,35 @@ const mappings: Record<string, Record<string, string>> = {
|
||||
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",
|
||||
@@ -79,7 +124,12 @@ async function loadLanguage(language: string) {
|
||||
const cached = languages.get(language)
|
||||
if (cached) return cached
|
||||
|
||||
const loaded = await Language.load(language === "C" ? cWasmPath : pythonWasmPath)
|
||||
const wasmPath = language === "C"
|
||||
? cWasmPath
|
||||
: language === "C++"
|
||||
? cppWasmPath
|
||||
: pythonWasmPath
|
||||
const loaded = await Language.load(wasmPath)
|
||||
languages.set(language, loaded)
|
||||
return loaded
|
||||
}
|
||||
@@ -95,9 +145,9 @@ function hasNode(root: Node, type: string): boolean {
|
||||
return root.children.some((child) => hasNode(child, type))
|
||||
}
|
||||
|
||||
function targetName(rule: AstRule) {
|
||||
function targetName(rule: AstRule, language?: string) {
|
||||
const target = rule.target ?? ""
|
||||
return rule.label || AST_NODE_TARGET_LABELS[target] || target || "指定语法"
|
||||
return rule.label || astNodeLabel(target, language) || "指定语法"
|
||||
}
|
||||
|
||||
function countPhrase(verb: string, rule: AstRule) {
|
||||
@@ -114,9 +164,9 @@ function countPhrase(verb: string, rule: AstRule) {
|
||||
* 用的是同一份 —— 原来前端 ProblemContent.vue 里另有一份几乎一样的实现,
|
||||
* 只有 min/max 同时给出时的措辞不一样(生产库里没有这种规则)。
|
||||
*/
|
||||
export function describeAstRule(rule: AstRule): string {
|
||||
export function describeAstRule(rule: AstRule, language?: string): string {
|
||||
if (rule.message) return rule.message
|
||||
const name = targetName(rule)
|
||||
const name = targetName(rule, language)
|
||||
const target = rule.target ?? ""
|
||||
switch (rule.engine) {
|
||||
case "must_exist_node":
|
||||
@@ -136,10 +186,12 @@ export function describeAstRule(rule: AstRule): string {
|
||||
case "must_not_call_method":
|
||||
return `不能调用 .${target}()`
|
||||
case "must_use_operator":
|
||||
return `必须使用 ${target} 运算符`
|
||||
return `必须使用 ${astOperatorLabel(target, language)} 运算符`
|
||||
case "must_have_nesting": {
|
||||
const outer = rule.outer ?? ""
|
||||
const inner = rule.inner ?? ""
|
||||
// 这两个走 astNodeLabel 而不是裸值 —— 少了这一步文案就是
|
||||
// 「必须使用 for_loop 嵌套」,旧栈 ast_checker/engines/nesting.py 是翻的
|
||||
const outer = astNodeLabel(rule.outer ?? "", language)
|
||||
const inner = astNodeLabel(rule.inner ?? "", language)
|
||||
return outer === inner
|
||||
? `必须使用 ${outer} 嵌套`
|
||||
: `必须在 ${outer} 中嵌套使用 ${inner}`
|
||||
@@ -157,6 +209,10 @@ function requirementKind(engine: AstRule["engine"]): AstRequirement["kind"] {
|
||||
/**
|
||||
* 把规则原文投影成下发给学生的「代码要求」。规则里的 engine / target 不出现在
|
||||
* 响应里 —— 阶段 3 泄露评审收掉 ast_rules 时要的就是这个,见契约的注释。
|
||||
*
|
||||
* 只投影判题机真检查得了的语言。原来这里不看语言,给 C++ 题配的规则照样渲染成
|
||||
* 「必须使用 for 循环」挂在题目页上,而 loadLanguage 对 C++ 返回 null、
|
||||
* checkAst 直接放行 —— 学生看得见要求,判题从不检查。
|
||||
*/
|
||||
export function astRequirements(value: unknown): AstRequirements | null {
|
||||
const grouped = value && typeof value === "object" && !Array.isArray(value)
|
||||
@@ -166,11 +222,13 @@ export function astRequirements(value: unknown): AstRequirements | null {
|
||||
const out: AstRequirements = {}
|
||||
for (const [language, rules] of Object.entries(grouped)) {
|
||||
if (!Array.isArray(rules)) continue
|
||||
if (!AST_SUPPORTED_LANGUAGES.includes(language)) continue
|
||||
const items = rules.flatMap((rule) => {
|
||||
const parsed = astRuleSchema.safeParse(rule)
|
||||
if (!parsed.success) return []
|
||||
if (!astRuleIsMeaningful(parsed.data)) return []
|
||||
return [{
|
||||
description: describeAstRule(parsed.data),
|
||||
description: describeAstRule(parsed.data, language),
|
||||
kind: requirementKind(parsed.data.engine),
|
||||
}]
|
||||
})
|
||||
@@ -179,6 +237,63 @@ export function astRequirements(value: unknown): AstRequirements | null {
|
||||
return Object.keys(out).length > 0 ? out : null
|
||||
}
|
||||
|
||||
/**
|
||||
* AST 规则的语义校验。zod 只管形状(engine 在枚举里、min 是整数),管不了
|
||||
* 「给 C 题选了只有 Python 才有的 list_comprehension」这类组合 —— 那种规则存得进去,
|
||||
* 判题时 `mapping[target] ?? target` 拿裸名去比节点类型,永远失败或永远通过,
|
||||
* 两头都不报错,只有学生受着。
|
||||
*
|
||||
* 放这儿而不是 astRulesSchema 的 refine 上:那个 schema 同时用于**读**后台题目详情,
|
||||
* 在读路径上抛错会让历史脏数据直接把题目详情打不开。
|
||||
*/
|
||||
export function astRulesError(astRules: AstRules | null): string | null {
|
||||
if (!astRules) return null
|
||||
for (const [language, rules] of Object.entries(astRules)) {
|
||||
if (rules.length === 0) continue
|
||||
if (!AST_SUPPORTED_LANGUAGES.includes(language)) {
|
||||
return `代码规则暂不支持 ${language},判题机只检查 ${AST_SUPPORTED_LANGUAGES.join(" / ")}`
|
||||
}
|
||||
const nodes = AST_NODE_TARGETS_BY_LANGUAGE[language] ?? {}
|
||||
const operators = AST_OPERATOR_TARGETS_BY_LANGUAGE[language] ?? {}
|
||||
for (const [index, rule] of rules.entries()) {
|
||||
const at = `代码规则 ${language} 第 ${index + 1} 条`
|
||||
const target = rule.target ?? ""
|
||||
if (rule.engine.endsWith("_node")) {
|
||||
if (!(target in nodes)) return `${at}:${language} 没有「${target}」这种语法`
|
||||
} else if (rule.engine === "must_use_operator") {
|
||||
if (!(target in operators)) return `${at}:${language} 没有「${target}」运算符`
|
||||
} else if (rule.engine === "must_have_nesting") {
|
||||
for (const value of [rule.outer ?? "", rule.inner ?? ""]) {
|
||||
if (!(value in nodes)) return `${at}:${language} 没有「${value}」这种语法`
|
||||
}
|
||||
} else if (!target.trim()) {
|
||||
return `${at}:要检查的函数名/方法名不能为空`
|
||||
}
|
||||
if (!astRuleIsMeaningful(rule)) return `${at}:次数检查至少要填一个数字`
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前清掉够不着的规则分组:不在题目 languages 里的(老师改过语言列表),
|
||||
* 以及判题机检查不了的(C++ / Java / …)。两者编辑器都不给开 tab,留着就是死数据。
|
||||
*
|
||||
* 必须先剔除再校验,否则历史脏数据会把老师锁死:一道 languages 含 C++ 的题,
|
||||
* 早年配过 C++ 规则,如今 tab 里看不到那组规则,保存却被「暂不支持 C++」拦下,
|
||||
* 老师在界面上无从修改。
|
||||
*/
|
||||
export function pickAstRules(astRules: AstRules | null, languages: string[]): AstRules | null {
|
||||
if (!astRules) return null
|
||||
const out: AstRules = {}
|
||||
for (const [language, rules] of Object.entries(astRules)) {
|
||||
if (!languages.includes(language)) continue
|
||||
if (!AST_SUPPORTED_LANGUAGES.includes(language)) continue
|
||||
if (rules.length > 0) out[language] = rules
|
||||
}
|
||||
return Object.keys(out).length > 0 ? out : null
|
||||
}
|
||||
|
||||
function rangePassed(count: number, rule: AstRule) {
|
||||
if (rule.exact !== undefined && count !== rule.exact) return false
|
||||
if (rule.min !== undefined && count < rule.min) return false
|
||||
@@ -186,16 +301,41 @@ function rangePassed(count: number, rule: AstRule) {
|
||||
return true
|
||||
}
|
||||
|
||||
const CALL_NODE_TYPES: Record<string, string> = {
|
||||
C: "call_expression",
|
||||
"C++": "call_expression",
|
||||
Python3: "call",
|
||||
}
|
||||
|
||||
function functionCalls(root: Node, target: string, language: string) {
|
||||
const callType = language === "C" ? "call_expression" : "call"
|
||||
const callType = CALL_NODE_TYPES[language] ?? "call"
|
||||
return collectNodes(root, callType).filter((call) => {
|
||||
const fn = call.childForFieldName("function")
|
||||
return fn?.type === "identifier" && fn.text === target
|
||||
if (!fn) return false
|
||||
if (fn.type === "identifier") return fn.text === target
|
||||
// `std::sort(...)` 是 qualified_identifier。学生写 sort 还是 std::sort 取决于
|
||||
// 有没有 using namespace std,两种都得认,所以末段也比一次
|
||||
if (language === "C++" && fn.type === "qualified_identifier") {
|
||||
return fn.text === target || fn.text.split("::").pop() === target
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
function methodCalls(root: Node, target: string, language: string) {
|
||||
if (language === "C") return []
|
||||
// C++ 的 `a.push_back()` / `p->push_back()` 都是 call_expression + field_expression,
|
||||
// 和 Python 的 attribute 不是一回事 —— 少了这个分支,C++ 的「必须调用 .push_back()」
|
||||
// 会静默地永远失败
|
||||
if (language === "C++") {
|
||||
return collectNodes(root, "call_expression").filter((call) => {
|
||||
const fn = call.childForFieldName("function")
|
||||
return (
|
||||
fn?.type === "field_expression" &&
|
||||
fn.childForFieldName("field")?.text === target
|
||||
)
|
||||
})
|
||||
}
|
||||
if (language !== "Python3") return []
|
||||
return collectNodes(root, "call").filter((call) => {
|
||||
const fn = call.childForFieldName("function")
|
||||
return (
|
||||
@@ -217,51 +357,53 @@ function evaluateRule(
|
||||
switch (rule.engine) {
|
||||
case "must_exist_node":
|
||||
return {
|
||||
description: describeAstRule(rule),
|
||||
description: describeAstRule(rule, language),
|
||||
passed: hasNode(root, nodeType),
|
||||
}
|
||||
case "must_not_exist_node":
|
||||
return {
|
||||
description: describeAstRule(rule),
|
||||
description: describeAstRule(rule, language),
|
||||
passed: !hasNode(root, nodeType),
|
||||
}
|
||||
case "count_node": {
|
||||
const count = collectNodes(root, nodeType).length
|
||||
return {
|
||||
description: describeAstRule(rule),
|
||||
description: describeAstRule(rule, language),
|
||||
passed: rangePassed(count, rule),
|
||||
actual: count,
|
||||
}
|
||||
}
|
||||
case "must_call_function":
|
||||
return {
|
||||
description: describeAstRule(rule),
|
||||
description: describeAstRule(rule, language),
|
||||
passed: functionCalls(root, target, language).length > 0,
|
||||
}
|
||||
case "must_not_call_function":
|
||||
return {
|
||||
description: describeAstRule(rule),
|
||||
description: describeAstRule(rule, language),
|
||||
passed: functionCalls(root, target, language).length === 0,
|
||||
}
|
||||
case "count_function_call": {
|
||||
const count = functionCalls(root, target, language).length
|
||||
return {
|
||||
description: describeAstRule(rule),
|
||||
description: describeAstRule(rule, language),
|
||||
passed: rangePassed(count, rule),
|
||||
actual: count,
|
||||
}
|
||||
}
|
||||
case "must_call_method":
|
||||
return {
|
||||
description: describeAstRule(rule),
|
||||
description: describeAstRule(rule, language),
|
||||
passed: methodCalls(root, target, language).length > 0,
|
||||
}
|
||||
case "must_not_call_method":
|
||||
return {
|
||||
description: describeAstRule(rule),
|
||||
description: describeAstRule(rule, language),
|
||||
passed: methodCalls(root, target, language).length === 0,
|
||||
}
|
||||
case "must_use_operator":
|
||||
return {
|
||||
description: describeAstRule(rule),
|
||||
description: describeAstRule(rule, language),
|
||||
passed: hasNode(root, nodeType),
|
||||
}
|
||||
case "must_have_nesting": {
|
||||
@@ -272,7 +414,7 @@ function evaluateRule(
|
||||
const passed = collectNodes(root, outerType).some((node) =>
|
||||
node.children.some((child) => hasNode(child, innerType)),
|
||||
)
|
||||
return { description: describeAstRule(rule), passed }
|
||||
return { description: describeAstRule(rule, language), passed }
|
||||
}
|
||||
default:
|
||||
return null
|
||||
@@ -297,6 +439,7 @@ export async function checkAst(
|
||||
try {
|
||||
const mapping = mappings[language] ?? {}
|
||||
const results = rules
|
||||
.filter(astRuleIsMeaningful)
|
||||
.map((rule) => evaluateRule(tree.rootNode, rule, language, mapping))
|
||||
.filter((result): result is AstResult => result !== null)
|
||||
return { passed: results.every((result) => result.passed), results }
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
sqlPreviewRequestSchema,
|
||||
sqlTestCaseScriptSchema,
|
||||
uploadTestCaseResponseSchema,
|
||||
type AstRules,
|
||||
type SqlConfig,
|
||||
} from "@oj2/contract"
|
||||
import { and, count, desc, eq, ilike, inArray, isNull, ne, or, sql } from "drizzle-orm"
|
||||
@@ -20,6 +21,7 @@ import { requireProblemPermission, type AppEnv } from "../../auth/middleware"
|
||||
import type { AuthUser } from "../../auth/session"
|
||||
import { db, schema } from "../../db"
|
||||
import { failure, success } from "../../http"
|
||||
import { astRulesError, pickAstRules } from "../../judge/ast"
|
||||
import { buildSqlDisplay } from "../../judge/sql"
|
||||
import { completeChat } from "../../services/ai"
|
||||
import { contestStatus } from "../../services/contest"
|
||||
@@ -176,7 +178,10 @@ function commonChecks(data: {
|
||||
samples: unknown[]
|
||||
sqlConfig: SqlConfig | null
|
||||
answers: Record<string, unknown>[]
|
||||
astRules: AstRules | null
|
||||
}): { error: string } | { sql: boolean } {
|
||||
const astError = astRulesError(pickAstRules(data.astRules, data.languages))
|
||||
if (astError) return { error: astError }
|
||||
if (data.languages.includes("SQL")) {
|
||||
if (data.languages.length !== 1) return { error: "SQL problem cannot be mixed with other languages" }
|
||||
if (!data.sqlConfig) return { error: "SQL problem requires sql_config" }
|
||||
@@ -247,7 +252,7 @@ function problemValues(data: ReturnType<typeof createProblemRequestSchema.parse>
|
||||
showFlowchart: data.showFlowchart,
|
||||
mermaidCode: data.mermaidCode,
|
||||
flowchartHint: data.flowchartHint,
|
||||
astRules: data.astRules ?? null,
|
||||
astRules: pickAstRules(data.astRules, data.languages),
|
||||
answers: data.answers,
|
||||
prompt: data.prompt,
|
||||
// 防脏数据:非 SQL 题不应携带 SQL 配置,对齐旧 common_checks
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { AST_NODE_TARGET_LABELS } from "@oj2/contract"
|
||||
import {
|
||||
AST_NODE_TARGETS_BY_LANGUAGE,
|
||||
AST_OPERATOR_TARGETS_BY_LANGUAGE,
|
||||
AST_SUPPORTED_LANGUAGES,
|
||||
} from "@oj2/contract"
|
||||
import type { AstRule, AstRules, LANGUAGE } from "utils/types"
|
||||
|
||||
interface Props {
|
||||
@@ -12,7 +16,17 @@ const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: AstRules | null): void
|
||||
}>()
|
||||
|
||||
const activeTab = ref(props.languages[0] || "Python3")
|
||||
// 判题机只认 C / Python3,别的语言配了规则也一条都不会跑(judge/ast.ts 的
|
||||
// loadLanguage 返回 null 就直接放行)。原来这里按题目的全部语言开 tab,老师给
|
||||
// C++ 配的规则存得下、题目页也照常显示成「要求」,判题却从不检查。
|
||||
const supportedLanguages = computed(() =>
|
||||
props.languages.filter((lang) => AST_SUPPORTED_LANGUAGES.includes(lang)),
|
||||
)
|
||||
const unsupportedLanguages = computed(() =>
|
||||
props.languages.filter((lang) => !AST_SUPPORTED_LANGUAGES.includes(lang)),
|
||||
)
|
||||
|
||||
const activeTab = ref(supportedLanguages.value[0] || "Python3")
|
||||
|
||||
const ENGINE_OPTIONS: SelectOption[] = [
|
||||
{
|
||||
@@ -52,32 +66,21 @@ const ENGINE_OPTIONS: SelectOption[] = [
|
||||
},
|
||||
]
|
||||
|
||||
// 选项从契约的 AST_NODE_TARGET_LABELS 生成 —— 这 15 条原来在这里和
|
||||
// ProblemContent.vue 各手抄一份
|
||||
const NODE_TARGET_OPTIONS: SelectOption[] = Object.entries(
|
||||
AST_NODE_TARGET_LABELS,
|
||||
).map(([value, label]) => ({ label, value }))
|
||||
// 选项按语言生成。原来是一张 C/Python 混合的 15 条表整份铺开,给 C 题也能选到
|
||||
// 列表推导式、f-string 这些 C 根本没有的东西 —— 存得进去,判题时永远失败
|
||||
// (或者反过来,「不能使用 f-string」永远通过),两头都不报错。
|
||||
function nodeTargetOptions(lang: string): SelectOption[] {
|
||||
return Object.entries(AST_NODE_TARGETS_BY_LANGUAGE[lang] ?? {}).map(
|
||||
([value, label]) => ({ label, value }),
|
||||
)
|
||||
}
|
||||
|
||||
const OPERATOR_TARGET_OPTIONS: SelectOption[] = [
|
||||
{ label: "+", value: "+" },
|
||||
{ label: "-", value: "-" },
|
||||
{ label: "*", value: "*" },
|
||||
{ label: "/", value: "/" },
|
||||
{ label: "//", value: "//" },
|
||||
{ label: "%", value: "%" },
|
||||
{ label: "**", value: "**" },
|
||||
{ label: "+=", value: "+=" },
|
||||
{ label: "-=", value: "-=" },
|
||||
{ label: "==", value: "==" },
|
||||
{ label: "!=", value: "!=" },
|
||||
{ label: ">", value: ">" },
|
||||
{ label: ">=", value: ">=" },
|
||||
{ label: "<", value: "<" },
|
||||
{ label: "<=", value: "<=" },
|
||||
{ label: "and / &&", value: "and" },
|
||||
{ label: "or / ||", value: "or" },
|
||||
{ label: "not / !", value: "not" },
|
||||
]
|
||||
// 逻辑名 and/or/not 在 C 里显示成 && / || / !,存进去的还是逻辑名
|
||||
function operatorTargetOptions(lang: string): SelectOption[] {
|
||||
return Object.entries(AST_OPERATOR_TARGETS_BY_LANGUAGE[lang] ?? {}).map(
|
||||
([value, label]) => ({ label: label === value ? value : `${label}(${value})`, value }),
|
||||
)
|
||||
}
|
||||
|
||||
const NODE_ENGINES = ["must_exist_node", "must_not_exist_node", "count_node"]
|
||||
const FUNCTION_ENGINES = [
|
||||
@@ -122,6 +125,9 @@ function updateCountMode(lang: string, index: number, mode: "exact" | "range") {
|
||||
delete rule.min
|
||||
delete rule.max
|
||||
} else {
|
||||
// 留空的 count 规则恒真(rangePassed 三个字段全 undefined 就返回 true),
|
||||
// 描述还会退化成光秃秃一个「for 循环」。切过来先给个 1
|
||||
rule.min = rule.min ?? 1
|
||||
delete rule.exact
|
||||
}
|
||||
rules[index] = rule
|
||||
@@ -162,12 +168,14 @@ function updateRules(lang: string, rules: AstRule[]) {
|
||||
emit("update:modelValue", Object.keys(current).length > 0 ? current : null)
|
||||
}
|
||||
|
||||
function getTargetLabel(engine: string, target: string): string | undefined {
|
||||
if (isNodeEngine(engine))
|
||||
return (NODE_TARGET_OPTIONS.find((o) => o.value === target) as any)?.label
|
||||
if (isOperatorEngine(engine))
|
||||
return (OPERATOR_TARGET_OPTIONS.find((o) => o.value === target) as any)
|
||||
?.label
|
||||
function getTargetLabel(
|
||||
lang: string,
|
||||
engine: string,
|
||||
target: string,
|
||||
): string | undefined {
|
||||
if (isNodeEngine(engine)) return AST_NODE_TARGETS_BY_LANGUAGE[lang]?.[target]
|
||||
// 运算符不写 label:判题结果的文案按语言翻译(astOperatorLabel),
|
||||
// 存一个固定 label 反而会把 C 的 && 钉死成 and
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -176,7 +184,7 @@ function addRule(lang: string) {
|
||||
rules.push({
|
||||
engine: "must_exist_node",
|
||||
target: "for_loop",
|
||||
label: "for 循环",
|
||||
label: getTargetLabel(lang, "must_exist_node", "for_loop"),
|
||||
message: "",
|
||||
})
|
||||
updateRules(lang, rules)
|
||||
@@ -196,10 +204,10 @@ function updateRule(lang: string, index: number, field: string, value: any) {
|
||||
rule.engine = value
|
||||
if (isNodeEngine(value)) {
|
||||
rule.target = "for_loop"
|
||||
rule.label = "for 循环"
|
||||
rule.label = getTargetLabel(lang, value, "for_loop")
|
||||
} else if (isOperatorEngine(value)) {
|
||||
rule.target = "+"
|
||||
rule.label = "+"
|
||||
delete rule.label
|
||||
} else {
|
||||
rule.target = ""
|
||||
delete rule.label
|
||||
@@ -207,9 +215,11 @@ function updateRule(lang: string, index: number, field: string, value: any) {
|
||||
delete rule.min
|
||||
delete rule.max
|
||||
delete rule.exact
|
||||
// 次数引擎不给默认值的话,存下去就是一条恒真规则
|
||||
if (isCountEngine(value)) rule.exact = 1
|
||||
} else if (field === "target") {
|
||||
rule.target = value
|
||||
const lbl = getTargetLabel(rule.engine, value)
|
||||
const lbl = getTargetLabel(lang, rule.engine, value)
|
||||
if (lbl) rule.label = lbl
|
||||
else delete rule.label
|
||||
} else if (field === "min") {
|
||||
@@ -226,22 +236,32 @@ function updateRule(lang: string, index: number, field: string, value: any) {
|
||||
updateRules(lang, rules)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.languages,
|
||||
(langs) => {
|
||||
if (langs.length && !langs.includes(activeTab.value as LANGUAGE)) {
|
||||
activeTab.value = langs[0]
|
||||
}
|
||||
},
|
||||
)
|
||||
watch(supportedLanguages, (langs) => {
|
||||
if (langs.length && !langs.includes(activeTab.value as LANGUAGE)) {
|
||||
activeTab.value = langs[0]
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-collapse>
|
||||
<n-collapse-item title="代码规则检查(选填)" name="ast-rules">
|
||||
<n-tabs v-if="languages.length" type="segment" v-model:value="activeTab">
|
||||
<n-alert
|
||||
v-if="unsupportedLanguages.length"
|
||||
type="info"
|
||||
:bordered="false"
|
||||
style="margin-bottom: 8px"
|
||||
>
|
||||
{{ unsupportedLanguages.join("、") }} 暂不支持代码规则检查,判题机只能检查
|
||||
{{ AST_SUPPORTED_LANGUAGES.join(" / ") }}
|
||||
</n-alert>
|
||||
<n-tabs
|
||||
v-if="supportedLanguages.length"
|
||||
type="segment"
|
||||
v-model:value="activeTab"
|
||||
>
|
||||
<n-tab-pane
|
||||
v-for="lang in languages"
|
||||
v-for="lang in supportedLanguages"
|
||||
:key="lang"
|
||||
:name="lang"
|
||||
:tab="lang"
|
||||
@@ -264,7 +284,7 @@ watch(
|
||||
/>
|
||||
<n-select
|
||||
v-if="needsTargetDropdown(rule.engine)"
|
||||
:options="NODE_TARGET_OPTIONS"
|
||||
:options="nodeTargetOptions(lang)"
|
||||
:value="rule.target"
|
||||
@update:value="
|
||||
(v: string) => updateRule(lang, index, 'target', v)
|
||||
@@ -285,7 +305,7 @@ watch(
|
||||
/>
|
||||
<n-select
|
||||
v-if="needsOperatorDropdown(rule.engine)"
|
||||
:options="OPERATOR_TARGET_OPTIONS"
|
||||
:options="operatorTargetOptions(lang)"
|
||||
:value="rule.target"
|
||||
@update:value="
|
||||
(v: string) => updateRule(lang, index, 'target', v)
|
||||
@@ -370,7 +390,14 @@ watch(
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
<n-empty v-else description="请先选择编程语言" />
|
||||
<n-empty
|
||||
v-else
|
||||
:description="
|
||||
languages.length
|
||||
? '当前语言不支持代码规则检查'
|
||||
: '请先选择编程语言'
|
||||
"
|
||||
/>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
</template>
|
||||
|
||||
@@ -178,6 +178,13 @@ const columns: DataTableColumn<Submission["info"]["data"][number]>[] = [
|
||||
<Icon :icon="rule.passed ? 'ph:check-bold' : 'ph:x-bold'" />
|
||||
</n-icon>
|
||||
<span>{{ rule.description }}</span>
|
||||
<!-- 次数类规则光说「出现 2 次 ✗」,学生不知道自己写了几次 -->
|
||||
<span
|
||||
v-if="!rule.passed && rule.actual !== undefined"
|
||||
:style="{ color: theme.errorColor }"
|
||||
>
|
||||
当前 {{ rule.actual }} 次
|
||||
</span>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
|
||||
@@ -289,7 +289,12 @@ export interface StatisticInfo {
|
||||
err_info?: string
|
||||
time_cost?: number
|
||||
memory_cost?: number
|
||||
ast_results?: Array<{ description: string; passed: boolean }>
|
||||
ast_results?: Array<{
|
||||
description: string
|
||||
passed: boolean
|
||||
/** count_* 规则实际数到的次数,判题机只在这两个引擎上写 */
|
||||
actual?: number
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
5
bun.lock
5
bun.lock
@@ -24,6 +24,7 @@
|
||||
"postgres": "^3.4.9",
|
||||
"sql.js": "^1.14.2",
|
||||
"tree-sitter-c": "^0.24.1",
|
||||
"tree-sitter-cpp": "^0.23.4",
|
||||
"tree-sitter-python": "^0.25.0",
|
||||
"web-tree-sitter": "^0.26.13",
|
||||
"zod": "^4.4.3",
|
||||
@@ -1365,6 +1366,8 @@
|
||||
|
||||
"tree-sitter-c": ["tree-sitter-c@0.24.1", "https://registry.npmjs.com/tree-sitter-c/-/tree-sitter-c-0.24.1.tgz", { "dependencies": { "node-addon-api": "^8.3.1", "node-gyp-build": "^4.8.4" }, "peerDependencies": { "tree-sitter": "^0.22.4" }, "optionalPeers": ["tree-sitter"] }, "sha512-lkYwWN3SRecpvaeqmFKkuPNR3ZbtnvHU+4XAEEkJdrp3JfSp2pBrhXOtvfsENUneye76g889Y0ddF2DM0gEDpA=="],
|
||||
|
||||
"tree-sitter-cpp": ["tree-sitter-cpp@0.23.4", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2", "tree-sitter-c": "^0.23.1" }, "peerDependencies": { "tree-sitter": "^0.21.1" }, "optionalPeers": ["tree-sitter"] }, "sha512-qR5qUDyhZ5jJ6V8/umiBxokRbe89bCGmcq/dk94wI4kN86qfdV8k0GHIUEKaqWgcu42wKal5E97LKpLeVW8sKw=="],
|
||||
|
||||
"tree-sitter-python": ["tree-sitter-python@0.25.0", "https://registry.npmjs.com/tree-sitter-python/-/tree-sitter-python-0.25.0.tgz", { "dependencies": { "node-addon-api": "^8.5.0", "node-gyp-build": "^4.8.4" }, "peerDependencies": { "tree-sitter": "^0.25.0" }, "optionalPeers": ["tree-sitter"] }, "sha512-eCmJx6zQa35GxaCtQD+wXHOhYqBxEL+bp71W/s3fcDMu06MrtzkVXR437dRrCrbrDbyLuUDJpAgycs7ncngLXw=="],
|
||||
|
||||
"treemate": ["treemate@0.3.11", "https://registry.npmjs.com/treemate/-/treemate-0.3.11.tgz", {}, "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg=="],
|
||||
@@ -1537,6 +1540,8 @@
|
||||
|
||||
"strip-literal/js-tokens": ["js-tokens@10.0.0", "https://registry.npmjs.com/js-tokens/-/js-tokens-10.0.0.tgz", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="],
|
||||
|
||||
"tree-sitter-cpp/tree-sitter-c": ["tree-sitter-c@0.23.6", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" }, "peerDependencies": { "tree-sitter": "^0.22.1" }, "optionalPeers": ["tree-sitter"] }, "sha512-0dxXKznVyUA0s6PjNolJNs2yF87O5aL538A/eR6njA5oqX3C3vH4vnx3QdOKwuUdpKEcFdHuiDpRKLLCA/tjvQ=="],
|
||||
|
||||
"tsx/esbuild": ["esbuild@0.28.1", "https://registry.npmjs.com/esbuild/-/esbuild-0.28.1.tgz", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
|
||||
|
||||
"unplugin-vue-components/magic-string": ["magic-string@0.30.21", "https://registry.npmjs.com/magic-string/-/magic-string-0.30.21.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
@@ -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