fix(语法错误): 有模板的题报错行号多 1;else 后面写了条件被说成「少了冒号」,a+b=c 被说成「判断相等要用 ==」
全库 9182 条 Python 编译错误逐条过了一遍翻译表,1014 条的说明变了: - 行号:判题跑的是「模板 + 代码」,运行时错误扣了模板行数、编译错误没扣, 「第 N 行」和标红都落在下一行(691 条)。提交和运行例子两条路都扣上, gcc 的 main.c:N: 和代码摘录左边的行号一起扣。报错前先打了一条警告时 File 那行是缩进的,行号原来解析不出来。 - 同一句报错要看出错那一行才分得开:expected ':' 九成是 else a<60:(行尾有冒号), 「Maybe you meant '=='」只有 if / while 行才是真想判相等,a+b=c、a=96,b=92、 print(1+1=2) 各有各的说法。 - 选了 Python 却交了 C 代码(294 条),直接说换语言。只凭 printf 不算: Python 里 print 手误成 printf 的不少。 - 单独说清楚的:elif: 没写条件、esle / else if、=< 写反、条件之间用逗号、 漏了 +、√ ² ≤ ≥、顿号、全角 #、emoji(正则少了 u 标志)。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f413518a85
commit
96ce9725b2
6 files changed
+132
-20
No files matched your search
@@ -14,7 +14,7 @@ import { asRecord } from "../routes/helpers"
|
||||
import type { JudgeJobData } from "./job"
|
||||
import { judgeConfigFor } from "./languages"
|
||||
import { isAccepted, JudgeStatus, type JudgeStatusValue } from "./status"
|
||||
import { parseProblemTemplate } from "./template"
|
||||
import { parseProblemTemplate, shiftCompileLines } from "./template"
|
||||
import { runSqlCase } from "./sql"
|
||||
import {
|
||||
nativeRuntimeError,
|
||||
@@ -536,9 +536,11 @@ export async function judgeSubmission(job: JudgeJobData) {
|
||||
let statisticInfo: Record<string, unknown> = {}
|
||||
|
||||
if (response.err) {
|
||||
const errInfo =
|
||||
typeof response.data === "string" ? response.data : JSON.stringify(response.data)
|
||||
result = JudgeStatus.COMPILE_ERROR
|
||||
statisticInfo = {
|
||||
err_info: typeof response.data === "string" ? response.data : JSON.stringify(response.data),
|
||||
err_info: shiftCompileLines(errInfo, template ? template.prepend.split("\n").length : 0),
|
||||
score: 0,
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -8,3 +8,25 @@ export function parseProblemTemplate(template: string) {
|
||||
append: section("APPEND"),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编译错误里的行号扣掉模板拼在前面的行数,和运行时错误同一个口径(那边在
|
||||
* runtime-diagnosis.ts 里扣)。不扣的话有模板的题「第 N 行」和编辑器标红都落在下一行
|
||||
* —— 全库 691 条 Python 编译错误是这样。
|
||||
*
|
||||
* 认三种写法:Python 的 `File "…/solution.py", line N` 和 `(solution.py, line N)`,
|
||||
* gcc 的 `main.c:N:M:` 以及它摘录代码时左边的 ` N | `。落在模板里的行(N 不大于
|
||||
* 模板行数)是模板自己的错,原样留着。
|
||||
*/
|
||||
export function shiftCompileLines(message: string, prependLines: number) {
|
||||
if (prependLines <= 0) return message
|
||||
const shift = (line: string) => {
|
||||
const n = Number(line)
|
||||
return n > prependLines ? String(n - prependLines) : line
|
||||
}
|
||||
return message
|
||||
.replace(/(solution\.py", line )(\d+)/g, (_, head: string, n: string) => head + shift(n))
|
||||
.replace(/(\(solution\.py, line )(\d+)/g, (_, head: string, n: string) => head + shift(n))
|
||||
.replace(/(main\.c(?:pp)?:)(\d+)(?=:)/g, (_, head: string, n: string) => head + shift(n))
|
||||
.replace(/^(\s*)(\d+)( \|)/gm, (_, pad: string, n: string, bar: string) => pad + shift(n) + bar)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { config } from "../config"
|
||||
import { postJudge, statusValue, type JudgeCase, type JudgeResponse } from "./run"
|
||||
import { nativeRuntimeError, parsePythonTraceback } from "./runtime-diagnosis"
|
||||
import { JudgeStatus, type JudgeStatusValue } from "./status"
|
||||
import { shiftCompileLines } from "./template"
|
||||
|
||||
/**
|
||||
* 试运行:「运行例子」「自己输入」、后台「生成测试点」。
|
||||
@@ -148,7 +149,10 @@ async function judgeTrial(options: Parameters<typeof runTrial>[0]): Promise<Tria
|
||||
if (parsed.err === "CompileError") {
|
||||
return {
|
||||
status: "compile-error",
|
||||
message: typeof parsed.data === "string" ? parsed.data : "",
|
||||
message:
|
||||
typeof parsed.data === "string"
|
||||
? shiftCompileLines(parsed.data, options.prependLines)
|
||||
: "",
|
||||
}
|
||||
}
|
||||
throw new Error(`JudgeServer error ${parsed.err}: ${JSON.stringify(parsed.data)}`)
|
||||
|
||||
@@ -26,7 +26,7 @@ const props = defineProps<{
|
||||
const codeStore = useCodeStore()
|
||||
const theme = useThemeVars()
|
||||
|
||||
const explanation = computed(() => explainPythonCompileError(props.errInfo))
|
||||
const explanation = computed(() => explainPythonCompileError(props.errInfo, codeStore.code.value))
|
||||
|
||||
/** 判题机的临时目录名(`/judger/run/<32 位随机串>/`)对学生没有意义,只剩文件名 */
|
||||
const rawError = computed(() => props.errInfo.replace(/\/judger\/run\/[^/"]+\//g, ""))
|
||||
|
||||
@@ -32,7 +32,8 @@ export interface PythonErrorExplanation {
|
||||
punctuation: string | null
|
||||
}
|
||||
|
||||
const WHERE_RE = /^File "[^"]*", line (\d+)/m
|
||||
// 行首可能有空格:报错前面先打了一条 SyntaxWarning(比如 `3and`)时,File 那行是缩进的
|
||||
const WHERE_RE = /^\s*File "[^"]*", line (\d+)/m
|
||||
const SHORT_RE = /^Sorry: (\w+Error): (.*) \([^,()]*, line (\d+)\)\s*$/m
|
||||
|
||||
/**
|
||||
@@ -66,6 +67,7 @@ export const PUNCTUATION_MAP: Record<string, string> = {
|
||||
"‘": "'",
|
||||
"’": "'",
|
||||
""": '"',
|
||||
"#": "#",
|
||||
"×": "*",
|
||||
"÷": "/",
|
||||
[IDEOGRAPHIC_SPACE]: " ",
|
||||
@@ -100,18 +102,52 @@ export const PUNCTUATION_NAMES: Record<string, string> = {
|
||||
"‘": "单引号",
|
||||
"’": "单引号",
|
||||
""": "引号",
|
||||
"#": "井号",
|
||||
[IDEOGRAPHIC_SPACE]: "空格",
|
||||
}
|
||||
|
||||
type Rule = [RegExp, (match: RegExpMatchArray) => string]
|
||||
/**
|
||||
* 规则除了报错原文,还能看出错的那一行(判题机打出来的,去掉了行首空格)。同一句报错
|
||||
* 学生错法不同,要看那一行才分得开:`expected ':'` 九成是 `else a < 60:`,行尾明明有冒号;
|
||||
* 「Maybe you meant '=='」只有 if / while 那行才是真想判断相等。返回 null 就接着往下找。
|
||||
*/
|
||||
type Rule = [RegExp, (match: RegExpMatchArray, source: string) => string | null]
|
||||
|
||||
/** 去掉引号里的内容,只看代码本身的符号 */
|
||||
function stripStrings(source: string) {
|
||||
return source.replace(/(["'])(?:\\.|(?!\1).)*\1/g, '""')
|
||||
}
|
||||
|
||||
/** 括号外面的逗号:`if a > 9, a < 100:` 里有,`if x in (1, 2):` 里没有 */
|
||||
function hasTopLevelComma(source: string) {
|
||||
let depth = 0
|
||||
for (const ch of stripStrings(source)) {
|
||||
if ("([{".includes(ch)) depth++
|
||||
else if (")]}".includes(ch)) depth--
|
||||
else if (ch === "," && depth === 0) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const CONDITION_LINE = /^(if|elif|while)\b/
|
||||
|
||||
const RULES: Rule[] = [
|
||||
[
|
||||
/^invalid character '(.)'/,
|
||||
// u:emoji 是两个 UTF-16 单元,不带 u 的 (.) 对不上
|
||||
/^invalid character '(.)'/u,
|
||||
([, char]) => {
|
||||
// 数学符号不是「中文的」,是写法不对:代码里乘除用 * 和 /
|
||||
if (char === "×") return "这里用了乘号「×」,代码里的乘法要写成星号「*」。"
|
||||
if (char === "÷") return "这里用了除号「÷」,代码里的除法要写成斜杠「/」。"
|
||||
if (char === "√")
|
||||
return "代码里没有根号「√」:开平方写成 x ** 0.5,或者 import math 之后用 math.sqrt(x)。"
|
||||
if (char === "²" || char === "³")
|
||||
return `代码里没有上标的「${char}」:平方写成 x ** 2,立方写成 x ** 3。`
|
||||
if (char === "≤") return "代码里没有「≤」:小于等于要写成小于号加等号 <=。"
|
||||
if (char === "≥") return "代码里没有「≥」:大于等于要写成大于号加等号 >=。"
|
||||
if (char === "≠") return "代码里没有「≠」:不等于要写成感叹号加等号 !=。"
|
||||
if (char === "、")
|
||||
return "代码里不用顿号「、」:几样东西之间要用英文逗号隔开。写代码的时候先切换到英文输入法。"
|
||||
const name = PUNCTUATION_NAMES[char!]
|
||||
if (name) {
|
||||
return `这里的${name}是中文输入法打出来的,要换成英文的${name}。写代码的时候先切换到英文输入法。`
|
||||
@@ -126,7 +162,10 @@ const RULES: Rule[] = [
|
||||
],
|
||||
[
|
||||
/^expected ':'/,
|
||||
() => "这一行末尾少了英文冒号「:」。if、elif、else、for、while、def 这些行的最后都要有冒号。",
|
||||
(_, source) =>
|
||||
/^else\b\s*[^\s:]/.test(source)
|
||||
? "else 后面不能写条件,它的意思是「上面的条件都不满足」。还要再判断一个条件,就用 elif 条件:。"
|
||||
: "这一行末尾少了英文冒号「:」。if、elif、else、for、while、def 这些行的最后都要有冒号。",
|
||||
],
|
||||
[
|
||||
/^expected an indented block after '(\w+)' statement on line (\d+)/,
|
||||
@@ -152,12 +191,22 @@ const RULES: Rule[] = [
|
||||
],
|
||||
[
|
||||
/^invalid syntax\. Perhaps you forgot a comma\?/,
|
||||
() => "这里好像少了逗号。括号里的几样东西之间,要用英文逗号「,」隔开。",
|
||||
// 抽样一半以上是拼文字漏了 +:print(a+"是"a+"岁")
|
||||
() =>
|
||||
"这里的几样东西之间少了连接的符号:要分开输出,用英文逗号「,」隔开;要把文字拼在一起,用加号「+」。",
|
||||
],
|
||||
[
|
||||
/Maybe you meant '==' instead of '='|Maybe you meant '==' or ':=' instead of '='|perhaps you meant "=="/,
|
||||
() =>
|
||||
"判断「是不是相等」要用两个等号「==」。一个等号「=」的意思是「把右边的值存进左边的变量」。",
|
||||
(_, source) => {
|
||||
// 只有条件里才是真想判断相等;别的行是想赋值,写法错了
|
||||
if (CONDITION_LINE.test(source))
|
||||
return "判断「是不是相等」要用两个等号「==」。一个等号「=」的意思是「把右边的值存进左边的变量」。"
|
||||
if (/^\w+\s*=[^=].*,\s*\w+\s*=[^=]/.test(stripStrings(source)))
|
||||
return "一行里给几个变量赋值,要写成 a, b = 96, 92,或者分成几行,一行写一个。"
|
||||
if (/^\w+\s*\(/.test(source))
|
||||
return '括号里不能直接写等号「=」。要把等号显示出来,就把它放进引号里,比如 print(a, "+", b, "=", a + b)。'
|
||||
return null
|
||||
},
|
||||
],
|
||||
[
|
||||
/^cannot assign to /,
|
||||
@@ -209,6 +258,21 @@ const RULES: Rule[] = [
|
||||
/^leading zeros in decimal integer literals/,
|
||||
() => "整数不能用 0 开头,比如「07」要写成「7」。",
|
||||
],
|
||||
[
|
||||
/^invalid syntax/,
|
||||
(_, source) => {
|
||||
if (/^elif\s*:/.test(source))
|
||||
return "elif 后面要写条件,比如 elif a > 60:。不需要条件的话就用 else:。"
|
||||
if (/^else\s+if\b/.test(source)) return "Python 里「否则如果」要写成 elif,不是 else if。"
|
||||
if (/^(esle|eles|els|elese|esif|elsif|elseif|eilf|elfi)\b/.test(source))
|
||||
return "这一行开头的关键字拼错了:「否则」是 else,「否则如果」是 elif。"
|
||||
const code = stripStrings(source)
|
||||
if (/=<|=>/.test(code)) return "小于等于要写成 <=,大于等于要写成 >=,等号放在后面。"
|
||||
if (CONDITION_LINE.test(source) && hasTopLevelComma(source))
|
||||
return "条件之间不能用逗号连:两个都要满足用 and,满足一个就行用 or,比如 if a > 9 and a < 100:。"
|
||||
return null
|
||||
},
|
||||
],
|
||||
[
|
||||
/^invalid syntax/,
|
||||
() =>
|
||||
@@ -216,22 +280,39 @@ const RULES: Rule[] = [
|
||||
],
|
||||
]
|
||||
|
||||
function translate(message: string) {
|
||||
function translate(message: string, source: string) {
|
||||
for (const [pattern, render] of RULES) {
|
||||
const match = message.match(pattern)
|
||||
if (match) return render(match)
|
||||
const text = match && render(match, source)
|
||||
if (text) return text
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** 翻不出来(判题机自己的报错、没见过的句式)就返回 null,界面照旧显示原文 */
|
||||
export function explainPythonCompileError(errInfo: string): PythonErrorExplanation | null {
|
||||
/**
|
||||
* 选了 Python 却交了 C 代码:全库 310 条,报错各式各样(多半是 invalid syntax),
|
||||
* 挨个解释毫无意义,该说的只有一句
|
||||
*/
|
||||
function looksLikeC(code: string) {
|
||||
if (/^\s*#\s*include\b|\bint\s+main\s*\(/m.test(code)) return true
|
||||
// 只凭 printf 不算:Python 代码里把 print 手误成 printf 的不少
|
||||
return /\b(printf|scanf)\s*\(|\bcout\s*<</.test(code) && !/\b(print|input)\s*\(/.test(code)
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻不出来(判题机自己的报错、没见过的句式)就返回 null,界面照旧显示原文。
|
||||
* `code` 是学生的代码,用来认出「交的其实是 C」
|
||||
*/
|
||||
export function explainPythonCompileError(
|
||||
errInfo: string,
|
||||
code = "",
|
||||
): PythonErrorExplanation | null {
|
||||
const text = errInfo.trim()
|
||||
|
||||
const short = text.match(SHORT_RE)
|
||||
if (short) {
|
||||
const [, , detail, line] = short
|
||||
const message = translate(detail!)
|
||||
const message = translate(detail!, "")
|
||||
if (!message) return null
|
||||
return {
|
||||
line: Number(line),
|
||||
@@ -247,8 +328,6 @@ export function explainPythonCompileError(errInfo: string): PythonErrorExplanati
|
||||
const last = lines[lines.length - 1]!
|
||||
const detail = last.replace(/^\w+Error: /, "")
|
||||
if (detail === last) return null
|
||||
const message = translate(detail)
|
||||
if (!message) return null
|
||||
|
||||
let sourceLine: string | null = null
|
||||
let caret: PythonErrorExplanation["caret"] = null
|
||||
@@ -263,7 +342,12 @@ export function explainPythonCompileError(errInfo: string): PythonErrorExplanati
|
||||
}
|
||||
}
|
||||
|
||||
const char = detail.match(/^invalid character '(.)'/)?.[1]
|
||||
const message = looksLikeC(code)
|
||||
? "这是 C 语言的代码,可提交时选的语言是 Python。把语言换成 C 再交。"
|
||||
: translate(detail, sourceLine?.trim() ?? "")
|
||||
if (!message) return null
|
||||
|
||||
const char = detail.match(/^invalid character '(.)'/u)?.[1]
|
||||
return {
|
||||
line: where ? Number(where[1]) : null,
|
||||
sourceLine,
|
||||
|
||||
@@ -134,7 +134,7 @@ const explain = computed(() => {
|
||||
const err = info?.err_info ? stripJudgePath(info.err_info) : ""
|
||||
switch (s.result) {
|
||||
case SubmissionStatus.compile_error: {
|
||||
const ex = s.language === "Python" && err ? explainPythonCompileError(err) : null
|
||||
const ex = s.language === "Python" && err ? explainPythonCompileError(err, s.code) : null
|
||||
if (ex) return { line: ex.line, text: ex.message, raw: err }
|
||||
return { line: null, text: "编译没通过,看看下面的原始报错。", raw: err }
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user