fix(运行时错误): 列表练习里 append() 参数给错,说明却是「把文字当成数字、input() 读进来的是文字」,代码里根本没有 input()
全库 9228 条 Python 运行时错误在判题沙箱里重跑了一遍(9028 条复现),按真实回溯重定归类表:
- TypeError 原来只认 12 种句式,633 条掉进兜底,一律被说成「把文字当成数字」。
实际多半是函数用法错:参数个数不对(点名 append / insert 并给正确写法)、
end= 拼成 enp=、list.index(x) 该写变量名.index(x)、忘写 ()(int(input)、
input[0],约 200 条)、拿 print() / append() 的返回值去算(约 100 条)等。
兜底从 633 条降到 64 条,ValueError 从 336 降到 5(split("")、%.2f 写错、
值不在列表里)。
- input[0] 原来被说成「对一个数字用了下标」:值的类型名比句式更说明问题,
builtin_function_or_method / NoneType 先于句式判。类型名来自代码不来自输入。
- AttributeError 带上 Python 的「Did you mean」(spilt → split 一种就 188 条),
s.len() 说成函数不是方法,数字没有 split() 说顺序反了。
- 「input() 读进来的是文字」只在学生代码里真有 input( 时才说。
- C:信号 31(system("pause") 被沙箱拦 / 字符数组溢出)和 25(死循环打印)
原来只有「崩溃了」,占 C 运行时错误一半多;void main 不再说成「要写 return 0」。
已经存进库的诊断没有新的 kind,旧提交看到的是新的中性兜底话。
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6306b5a919
commit
f413518a85
5 files changed
+260
-36
No files matched your search
@@ -15,31 +15,105 @@ import type { StatisticInfo } from "@oj2/contract"
|
|||||||
* `invalid literal for int() with base 10: '1 2'`,给学生看就等于把隐藏数据放出去 ——
|
* `invalid literal for int() with base 10: '1 2'`,给学生看就等于把隐藏数据放出去 ——
|
||||||
* 故意写 `raise Exception(input())` 就能一个点一个点地套。所以只存:
|
* 故意写 `raise Exception(input())` 就能一个点一个点地套。所以只存:
|
||||||
* - 异常类型和归好类的 `kind`(按消息的**句式**判,不取消息里的值)
|
* - 异常类型和归好类的 `kind`(按消息的**句式**判,不取消息里的值)
|
||||||
* - NameError / AttributeError 点名的名字,而且只在它**原样出现在学生代码里**时才存
|
* - NameError / AttributeError / 部分 TypeError 点名的名字,而且只在它**原样出现在
|
||||||
|
* 学生代码里**时才存
|
||||||
*/
|
*/
|
||||||
export type RuntimeErrorInfo = NonNullable<StatisticInfo["runtime_error"]>
|
export type RuntimeErrorInfo = NonNullable<StatisticInfo["runtime_error"]>
|
||||||
|
|
||||||
const IDENTIFIER = /^[A-Za-z_]\w*$/
|
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][]> = {
|
const KINDS: Record<string, [RegExp, string][]> = {
|
||||||
ValueError: [
|
ValueError: [
|
||||||
[/^invalid literal for int\(\)/, "int-parse"],
|
[/^invalid literal for int\(\)/, "int-parse"],
|
||||||
[/^could not convert string to float/, "float-parse"],
|
[/^could not convert string to float/, "float-parse"],
|
||||||
[/^(not enough|too many) values to unpack/, "unpack"],
|
[/^(not enough|too many) values to unpack/, "unpack"],
|
||||||
[/^math domain error/, "math-domain"],
|
[/^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: [
|
TypeError: [
|
||||||
|
...VALUE_KINDS,
|
||||||
[/^can only concatenate str/, "str-concat"],
|
[/^can only concatenate str/, "str-concat"],
|
||||||
[/^unsupported operand type/, "operand"],
|
[/^unsupported operand type/, "operand"],
|
||||||
[/cannot be interpreted as an integer/, "not-int"],
|
[/cannot be interpreted as an integer/, "not-int"],
|
||||||
[/not supported between instances of/, "compare"],
|
[/not supported between instances of/, "compare"],
|
||||||
[/object is not callable/, "not-callable"],
|
[/object is not callable/, "not-callable"],
|
||||||
|
[/^type '\w+' is not subscriptable/, "type-subscript"],
|
||||||
[/object is not subscriptable/, "not-subscriptable"],
|
[/object is not subscriptable/, "not-subscriptable"],
|
||||||
[/missing \d+ required positional argument/, "missing-arg"],
|
[/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"],
|
[/^(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"]],
|
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())`) */
|
/** 名字在学生自己的代码里原样出现过才算数,否则它可能来自输入(`eval(input())`) */
|
||||||
@@ -94,9 +168,21 @@ export function parsePythonTraceback(
|
|||||||
if (name) info.name = name
|
if (name) info.name = name
|
||||||
const suggestion = message.match(/Did you mean: '([^']+)'\?/)?.[1]
|
const suggestion = message.match(/Did you mean: '([^']+)'\?/)?.[1]
|
||||||
if (suggestion && IDENTIFIER.test(suggestion)) info.suggestion = suggestion
|
if (suggestion && IDENTIFIER.test(suggestion)) info.suggestion = suggestion
|
||||||
} else if (type === "AttributeError") {
|
// 用了 math.sqrt 却没 import math。Python 同时给了「Did you mean」时也是这个优先
|
||||||
const name = nameFromCode(message.match(/has no attribute '([^']+)'/)?.[1], code)
|
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
|
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
|
return info
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const props = defineProps<{
|
|||||||
code: string
|
code: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const message = computed(() => explainRuntimeError(props.info))
|
const message = computed(() => explainRuntimeError(props.info, props.code))
|
||||||
|
|
||||||
const sourceLine = computed(() => {
|
const sourceLine = computed(() => {
|
||||||
const line = props.info.line
|
const line = props.info.line
|
||||||
|
|||||||
@@ -10,32 +10,149 @@ import type { StatisticInfo } from "@oj2/contract"
|
|||||||
*/
|
*/
|
||||||
export type RuntimeErrorInfo = NonNullable<StatisticInfo["runtime_error"]>
|
export type RuntimeErrorInfo = NonNullable<StatisticInfo["runtime_error"]>
|
||||||
|
|
||||||
/** 说得最多的一句:input() 读进来的是文字。学生栽在类型上,十有八九是这个 */
|
/**
|
||||||
|
* 说得最多的一句:input() 读进来的是文字。只在代码里真有 input( 时才说 —— 列表练习题
|
||||||
|
* 里的类型错误压根没读输入,硬说这句等于指错方向(题目模板里的 input() 看不到,不算)
|
||||||
|
*/
|
||||||
const INPUT_IS_TEXT = "input() 读进来的是文字,要计算先用 int() 或 float() 转成数字。"
|
const INPUT_IS_TEXT = "input() 读进来的是文字,要计算先用 int() 或 float() 转成数字。"
|
||||||
|
|
||||||
const BY_KIND: Record<string, string> = {
|
/** 参数个数不对时,按函数补一句正确用法。都是学生最常用错的几个 */
|
||||||
"int-parse":
|
const ARG_TIPS: Record<string, string> = {
|
||||||
"把输入转成整数的时候失败了:读到的内容不是一个整数。常见原因:一行里输入了好几个数,要先用 split() 拆开再转换;或者输入的是小数,要用 float()。",
|
append:
|
||||||
"float-parse":
|
'append() 一次只能加一个值;要一次加好几个,用 extend(),把它们放进一个列表:extend(["甲", "乙"])。',
|
||||||
"把输入转成数字的时候失败了:读到的内容不是一个数。常见原因:一行里输入了好几个数,要先用 split() 拆开再转换。",
|
extend: 'extend() 括号里只放一个列表,比如 extend(["甲", "乙"]),不是把几个值分开写。',
|
||||||
unpack:
|
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() 要求这一行正好有两个值,对照题目的输入格式看看一行里有几个数。",
|
"等号左边的变量个数,和右边拆出来的值的个数对不上。比如 a, b = input().split() 要求这一行正好有两个值,对照题目的输入格式看看一行里有几个数。",
|
||||||
"math-domain": "数学函数拿到了不合法的值,比如给负数开平方。",
|
"math-domain": () => "数学函数拿到了不合法的值,比如给负数开平方。",
|
||||||
"str-concat": `文字和数字不能直接用 + 连在一起。${INPUT_IS_TEXT}要拼成一句话,就用 str() 把数字转成文字。`,
|
"empty-sep": () =>
|
||||||
operand: `这两个值的类型不能做这种运算,多半是拿文字和数字一起算。${INPUT_IS_TEXT}`,
|
"split() 的括号里不能放空的 '':要按空格拆,括号里什么都不写,就是 split();要把文字拆成一个个字,用 list(文字)。",
|
||||||
"not-int":
|
"not-in-list": () =>
|
||||||
"这里需要一个整数,给的却是文字或小数。比如 range() 里要放整数,input() 读进来的要先用 int() 转换。",
|
"要找或者要删的值不在列表里:index() 和 remove() 只能用列表里真有的值。可以先用 in 判断一下,比如 if x in line:。",
|
||||||
compare: `文字和数字不能比大小。${INPUT_IS_TEXT}`,
|
"format-spec": () =>
|
||||||
"not-callable":
|
"保留小数的格式写错了:要写成 %.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 这类名字当了变量名。",
|
"把一个不是函数的东西当成函数用了。常见原因:乘法漏写了 *(比如 2(a+b) 要写成 2*(a+b)),或者拿 print、input、sum 这类名字当了变量名。",
|
||||||
"not-subscriptable": "对一个数字用了下标 [ ]。只有列表、字符串这类值才能用 [ ] 取出其中一个。",
|
"not-subscriptable": () =>
|
||||||
"missing-arg": "调用函数的时候少给了参数。",
|
"对一个数字用了下标 [ ]。只有列表、字符串这类值才能用 [ ] 取出其中一个。",
|
||||||
"index-type": "下标要用整数。input() 读进来的是文字,要先用 int() 转换。",
|
"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> = {
|
const BY_TYPE: Record<string, string> = {
|
||||||
ValueError: "这里用到的值不符合要求。",
|
ValueError: "这里用到的值不符合要求。",
|
||||||
TypeError: `这里的值类型不对,比如把文字当成数字来用。${INPUT_IS_TEXT}`,
|
TypeError:
|
||||||
|
"这一行里有个值的类型不对,或者某个函数的用法不对,对照一下每个值是什么、函数该怎么用。",
|
||||||
IndexError:
|
IndexError:
|
||||||
"下标越界了:要取的位置超出了列表或字符串的长度。下标从 0 开始,长度是 n 的话,最后一个是 [n-1]。",
|
"下标越界了:要取的位置超出了列表或字符串的长度。下标从 0 开始,长度是 n 的话,最后一个是 [n-1]。",
|
||||||
UnboundLocalError: "函数里的这个变量还没赋值就先用了。",
|
UnboundLocalError: "函数里的这个变量还没赋值就先用了。",
|
||||||
@@ -50,7 +167,9 @@ const BY_TYPE: Record<string, string> = {
|
|||||||
OverflowError: "算出来的数太大了,超出了能表示的范围。",
|
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.type === "NameError") {
|
||||||
if (!info.name)
|
if (!info.name)
|
||||||
return "用到了一个没有定义过的名字:检查拼写和大小写,或者是不是还没赋值就先用了。"
|
return "用到了一个没有定义过的名字:检查拼写和大小写,或者是不是还没赋值就先用了。"
|
||||||
@@ -59,17 +178,21 @@ function explainPython(info: RuntimeErrorInfo) {
|
|||||||
return `「${info.name}」这个名字没有定义过:检查拼写和大小写,或者是不是还没赋值就先用了。`
|
return `「${info.name}」这个名字没有定义过:检查拼写和大小写,或者是不是还没赋值就先用了。`
|
||||||
}
|
}
|
||||||
if (info.type === "AttributeError") {
|
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}」这个` : "你用的这个"
|
const what = info.name ? `「${info.name}」这个` : "你用的这个"
|
||||||
return `这个值没有${what}方法或属性:可能是拼错了,或者这个值的类型不对。`
|
return `这个值没有${what}方法或属性:可能是拼错了,或者这个值的类型不对。`
|
||||||
}
|
}
|
||||||
return (
|
return (info.type && BY_TYPE[info.type]) || "程序运行到这里出错,停下来了。"
|
||||||
(info.kind && BY_KIND[info.kind]) ||
|
|
||||||
(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) {
|
switch (info.signal) {
|
||||||
case 11:
|
case 11:
|
||||||
return "程序访问了不该访问的内存。常见原因:scanf 的变量前面忘了写 &;数组下标越界了;数组开得太大(可以挪到 main 外面定义)。"
|
return "程序访问了不该访问的内存。常见原因:scanf 的变量前面忘了写 &;数组下标越界了;数组开得太大(可以挪到 main 外面定义)。"
|
||||||
@@ -77,13 +200,28 @@ function explainNative(info: RuntimeErrorInfo) {
|
|||||||
return "整数除以 0 了:除法 / 或者取余 % 的右边是 0。"
|
return "整数除以 0 了:除法 / 或者取余 % 的右边是 0。"
|
||||||
case 6:
|
case 6:
|
||||||
return "程序被强制终止了,常见原因是内存访问出错、数组越界,C++ 里也可能是有异常没有处理。"
|
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.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 `程序结束时返回的是 ${info.exit_code} 而不是 0:main 函数的最后要写 return 0;`
|
||||||
|
}
|
||||||
return "程序运行到一半出错,停下来了。"
|
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)
|
||||||
}
|
}
|
||||||
@@ -140,7 +140,7 @@ const explain = computed(() => {
|
|||||||
}
|
}
|
||||||
case SubmissionStatus.runtime_error: {
|
case SubmissionStatus.runtime_error: {
|
||||||
const rt = info?.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 }
|
return { line: null, text: "程序运行到一半出错,停下来了。", raw: err }
|
||||||
}
|
}
|
||||||
case SubmissionStatus.cpu_time_limit_exceeded:
|
case SubmissionStatus.cpu_time_limit_exceeded:
|
||||||
|
|||||||
@@ -92,9 +92,9 @@ export const statisticInfoSchema = z.looseObject({
|
|||||||
type: z.string().optional(),
|
type: z.string().optional(),
|
||||||
/** 细分,如 int-parse / index / str-concat,见 runtime-diagnosis.ts 的 KINDS */
|
/** 细分,如 int-parse / index / str-concat,见 runtime-diagnosis.ts 的 KINDS */
|
||||||
kind: z.string().optional(),
|
kind: z.string().optional(),
|
||||||
/** NameError / AttributeError 点名的那个名字,只在它出现在学生代码里时才存 */
|
/** NameError / AttributeError / 部分 TypeError 点名的那个名字,只在它出现在学生代码里时才存 */
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
/** NameError 的「Did you mean」,Python 从作用域里的名字挑的,不来自输入 */
|
/** NameError / 参数名写错的「Did you mean」,Python 从作用域或参数表里挑的,不来自输入 */
|
||||||
suggestion: z.string().optional(),
|
suggestion: z.string().optional(),
|
||||||
/** C / C++:进程收到的信号(11 段错误、8 除零……)和退出码 */
|
/** C / C++:进程收到的信号(11 段错误、8 除零……)和退出码 */
|
||||||
signal: z.number().int().optional(),
|
signal: z.number().int().optional(),
|
||||||
|
|||||||
Reference in new issue
Block a user