Compare commits
3
Commits
6306b5a919
...
e118d80057
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e118d80057 | ||
|
|
96ce9725b2 | ||
|
|
f413518a85 |
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,10 +536,21 @@ export async function judgeSubmission(job: JudgeJobData) {
|
||||
let statisticInfo: Record<string, unknown> = {}
|
||||
|
||||
if (response.err) {
|
||||
result = JudgeStatus.COMPILE_ERROR
|
||||
statisticInfo = {
|
||||
err_info: typeof response.data === "string" ? response.data : JSON.stringify(response.data),
|
||||
score: 0,
|
||||
const errInfo =
|
||||
typeof response.data === "string" ? response.data : JSON.stringify(response.data)
|
||||
// 只有 CompileError 是学生的错。别的(JudgeClientError 的「Test case not found」
|
||||
// 这类)是题目或判题机的问题:原来一律记成编译错误,学生看到一句英文、以为自己写错了,
|
||||
// 全库 337 条,最近一条就在 2026-09。
|
||||
if (response.err === "CompileError") {
|
||||
result = JudgeStatus.COMPILE_ERROR
|
||||
statisticInfo = {
|
||||
err_info: shiftCompileLines(errInfo, template ? template.prepend.split("\n").length : 0),
|
||||
score: 0,
|
||||
}
|
||||
} else {
|
||||
console.warn(`[judge] 判题机报错 ${row.submission.id}: ${response.err} ${errInfo}`)
|
||||
result = JudgeStatus.SYSTEM_ERROR
|
||||
statisticInfo = { err_info: errInfo, score: 0 }
|
||||
}
|
||||
} else {
|
||||
if (!Array.isArray(response.data)) {
|
||||
|
||||
@@ -15,31 +15,105 @@ import type { StatisticInfo } from "@oj2/contract"
|
||||
* `invalid literal for int() with base 10: '1 2'`,给学生看就等于把隐藏数据放出去 ——
|
||||
* 故意写 `raise Exception(input())` 就能一个点一个点地套。所以只存:
|
||||
* - 异常类型和归好类的 `kind`(按消息的**句式**判,不取消息里的值)
|
||||
* - NameError / AttributeError 点名的名字,而且只在它**原样出现在学生代码里**时才存
|
||||
* - NameError / AttributeError / 部分 TypeError 点名的名字,而且只在它**原样出现在
|
||||
* 学生代码里**时才存
|
||||
*/
|
||||
export type RuntimeErrorInfo = NonNullable<StatisticInfo["runtime_error"]>
|
||||
|
||||
const IDENTIFIER = /^[A-Za-z_]\w*$/
|
||||
|
||||
/** 按消息句式细分。键是异常类型,值是 [句式, kind],从上往下取第一个对上的 */
|
||||
/**
|
||||
* 值的类型比句式更能说明错在哪。消息里的类型名是 Python 按学生代码里的值报的,
|
||||
* 不来自输入,所以可以拿来分类。三种在 TypeError 和 AttributeError 里都会出现,
|
||||
* 句式五花八门(`int(input)`、`a, b = input`、`input[0]`、`input.split()`……),
|
||||
* 按句式分会被说成「类型不对」「对数字用了下标」,其实全是同一个错:
|
||||
*/
|
||||
const VALUE_KINDS: [RegExp, string][] = [
|
||||
// 函数名后面忘了写 (),拿到的是函数本身
|
||||
[/\bbuiltin_function_or_method\b/, "func-value"],
|
||||
// print() / append() / sort() 的结果、没有 return 的函数的结果
|
||||
[/'NoneType'/, "none-value"],
|
||||
// 把 list 这个类型名当成了自己的列表变量:list[i]
|
||||
[/\btypes\.GenericAlias\b/, "type-name"],
|
||||
// map() 的结果不能直接 len()、下标
|
||||
[/'map' object is not subscriptable|object of type 'map' has no len/, "map-value"],
|
||||
]
|
||||
|
||||
/**
|
||||
* 按消息句式细分。键是异常类型,值是 [句式, kind],从上往下取第一个对上的。
|
||||
*
|
||||
* 表是照全库 9228 条 Python 运行时错误重跑出来的回溯定的(2026-09):原来只有
|
||||
* 12 种,TypeError 过半掉进兜底,被说成「把文字当成数字」,而代码里往往连 input()
|
||||
* 都没有 —— 实际是参数个数不对、参数名拼错、`list.index(x)` 这类用法错误。
|
||||
*/
|
||||
const KINDS: Record<string, [RegExp, string][]> = {
|
||||
ValueError: [
|
||||
[/^invalid literal for int\(\)/, "int-parse"],
|
||||
[/^could not convert string to float/, "float-parse"],
|
||||
[/^(not enough|too many) values to unpack/, "unpack"],
|
||||
[/^math domain error/, "math-domain"],
|
||||
[/^empty separator/, "empty-sep"],
|
||||
[/(x not in list|is not in list)$/, "not-in-list"],
|
||||
[
|
||||
/^(unsupported format character|Format specifier missing precision|Invalid format specifier|Unknown format code|incomplete format)/,
|
||||
"format-spec",
|
||||
],
|
||||
],
|
||||
TypeError: [
|
||||
...VALUE_KINDS,
|
||||
[/^can only concatenate str/, "str-concat"],
|
||||
[/^unsupported operand type/, "operand"],
|
||||
[/cannot be interpreted as an integer/, "not-int"],
|
||||
[/not supported between instances of/, "compare"],
|
||||
[/object is not callable/, "not-callable"],
|
||||
[/^type '\w+' is not subscriptable/, "type-subscript"],
|
||||
[/object is not subscriptable/, "not-subscriptable"],
|
||||
[/missing \d+ required positional argument/, "missing-arg"],
|
||||
// s[2, 2]:逗号让下标变成了一组值
|
||||
[/^(list|string|tuple) indices must be integers.*, not '?tuple'?$/, "index-comma"],
|
||||
[/^(list|string|tuple) indices must be integers/, "index-type"],
|
||||
[/^slice indices must be integers/, "slice-type"],
|
||||
[/unexpected keyword argument/, "keyword"],
|
||||
[/takes no keyword arguments/, "no-keyword"],
|
||||
[
|
||||
/^(descriptor '\w+' for '\w+' objects doesn't apply|unbound method \w+\.\w+\(\) needs an argument)/,
|
||||
"descriptor",
|
||||
],
|
||||
[
|
||||
/takes (exactly one|no|at most \d+|at least \d+|\d+)( positional)? arguments?|expected (at least |at most )?\d+ arguments?, got \d+/,
|
||||
"arg-count",
|
||||
],
|
||||
// 读进来的是文字,却拿去 % 取余、* 乘
|
||||
[/^not all arguments converted during string formatting/, "str-mod"],
|
||||
[/^can't multiply sequence by non-int/, "seq-mul"],
|
||||
[/^bad operand type for unary [+-]: 'str'/, "unary"],
|
||||
[/^cannot unpack non-iterable (int|float) object/, "unpack-number"],
|
||||
[/^(int|float)\(\) argument must be/, "convert-arg"],
|
||||
[/is not iterable/, "not-iterable"],
|
||||
[/has no len\(\)/, "no-len"],
|
||||
],
|
||||
AttributeError: [
|
||||
...VALUE_KINDS,
|
||||
[/attribute '\w+' is read-only/, "read-only"],
|
||||
// 数字没有 split() / count():多半是先 int() 再 split(),顺序反了
|
||||
[/^'(int|float)' object has no attribute/, "number-attr"],
|
||||
],
|
||||
IndexError: [[/out of range/, "index"]],
|
||||
// 程序本身编译过了,运行时的 SyntaxError 只可能来自 eval() / exec() 读到的内容
|
||||
SyntaxError: [[/./, "eval-syntax"]],
|
||||
}
|
||||
|
||||
/**
|
||||
* TypeError 细分之后点名的名字:函数名(arg-count / no-keyword,消息开头的
|
||||
* `list.append()` / `insert expected` 取最后一段)、写错的参数名(keyword)、
|
||||
* 方法名(descriptor)、类型名(type-subscript)
|
||||
*/
|
||||
const TYPE_ERROR_NAMES: Record<string, RegExp> = {
|
||||
"arg-count": /^(?:\w+\.)*(\w+)(?:\(\) takes| expected)/,
|
||||
"no-keyword": /^(?:\w+\.)*(\w+)\(\) takes no keyword/,
|
||||
keyword: /unexpected keyword argument '([^']+)'/,
|
||||
descriptor: /^(?:descriptor '|unbound method \w+\.)(\w+)/,
|
||||
"type-subscript": /^type '([^']+)'/,
|
||||
}
|
||||
|
||||
/** 名字在学生自己的代码里原样出现过才算数,否则它可能来自输入(`eval(input())`) */
|
||||
@@ -94,9 +168,21 @@ export function parsePythonTraceback(
|
||||
if (name) info.name = name
|
||||
const suggestion = message.match(/Did you mean: '([^']+)'\?/)?.[1]
|
||||
if (suggestion && IDENTIFIER.test(suggestion)) info.suggestion = suggestion
|
||||
} else if (type === "AttributeError") {
|
||||
const name = nameFromCode(message.match(/has no attribute '([^']+)'/)?.[1], code)
|
||||
// 用了 math.sqrt 却没 import math。Python 同时给了「Did you mean」时也是这个优先
|
||||
if (/Did you forget to import '/.test(message)) info.kind = "import"
|
||||
} else if (type === "TypeError" && kind && TYPE_ERROR_NAMES[kind]) {
|
||||
const name = nameFromCode(message.match(TYPE_ERROR_NAMES[kind])?.[1], code)
|
||||
if (name) info.name = name
|
||||
// 「Did you mean 'end'?」是 Python 从函数的参数表里挑的,不来自输入
|
||||
const suggestion = message.match(/Did you mean '([^']+)'\?/)?.[1]
|
||||
if (kind === "keyword" && suggestion && IDENTIFIER.test(suggestion))
|
||||
info.suggestion = suggestion
|
||||
} else if (type === "AttributeError") {
|
||||
const name = nameFromCode(message.match(/(?:has no attribute|attribute) '([^']+)'/)?.[1], code)
|
||||
if (name) info.name = name
|
||||
// 拼错方法名(spilt → split)占 AttributeError 的一半多,Python 从这个值的方法里挑的
|
||||
const suggestion = message.match(/Did you mean: '([^']+)'\?/)?.[1]
|
||||
if (suggestion && IDENTIFIER.test(suggestion)) info.suggestion = suggestion
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
@@ -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, ""))
|
||||
|
||||
@@ -12,7 +12,7 @@ const props = defineProps<{
|
||||
code: string
|
||||
}>()
|
||||
|
||||
const message = computed(() => explainRuntimeError(props.info))
|
||||
const message = computed(() => explainRuntimeError(props.info, props.code))
|
||||
|
||||
const sourceLine = computed(() => {
|
||||
const line = props.info.line
|
||||
|
||||
@@ -146,6 +146,10 @@ const msg = computed(() => {
|
||||
"程序占的内存太多了。通常是列表、数组开得太大,或者在循环里不停地往里加东西、停不下来。\n\n"
|
||||
}
|
||||
|
||||
// 判题机的原文是英文(多半是「Test case not found」,题目缺测试数据),学生看了只会以为自己错了
|
||||
if (result === SubmissionStatus.system_error)
|
||||
return "判题出了问题,不是你的代码写错了。告诉老师看一下这道题。"
|
||||
|
||||
if (result !== SubmissionStatus.ast_check_failed && props.submission.statisticInfo?.err_info) {
|
||||
msg += stripJudgePath(props.submission.statisticInfo.err_info)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -10,32 +10,149 @@ import type { StatisticInfo } from "@oj2/contract"
|
||||
*/
|
||||
export type RuntimeErrorInfo = NonNullable<StatisticInfo["runtime_error"]>
|
||||
|
||||
/** 说得最多的一句:input() 读进来的是文字。学生栽在类型上,十有八九是这个 */
|
||||
/**
|
||||
* 说得最多的一句:input() 读进来的是文字。只在代码里真有 input( 时才说 —— 列表练习题
|
||||
* 里的类型错误压根没读输入,硬说这句等于指错方向(题目模板里的 input() 看不到,不算)
|
||||
*/
|
||||
const INPUT_IS_TEXT = "input() 读进来的是文字,要计算先用 int() 或 float() 转成数字。"
|
||||
|
||||
const BY_KIND: Record<string, string> = {
|
||||
"int-parse":
|
||||
"把输入转成整数的时候失败了:读到的内容不是一个整数。常见原因:一行里输入了好几个数,要先用 split() 拆开再转换;或者输入的是小数,要用 float()。",
|
||||
"float-parse":
|
||||
"把输入转成数字的时候失败了:读到的内容不是一个数。常见原因:一行里输入了好几个数,要先用 split() 拆开再转换。",
|
||||
unpack:
|
||||
/** 参数个数不对时,按函数补一句正确用法。都是学生最常用错的几个 */
|
||||
const ARG_TIPS: Record<string, string> = {
|
||||
append:
|
||||
'append() 一次只能加一个值;要一次加好几个,用 extend(),把它们放进一个列表:extend(["甲", "乙"])。',
|
||||
extend: 'extend() 括号里只放一个列表,比如 extend(["甲", "乙"]),不是把几个值分开写。',
|
||||
insert: 'insert() 要两个值:先写位置,再写要插进去的值,比如 insert(1, "甲")。',
|
||||
pop: "pop() 括号里最多放一个位置,不写就是取出最后一个。",
|
||||
remove: "remove() 括号里只放要删掉的那一个值。",
|
||||
index: "index() 括号里放要找的那个值。",
|
||||
count: "count() 括号里放要数的那个值。",
|
||||
find: "find() 括号里放要找的那段文字。",
|
||||
len: "len() 括号里只放一个值,比如 len(line)。",
|
||||
input: "input() 括号里最多放一句提示文字;要输出几个值,用 print()。",
|
||||
replace: 'replace() 要两个值:先写旧的,再写新的,比如 replace("a", "b")。',
|
||||
range: "range() 括号里放一到三个整数,比如 range(5)、range(1, 10)。",
|
||||
join: 'join() 括号里只放一个列表,比如 "、".join(line)。',
|
||||
}
|
||||
|
||||
/** 学生常当成方法写的内置函数:s.len()、line.sorted() */
|
||||
const BUILTIN_FUNCTIONS = new Set([
|
||||
"len",
|
||||
"sorted",
|
||||
"sum",
|
||||
"max",
|
||||
"min",
|
||||
"int",
|
||||
"str",
|
||||
"float",
|
||||
"list",
|
||||
"print",
|
||||
"input",
|
||||
"reversed",
|
||||
"round",
|
||||
"abs",
|
||||
])
|
||||
|
||||
type Explain = (info: RuntimeErrorInfo, hasInput: boolean) => string
|
||||
|
||||
const BY_KIND: Record<string, Explain> = {
|
||||
"int-parse": (_, hasInput) =>
|
||||
hasInput
|
||||
? "把输入转成整数的时候失败了:读到的内容不是一个整数。常见原因:一行里输入了好几个数,要先用 split() 拆开再转换;或者输入的是小数,要用 float()。"
|
||||
: "把文字转成整数的时候失败了:这段文字不是一个整数,比如里面有空格、小数点或者别的字。",
|
||||
"float-parse": (_, hasInput) =>
|
||||
hasInput
|
||||
? "把输入转成数字的时候失败了:读到的内容不是一个数。常见原因:一行里输入了好几个数,要先用 split() 拆开再转换。"
|
||||
: "把文字转成数字的时候失败了:这段文字不是一个数。",
|
||||
unpack: () =>
|
||||
"等号左边的变量个数,和右边拆出来的值的个数对不上。比如 a, b = input().split() 要求这一行正好有两个值,对照题目的输入格式看看一行里有几个数。",
|
||||
"math-domain": "数学函数拿到了不合法的值,比如给负数开平方。",
|
||||
"str-concat": `文字和数字不能直接用 + 连在一起。${INPUT_IS_TEXT}要拼成一句话,就用 str() 把数字转成文字。`,
|
||||
operand: `这两个值的类型不能做这种运算,多半是拿文字和数字一起算。${INPUT_IS_TEXT}`,
|
||||
"not-int":
|
||||
"这里需要一个整数,给的却是文字或小数。比如 range() 里要放整数,input() 读进来的要先用 int() 转换。",
|
||||
compare: `文字和数字不能比大小。${INPUT_IS_TEXT}`,
|
||||
"not-callable":
|
||||
"math-domain": () => "数学函数拿到了不合法的值,比如给负数开平方。",
|
||||
"empty-sep": () =>
|
||||
"split() 的括号里不能放空的 '':要按空格拆,括号里什么都不写,就是 split();要把文字拆成一个个字,用 list(文字)。",
|
||||
"not-in-list": () =>
|
||||
"要找或者要删的值不在列表里:index() 和 remove() 只能用列表里真有的值。可以先用 in 判断一下,比如 if x in line:。",
|
||||
"format-spec": () =>
|
||||
"保留小数的格式写错了:要写成 %.2f 或者 {:.2f}。中间的点是英文句点,不是逗号,也不是中文句号;点后面写数字,最后是 f。",
|
||||
"str-concat": (_, hasInput) =>
|
||||
`文字和数字不能直接用 + 连在一起。${hasInput ? INPUT_IS_TEXT : ""}要拼成一句话,就用 str() 把数字转成文字。`,
|
||||
operand: (_, hasInput) =>
|
||||
hasInput
|
||||
? `这两个值的类型不能做这种运算,多半是拿文字和数字一起算。${INPUT_IS_TEXT}`
|
||||
: "这两个值的类型不能做这种运算,比如文字和数字不能相减、相除。",
|
||||
"not-int": (_, hasInput) =>
|
||||
`这里需要一个整数,给的却是文字、小数或者列表。比如 range() 里要放整数${hasInput ? ",input() 读进来的要先用 int() 转换" : ""}。`,
|
||||
compare: (_, hasInput) => `文字和数字不能比大小。${hasInput ? INPUT_IS_TEXT : ""}`,
|
||||
"not-callable": () =>
|
||||
"把一个不是函数的东西当成函数用了。常见原因:乘法漏写了 *(比如 2(a+b) 要写成 2*(a+b)),或者拿 print、input、sum 这类名字当了变量名。",
|
||||
"not-subscriptable": "对一个数字用了下标 [ ]。只有列表、字符串这类值才能用 [ ] 取出其中一个。",
|
||||
"missing-arg": "调用函数的时候少给了参数。",
|
||||
"index-type": "下标要用整数。input() 读进来的是文字,要先用 int() 转换。",
|
||||
"not-subscriptable": () =>
|
||||
"对一个数字用了下标 [ ]。只有列表、字符串这类值才能用 [ ] 取出其中一个。",
|
||||
"missing-arg": () => "调用函数的时候,括号里少给了值。",
|
||||
"index-type": (_, hasInput) =>
|
||||
`下标要用整数。${hasInput ? "input() 读进来的是文字,要先用 int() 转换。" : ""}`,
|
||||
"index-comma": () => "下标里不能写逗号:取一个用 s[2],取一段用冒号,比如 s[2:4]。",
|
||||
"slice-type": () =>
|
||||
"表示位置的值要用整数。常见的是 index():括号里先写要找的那个值,后面如果再写,就是查找范围的起止位置,只能是整数;切片 a[开始:结束] 里的位置也一样。",
|
||||
"arg-count": ({ name }) => {
|
||||
if (!name) return "调用函数的时候,括号里给的值的个数不对。"
|
||||
return `「${name}()」括号里给的值的个数不对。${ARG_TIPS[name] ?? ""}`
|
||||
},
|
||||
keyword: ({ name, suggestion }) => {
|
||||
if (name && suggestion) return `「${name}=」这个名字写错了,是不是想写「${suggestion}=」?`
|
||||
if (name) return `函数不认识「${name}=」这个写法,检查一下拼写。`
|
||||
return "函数不认识括号里「名字=」这个写法,检查一下拼写。"
|
||||
},
|
||||
"no-keyword": ({ name }) =>
|
||||
`${name ? `「${name}()」的` : ""}括号里不能写「名字=值」,直接写值就行。`,
|
||||
descriptor: ({ name }) =>
|
||||
name
|
||||
? `要写成「变量名.${name}(...)」:点前面是你自己的那个列表或字符串,而不是 list、str 这种类型名。`
|
||||
: "要写成「变量名.方法名(...)」:点前面是你自己的那个列表或字符串,而不是 list、str 这种类型名。",
|
||||
"type-subscript": ({ name }) =>
|
||||
name
|
||||
? `「${name}」后面要用圆括号:写成 ${name}(...),不是 ${name}[...]。`
|
||||
: "类型名后面要用圆括号,比如 int(...),不是 int[...]。",
|
||||
"func-value": () =>
|
||||
"有一个函数名后面忘了写括号:比如 input 要写成 input(),s.split 要写成 s.split()。不写括号,拿到的是函数本身,不是它的结果。",
|
||||
"none-value": () =>
|
||||
"这个值是 None,也就是什么都没有:print()、append()、sort() 这类函数只做事、不给结果,自己写的函数没写 return 也一样。比如 a = print(x) 之后 a 就是 None,别拿它接着用。",
|
||||
"type-name": () =>
|
||||
"list、dict 这些是 Python 自带的类型名,不是你的变量:这里要写你自己起的变量名,比如 scores[i],而不是 list[i]。",
|
||||
"map-value": () =>
|
||||
"map() 的结果不能直接用 len() 或者下标 [ ],先用 list() 把它转成列表,比如 scores = list(map(int, input().split()))。",
|
||||
"str-mod": (_, hasInput) =>
|
||||
hasInput
|
||||
? `对文字用了 %:% 用在文字上是格式化,不是取余。${INPUT_IS_TEXT}`
|
||||
: "对文字用了 %:% 用在文字上是格式化,不是取余;用 % 格式化的话,文字里 % 的个数要和后面给的值一样多。",
|
||||
"seq-mul": (_, hasInput) =>
|
||||
`文字只能乘一个整数,意思是把它重复几遍,不能和文字或者小数相乘。${hasInput ? INPUT_IS_TEXT : ""}`,
|
||||
unary: () =>
|
||||
"+ 的左边少了东西:比如写成了 print(+a) 或者 , +b,+ 只有右边有值。要把文字连起来,+ 两边都要有值;要分开输出,用逗号隔开就行,不用再写 +。",
|
||||
"unpack-number": (_, hasInput) =>
|
||||
hasInput
|
||||
? "一个数不能拆给好几个变量。一行里有好几个数的话,要先用 split() 拆开,比如 a, b = map(int, input().split())。"
|
||||
: "一个数不能拆给好几个变量:等号右边要是一组值,比如 a, b = 1, 2。",
|
||||
"convert-arg": (_, hasInput) =>
|
||||
`int() 和 float() 一次只能转换一个值,不能直接转换整个列表。${hasInput ? "一行里有好几个数的话,写成 map(int, input().split())。" : ""}`,
|
||||
"not-iterable": () =>
|
||||
"这个值不能一个个地取出来:for 和 in 后面要放列表、字符串或者 range(),比如 for i in 5 要写成 for i in range(5)。",
|
||||
"no-len": () => "数字没有长度:len() 只能用在字符串、列表这类值上。",
|
||||
"read-only": ({ name }) =>
|
||||
name
|
||||
? `「${name}」是方法,要用括号调用,写成「变量名.${name}(...)」,不能用等号给它赋值。`
|
||||
: "方法要用括号调用,不能用等号给它赋值。",
|
||||
"eval-syntax": (_, hasInput) =>
|
||||
`eval() 读到的内容不是一个完整的算式。${hasInput ? "常见原因:一行里有好几个数用空格隔开,这时要用 input().split() 拆开,再用 int() 转换。" : ""}`,
|
||||
"number-attr": ({ name }) =>
|
||||
`数字没有${name ? `「${name}」这个` : "这个"}方法,split()、count() 这些是文字和列表才有的。常见原因:先用 int() 转换、再 split(),顺序反了,要先拆开再转换。`,
|
||||
import: ({ name }) =>
|
||||
name
|
||||
? `用「${name}」之前要先导入:在程序的开头写 import ${name}。`
|
||||
: "用到的模块还没导入:在程序的开头写 import 和模块名。",
|
||||
}
|
||||
|
||||
const BY_TYPE: Record<string, string> = {
|
||||
ValueError: "这里用到的值不符合要求。",
|
||||
TypeError: `这里的值类型不对,比如把文字当成数字来用。${INPUT_IS_TEXT}`,
|
||||
TypeError:
|
||||
"这一行里有个值的类型不对,或者某个函数的用法不对,对照一下每个值是什么、函数该怎么用。",
|
||||
IndexError:
|
||||
"下标越界了:要取的位置超出了列表或字符串的长度。下标从 0 开始,长度是 n 的话,最后一个是 [n-1]。",
|
||||
UnboundLocalError: "函数里的这个变量还没赋值就先用了。",
|
||||
@@ -50,7 +167,9 @@ const BY_TYPE: Record<string, string> = {
|
||||
OverflowError: "算出来的数太大了,超出了能表示的范围。",
|
||||
}
|
||||
|
||||
function explainPython(info: RuntimeErrorInfo) {
|
||||
function explainPython(info: RuntimeErrorInfo, code: string) {
|
||||
const byKind = info.kind ? BY_KIND[info.kind] : undefined
|
||||
if (byKind) return byKind(info, /\binput\s*\(/.test(code))
|
||||
if (info.type === "NameError") {
|
||||
if (!info.name)
|
||||
return "用到了一个没有定义过的名字:检查拼写和大小写,或者是不是还没赋值就先用了。"
|
||||
@@ -59,17 +178,21 @@ function explainPython(info: RuntimeErrorInfo) {
|
||||
return `「${info.name}」这个名字没有定义过:检查拼写和大小写,或者是不是还没赋值就先用了。`
|
||||
}
|
||||
if (info.type === "AttributeError") {
|
||||
if (info.name && BUILTIN_FUNCTIONS.has(info.name))
|
||||
return `「${info.name}」不是方法,是函数:要写成 ${info.name}(值),不是 值.${info.name}()。`
|
||||
if (info.suggestion)
|
||||
return `${info.name ? `这个值没有「${info.name}」这个方法` : "方法名写错了"},是不是想写「${info.suggestion}」?`
|
||||
const what = info.name ? `「${info.name}」这个` : "你用的这个"
|
||||
return `这个值没有${what}方法或属性:可能是拼错了,或者这个值的类型不对。`
|
||||
}
|
||||
return (
|
||||
(info.kind && BY_KIND[info.kind]) ||
|
||||
(info.type && BY_TYPE[info.type]) ||
|
||||
"程序运行到这里出错,停下来了。"
|
||||
)
|
||||
return (info.type && BY_TYPE[info.type]) || "程序运行到这里出错,停下来了。"
|
||||
}
|
||||
|
||||
function explainNative(info: RuntimeErrorInfo) {
|
||||
/**
|
||||
* C / C++ 只有信号和退出码。31、25 两个分支是照全库 C 运行时错误重跑补的(2026-09):
|
||||
* 两类占了一半多,原来都只有一句「崩溃了」
|
||||
*/
|
||||
function explainNative(info: RuntimeErrorInfo, code: string) {
|
||||
switch (info.signal) {
|
||||
case 11:
|
||||
return "程序访问了不该访问的内存。常见原因:scanf 的变量前面忘了写 &;数组下标越界了;数组开得太大(可以挪到 main 外面定义)。"
|
||||
@@ -77,13 +200,28 @@ function explainNative(info: RuntimeErrorInfo) {
|
||||
return "整数除以 0 了:除法 / 或者取余 % 的右边是 0。"
|
||||
case 6:
|
||||
return "程序被强制终止了,常见原因是内存访问出错、数组越界,C++ 里也可能是有异常没有处理。"
|
||||
// 判题沙箱拦下的系统调用:system("pause") 要起子进程;字符数组溢出时 glibc 要去开
|
||||
// /dev/tty 报错,也被拦成了这个
|
||||
case 31:
|
||||
return /\bsystem\s*\(/.test(code)
|
||||
? '判题的时候不能用 system(),比如 system("pause"),把这一行删掉。'
|
||||
: "程序被判题机拦下来了。常见原因:字符数组开得太小,装不下输入的内容(字符串的结尾还要多留一个位置),把数组开大一点。"
|
||||
case 25:
|
||||
return "输出的内容太多了:多半是循环停不下来,一直在打印。检查循环条件会不会变成假、循环变量有没有在变。"
|
||||
}
|
||||
if (info.signal) return "程序运行的时候崩溃了。"
|
||||
if (info.exit_code)
|
||||
if (info.exit_code) {
|
||||
if (!/\bint\s+main\s*\(/.test(code))
|
||||
return "main 前面要写 int,写成 int main(),最后再写 return 0;。"
|
||||
return `程序结束时返回的是 ${info.exit_code} 而不是 0:main 函数的最后要写 return 0;`
|
||||
}
|
||||
return "程序运行到一半出错,停下来了。"
|
||||
}
|
||||
|
||||
export function explainRuntimeError(info: RuntimeErrorInfo) {
|
||||
return info.type ? explainPython(info) : explainNative(info)
|
||||
/**
|
||||
* `code` 是学生自己的代码(不含题目模板):Python 用来判断说不说「input() 读进来的是
|
||||
* 文字」,C 用来认出 system() 和没写 int 的 main
|
||||
*/
|
||||
export function explainRuntimeError(info: RuntimeErrorInfo, code: string) {
|
||||
return info.type ? explainPython(info, code) : explainNative(info, code)
|
||||
}
|
||||
@@ -134,13 +134,13 @@ 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 }
|
||||
}
|
||||
case SubmissionStatus.runtime_error: {
|
||||
const rt = info?.runtime_error
|
||||
if (rt) return { line: rt.line ?? null, text: explainRuntimeError(rt), raw: err }
|
||||
if (rt) return { line: rt.line ?? null, text: explainRuntimeError(rt, s.code), raw: err }
|
||||
return { line: null, text: "程序运行到一半出错,停下来了。", raw: err }
|
||||
}
|
||||
case SubmissionStatus.cpu_time_limit_exceeded:
|
||||
|
||||
@@ -92,9 +92,9 @@ export const statisticInfoSchema = z.looseObject({
|
||||
type: z.string().optional(),
|
||||
/** 细分,如 int-parse / index / str-concat,见 runtime-diagnosis.ts 的 KINDS */
|
||||
kind: z.string().optional(),
|
||||
/** NameError / AttributeError 点名的那个名字,只在它出现在学生代码里时才存 */
|
||||
/** NameError / AttributeError / 部分 TypeError 点名的那个名字,只在它出现在学生代码里时才存 */
|
||||
name: z.string().optional(),
|
||||
/** NameError 的「Did you mean」,Python 从作用域里的名字挑的,不来自输入 */
|
||||
/** NameError / 参数名写错的「Did you mean」,Python 从作用域或参数表里挑的,不来自输入 */
|
||||
suggestion: z.string().optional(),
|
||||
/** C / C++:进程收到的信号(11 段错误、8 除零……)和退出码 */
|
||||
signal: z.number().int().optional(),
|
||||
|
||||
Reference in new issue
Block a user