diff --git a/apps/api/src/judge/runtime-diagnosis.ts b/apps/api/src/judge/runtime-diagnosis.ts index b4f45b8..8eaa416 100644 --- a/apps/api/src/judge/runtime-diagnosis.ts +++ b/apps/api/src/judge/runtime-diagnosis.ts @@ -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 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 = { 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 = { + "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 } diff --git a/apps/web/src/oj/problem/components/RuntimeErrorExplain.vue b/apps/web/src/oj/problem/components/RuntimeErrorExplain.vue index 8ceb056..3dbb6f4 100644 --- a/apps/web/src/oj/problem/components/RuntimeErrorExplain.vue +++ b/apps/web/src/oj/problem/components/RuntimeErrorExplain.vue @@ -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 diff --git a/apps/web/src/oj/problem/utils/runtimeError.ts b/apps/web/src/oj/problem/utils/runtimeError.ts index c637c16..554cc61 100644 --- a/apps/web/src/oj/problem/utils/runtimeError.ts +++ b/apps/web/src/oj/problem/utils/runtimeError.ts @@ -10,32 +10,149 @@ import type { StatisticInfo } from "@oj2/contract" */ export type RuntimeErrorInfo = NonNullable -/** 说得最多的一句:input() 读进来的是文字。学生栽在类型上,十有八九是这个 */ +/** + * 说得最多的一句:input() 读进来的是文字。只在代码里真有 input( 时才说 —— 列表练习题 + * 里的类型错误压根没读输入,硬说这句等于指错方向(题目模板里的 input() 看不到,不算) + */ const INPUT_IS_TEXT = "input() 读进来的是文字,要计算先用 int() 或 float() 转成数字。" -const BY_KIND: Record = { - "int-parse": - "把输入转成整数的时候失败了:读到的内容不是一个整数。常见原因:一行里输入了好几个数,要先用 split() 拆开再转换;或者输入的是小数,要用 float()。", - "float-parse": - "把输入转成数字的时候失败了:读到的内容不是一个数。常见原因:一行里输入了好几个数,要先用 split() 拆开再转换。", - unpack: +/** 参数个数不对时,按函数补一句正确用法。都是学生最常用错的几个 */ +const ARG_TIPS: Record = { + 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 = { + "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 = { ValueError: "这里用到的值不符合要求。", - TypeError: `这里的值类型不对,比如把文字当成数字来用。${INPUT_IS_TEXT}`, + TypeError: + "这一行里有个值的类型不对,或者某个函数的用法不对,对照一下每个值是什么、函数该怎么用。", IndexError: "下标越界了:要取的位置超出了列表或字符串的长度。下标从 0 开始,长度是 n 的话,最后一个是 [n-1]。", UnboundLocalError: "函数里的这个变量还没赋值就先用了。", @@ -50,7 +167,9 @@ const BY_TYPE: Record = { 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) } diff --git a/apps/web/src/oj/submission/components/SubmissionPane.vue b/apps/web/src/oj/submission/components/SubmissionPane.vue index 52272de..d105e68 100644 --- a/apps/web/src/oj/submission/components/SubmissionPane.vue +++ b/apps/web/src/oj/submission/components/SubmissionPane.vue @@ -140,7 +140,7 @@ const explain = computed(() => { } 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: diff --git a/packages/contract/src/submission.ts b/packages/contract/src/submission.ts index 71bc6b4..470aeb1 100644 --- a/packages/contract/src/submission.ts +++ b/packages/contract/src/submission.ts @@ -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(),