feat(编译错误): C / C++ 编译没过时也给中文说明、标出出错位置,中文标点能一键换成英文
Deploy / deploy (push) Canceled after 0s

原来 C / C++ 的编译错误学生看到的是 gcc 的英文原文。现在和 Python 一样一张翻译表
(cError.ts),照全库 2851 条 C / C++ 编译错误用现在的判题机(gcc-14)重新编译出来的
报错定的,只看第一条 error,98% 能给出说明。

三件事先于句式判,因为 gcc 对同一个错报法五花八门:
- 交成了 Python 代码(330 条):gcc 报的是 request for member 之类,直接说换语言;
- C 里写了 C++(iostream、cout);
- 中文标点:gcc-14 不再报 stray,而是把「;」吞进变量名、错报到下一行,所以直接在
  代码里找引号和注释之外的中文标点,行号按代码算。中文标点查找认 C 的 // 和 /* */。

句式上:缺分号报在下一句开头的往回挪一行,把分号写成冒号、关键字拼错(coutinue、
RETURN、return0)、文字没加引号、#include 拼错、main 外面写语句等各有说法。
gcc 的 did you mean 会从库函数里挑(num → enum),只信学生代码里有的名字。
列范围按 gcc 的显示宽度换算(中文占两格),含中文的行也能标到具体位置。

卡片改名 CompileErrorExplain,提交结果、运行例子、提交列表弹框三处都接上了。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-09-30 11:22:31 +08:00
1 parent 4c6f2e7611
commit fa092d334c
9 files changed
+508 -39

No files matched your search

@@ -9,24 +9,34 @@ import {
showErrorMark,
showPunctuationMarks,
} from "oj/problem/utils/errorMark"
import { explainCCompileError } from "oj/problem/utils/cError"
import { explainPythonCompileError } from "oj/problem/utils/pythonError"
/**
* Python 语法错误的中文说明卡片。两处用它:提交前的语法检查(SubmitCode,报错来自
* 服务端的 CPython)和判题回来的编译错误(SubmissionResult,来自判题机)。两边是
* 同一个大版本的 CPython,报错原文一样,所以共用一张翻译表。
* 编译错误的中文说明卡片:Python 的语法错误走 pythonError.ts,C / C++ 的 gcc 报错走
* cError.ts,两张表给出同一种形状。Python 有两处来源:提交前的语法检查(SubmitCode,
* 报错来自服务端的 CPython)和判题回来的编译错误(来自判题机),同一个大版本的
* CPython,报错原文一样。
*
* 挂上就在编辑器里标出位置,卸掉就清掉。
*/
const props = defineProps<{
/** CPython 的报错原文(判题机的 `err_info` 或 `/code/format` 的 syntax-error) */
errInfo: string
}>()
const props = withDefaults(
defineProps<{
/** 编译器的报错原文(判题机的 `err_info`,或 `/code/format` 的 syntax-error) */
errInfo: string
language?: string
}>(),
{ language: "Python" },
)
const codeStore = useCodeStore()
const theme = useThemeVars()
const explanation = computed(() => explainPythonCompileError(props.errInfo, codeStore.code.value))
const explanation = computed(() =>
props.language === "Python"
? explainPythonCompileError(props.errInfo, codeStore.code.value)
: explainCCompileError(props.errInfo, codeStore.code.value, props.language),
)
/** 判题机的临时目录名(`/judger/run/<32 位随机串>/`)对学生没有意义,只剩文件名 */
const rawError = computed(() => props.errInfo.replace(/\/judger\/run\/[^/"]+\//g, ""))
@@ -41,7 +51,7 @@ const otherPunctuationLines = computed(() => {
if (!ex?.punctuation) return []
const code = codeStore.code.value
const lines = new Set<number>()
for (const fix of findChinesePunctuation(code)) {
for (const fix of findChinesePunctuation(code, props.language)) {
lines.add(code.slice(0, fix.from).split("\n").length)
}
lines.delete(ex.line ?? -1)
@@ -50,14 +60,16 @@ const otherPunctuationLines = computed(() => {
// 数的是编辑器里**现在**的代码:学生可能已经自己改掉几处了
const punctuationFixCount = computed(() =>
explanation.value?.punctuation ? findChinesePunctuation(codeStore.code.value).length : 0,
explanation.value?.punctuation
? findChinesePunctuation(codeStore.code.value, props.language).length
: 0,
)
const fixedCount = ref<number | null>(null)
function fixPunctuation() {
const code = currentCode()
if (code === null) return
const fixes = findChinesePunctuation(code)
const fixes = findChinesePunctuation(code, props.language)
if (applyFixes(fixes)) fixedCount.value = fixes.length
}
@@ -69,7 +81,7 @@ watch(
else clearErrorMark()
// 中文标点一处不漏地标出来,和按钮上的「N 处」对得上
const code = ex?.punctuation ? currentCode() : null
if (code !== null) showPunctuationMarks(findChinesePunctuation(code))
if (code !== null) showPunctuationMarks(findChinesePunctuation(code, props.language))
},
{ immediate: true },
)
@@ -21,7 +21,7 @@ import ResultHeader from "./ResultHeader.vue"
// 第一次切到「结果」时才加载:它带着 DataTable 和 Markdown 渲染,只看题不提交的人不用付
const SubmissionResult = defineAsyncComponent(() => import("./SubmissionResult.vue"))
const PythonErrorExplain = defineAsyncComponent(() => import("./PythonErrorExplain.vue"))
const CompileErrorExplain = defineAsyncComponent(() => import("./CompileErrorExplain.vue"))
const SampleRunResult = defineAsyncComponent(() => import("./SampleRunResult.vue"))
const CustomRun = defineAsyncComponent(() => import("./CustomRun.vue"))
const FlowchartResult = defineAsyncComponent(() => import("./FlowchartResult.vue"))
@@ -136,7 +136,7 @@ function pick(segment: Segment) {
<div v-show="!viewingFlowchart && (resultSegment === 'submit' || !canTrial)">
<template v-if="syntaxErrorInfo">
<ResultHeader kind="warning" title="代码有语法错误,还没有交上去" />
<PythonErrorExplain :err-info="syntaxErrorInfo" />
<CompileErrorExplain :err-info="syntaxErrorInfo" />
</template>
<template v-else-if="submission">
<p v-if="formattedBeforeSubmit" class="formatted">
@@ -15,7 +15,7 @@ import SubmissionResultTag from "shared/components/SubmissionResultTag.vue"
import { useProblemStore } from "oj/store/problem"
import { useSubmissionStore } from "oj/store/submission"
import { markdownToText } from "oj/problem/utils/plainText"
import PythonErrorExplain from "./PythonErrorExplain.vue"
import CompileErrorExplain from "./CompileErrorExplain.vue"
import RuntimeErrorExplain from "./RuntimeErrorExplain.vue"
import WrongAnswerExplain from "./WrongAnswerExplain.vue"
import LessonNext from "./LessonNext.vue"
@@ -62,14 +62,11 @@ function stripJudgePath(text: string) {
return text.replace(/\/judger\/run\/[^/"]+\//g, "")
}
/**
* Python 的编译错误交给 PythonErrorExplain 翻成中文。只管 Python:C / C++ 的
* gcc 报错另是一套句式,还没做。
*/
const pythonCompileError = computed(() => {
/** 编译错误交给 CompileErrorExplain 翻成中文:Python、C、C++ 各有一张翻译表 */
const compileError = computed(() => {
const submission = props.submission
if (!submission || submission.result !== SubmissionStatus.compile_error) return ""
if (submission.language !== "Python") return ""
if (!["Python", "C", "C++"].includes(submission.language)) return ""
return submission.statisticInfo?.err_info ?? ""
})
@@ -117,8 +114,8 @@ const showSimilar = computed(
const msg = computed(() => {
if (!props.submission) return ""
// 走 PythonErrorExplain / RuntimeErrorExplain 那两张中文卡片
if (pythonCompileError.value || runtimeError.value) return ""
// 走 CompileErrorExplain / RuntimeErrorExplain 那两张中文卡片
if (compileError.value || runtimeError.value) return ""
let msg = ""
const result = props.submission.result
@@ -318,7 +315,7 @@ const columns: DataTableColumn<JudgeCaseResult>[] = [
<n-flex
vertical
v-if="
pythonCompileError ||
compileError ||
runtimeError ||
sampleCheck ||
msg ||
@@ -326,7 +323,11 @@ const columns: DataTableColumn<JudgeCaseResult>[] = [
submission.statisticInfo?.ast_results?.length
"
>
<PythonErrorExplain v-if="pythonCompileError" :err-info="pythonCompileError" />
<CompileErrorExplain
v-if="compileError"
:err-info="compileError"
:language="submission.language"
/>
<RuntimeErrorExplain v-if="runtimeError" :info="runtimeError" :code="submission.code" />
<WrongAnswerExplain
v-if="sampleCheck"
@@ -1,13 +1,14 @@
<script setup lang="ts">
import { SubmissionStatus } from "utils/constants"
import type { StatisticInfo } from "@oj2/contract"
import PythonErrorExplain from "./PythonErrorExplain.vue"
import CompileErrorExplain from "./CompileErrorExplain.vue"
import RuntimeErrorExplain from "./RuntimeErrorExplain.vue"
/**
* 试跑没跑完时给学生看的中文说明。判题机回来的是英文原文(Traceback、gcc 的报错),
* 学生读不懂,原文收进折叠区。Python 的语法错误是判题机上 CPython 的原文,和提交判出来的
* 一样,直接交给 PythonErrorExplain(中文说明 + 编辑器里标出位置 + 一键换中文标点)。
* 一样,直接交给 CompileErrorExplain(中文说明 + 编辑器里标出位置 + 一键换中文标点),
* C / C++ 的 gcc 报错同理。
*/
const props = defineProps<{
result: SubmissionStatus
@@ -35,11 +36,11 @@ const runtimeExplain = computed(
!!props.runtimeError,
)
const pythonSyntaxError = computed(
const compileError = computed(
() =>
!props.note &&
props.result === SubmissionStatus.compile_error &&
props.language === "Python" &&
["Python", "C", "C++"].includes(props.language) &&
!!props.output,
)
@@ -67,7 +68,7 @@ const rawOutput = computed(() => props.output.replace(/\/judger\/run\/[^/"]+\//g
</script>
<template>
<PythonErrorExplain v-if="pythonSyntaxError" :err-info="output" />
<CompileErrorExplain v-if="compileError" :err-info="output" :language="language" />
<n-flex v-else vertical :size="8">
<RuntimeErrorExplain v-if="runtimeExplain" :info="runtimeError!" :code="code ?? ''" />
<n-alert v-else type="error" :show-icon="false">{{ message }}</n-alert>
+434
View File
@@ -0,0 +1,434 @@
import { findChinesePunctuation } from "./chinesePunctuation"
import { PUNCTUATION_NAMES, type PythonErrorExplanation } from "./pythonError"
/**
* 把 C / C++ 的编译错误(gcc 的原文)翻成中文,和 pythonError.ts 同一个思路:
* 一张规则表,不是通用翻译。
*
* 表是照全库 2851 条 C / C++ 编译错误、用现在的判题机(gcc-14、languages.ts 的编译参数)
* 重新编译出来的报错定的(2026-09),只看**第一条 error**:后面的多半是它连带出来的。
* 判题机的原文长这样:
*
* main.c: In function 'main':
* main.c:17:16: error: expected ';' before ':' token
* 17 | printf("a"):
* | ^
* | ;
*
* 三件事先于句式判,因为同一个错在 gcc 那里的报法五花八门:
* - **交成了 Python 代码**(全库 330 条):gcc 会把 `a = input()` 当成省略了类型的声明,
* 报出来的是 request for member、initializer element is not constant 之类,挨个解释没有意义。
* - **C 里写了 C++**(iostream、cout)。
* - **中文标点**:gcc-14 不再报 `stray '\357'`,而是把「;」吞进前面的名字里(`a;` 成了
* 一个变量名),报错落在下一行、写成 `'scanf\U0000ff08' undeclared`。所以直接在代码里
* 找引号和注释之外的中文标点,行号也按代码里的位置算。
*/
/** 第一条 error(链接错误没有行号,是 `undefined reference to`) */
const FIRST_ERROR_RE =
/^[^\n:]*?:(\d+):(\d+): (?:fatal )?error: (.*)$|undefined reference to `([^']+)'/m
type Context = {
/** gcc 摘录的出错那一行(去了行首空白) */
source: string
/** 报错点名的那个词正好是这一行的开头:`before 'scanf'`,真正少分号的是上一行 */
atLineStart: boolean
code: string
language: string
}
type Rule = [RegExp, (match: RegExpMatchArray, context: Context) => string | null]
/** gcc 把非 ASCII 字符写成 `\U0000ff1b`,还原成字符才能给学生看 */
function unescapeGcc(text: string) {
return text.replace(/\\U([0-9a-f]{8})/g, (_, hex: string) =>
String.fromCodePoint(Number.parseInt(hex, 16)),
)
}
/** 学生最常拼错的几个函数名:print、sacnf、pritnf…… */
const IO_TYPO = /^(pr\w*|sc\w*|sa\w*|sn\w*|pi\w*|pt\w*)$/
const MISSING_SEMICOLON = "这一行末尾少了分号 ;。C 语言每条语句写完都要加分号。"
const PREVIOUS_SEMICOLON = "上一行末尾少了分号 ;。C 语言每条语句写完都要加分号。"
const COLON_FOR_SEMICOLON = "语句的末尾要用分号 ;,这里写成了冒号 :。"
const KEYWORDS = [
"return",
"else",
"continue",
"break",
"while",
"switch",
"case",
"default",
"printf",
"scanf",
"double",
"float",
"char",
"include",
]
const TYPE_NAMES = new Set(["int", "char", "float", "double", "long", "short"])
/** 编辑距离,只用来认关键字拼错(coutinue、retrun、esle),名字都很短 */
function distance(a: string, b: string) {
const row = Array.from({ length: b.length + 1 }, (_, i) => i)
for (let i = 1; i <= a.length; i++) {
let previous = row[0]!
row[0] = i
for (let j = 1; j <= b.length; j++) {
const current = row[j]!
row[j] = Math.min(row[j]! + 1, row[j - 1]! + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1))
previous = current
}
}
return row[b.length]!
}
function keywordTypo(name: string) {
if (name.length < 3) return null
// RETURN、Printf:C 区分大小写
const lower = name.toLowerCase()
if (lower !== name && KEYWORDS.includes(lower))
return { keyword: lower, glued: false, cased: true }
// return0:关键字和后面的值之间少了空格
const glued = KEYWORDS.find((keyword) => name.startsWith(keyword) && name.length > keyword.length)
if (glued && /^\d/.test(name.slice(glued.length)))
return { keyword: glued, glued: true, cased: false }
const keyword = KEYWORDS.find(
(item) => item !== name && distance(item, name) <= (item.length > 5 ? 2 : 1),
)
return keyword ? { keyword, glued: false, cased: false } : null
}
const OUTSIDE_MAIN =
"这些语句写在了 main 外面:printf、scanf 这些都要写在 int main() { 和 } 之间。看看 } 是不是提前结束了 main。"
const RULES: Rule[] = [
// 名字里有中文:要输出的文字忘了加引号(printf(你好)),或者中文写到了代码里
[
/^(?:'[^']*\P{ASCII}[^']*' (?:undeclared|was not declared)|unknown type name '[^']*\P{ASCII})/u,
() =>
'中文只能写在英文双引号里(要输出的文字),或者写在注释里。要原样输出的文字,比如 printf("你好");。',
],
[/^expected (?:';'|',' or ';') before ':' token/, () => COLON_FOR_SEMICOLON],
[/^expected ';' before '(\S+)'/, () => MISSING_SEMICOLON],
// 报在下一句开头(`before 'int'`),真正少分号的是上一句;也可能是几个变量之间少了逗号
[
/^expected ',' or ';' before/,
(_, { atLineStart }) =>
atLineStart
? PREVIOUS_SEMICOLON
: "这一句少了分号或者逗号:一行里定义几个变量,变量之间要用逗号隔开,最后加分号,比如 int a, b;。",
],
[
/^'(\w+)' (?:undeclared|was not declared in this scope)(?:.*did you mean '(\w+)')?/,
([, name, suggestion], { source, code, language }) => {
if (language === "C++" && /^(cout|cin|endl)$/.test(name!))
return `「${name}」要先引入:程序开头写 #include <iostream>,再写 using namespace std;。`
if (language === "C++" && /^(setprecision|setw|fixed)$/.test(name!))
return `用「${name}」要在程序开头加上 #include <iomanip>。`
const typo = keywordTypo(name!)
if (typo?.glued)
return `「${typo.keyword}」和后面的值之间要空一格,写成 ${typo.keyword} ${name!.slice(typo.keyword.length)}。`
if (typo?.cased) return `C 语言区分大小写,「${name}」要写成「${typo.keyword}」。`
if (typo) return `「${name}」拼错了,是不是想写「${typo.keyword}」?`
// printf(Hello) —— 要原样输出的文字没加引号
if (
new RegExp(`\\b(printf|puts)\\s*\\(\\s*${name}\\b|<<\\s*${name}\\b`).test(source) ||
/^[A-Z]{4,}$/.test(name!)
)
return language === "C++"
? '要原样输出的文字得放在英文双引号里,比如 cout << "Hello";。'
: '要原样输出的文字得放在英文双引号里,比如 printf("Hello");。'
// gcc 的「did you mean」会从库函数里挑(num → enum、Hello → ftello),
// 只信学生自己代码里有的名字
if (suggestion && new RegExp(`\\b${suggestion}\\b`).test(code))
return `「${name}」没有定义过,是不是想写「${suggestion}」?`
return `「${name}」这个变量还没定义就用了:先写 int ${name}; 这样的定义,也检查一下拼写和大小写。`
},
],
[
/^expected declaration or statement at end of input|^expected '}' at end of input/,
() => "少写了 }:数一数每个 { 是不是都有对应的 }。",
],
[
/^expected expression before '(.+?)'/,
([, token], { source }) => {
if (/=<|=>/.test(source)) return "小于等于要写成 <=,大于等于要写成 >=,等号放在后面。"
if (token === "else") return "else 前面的 if 写得不完整:看看 if 那一行末尾是不是多写了分号。"
if (TYPE_NAMES.has(token!))
return `「${token}」是类型名,不能当变量用,这里要写你自己起的变量名。`
return "这里少了一个值:运算符、逗号的两边都得有东西,看看是不是多写了符号,或者漏写了变量。"
},
],
[
/^expected '=', ',', ';', 'asm' or '__attribute__' (?:before|at end of input)/,
(_, { source, atLineStart }) => {
if (/^include\b/.test(source)) return "#include 前面的 # 没有写。"
if (atLineStart) return PREVIOUS_SEMICOLON
return "定义变量的这一句写法不对:几个变量之间要用逗号隔开,最后加分号,比如 int a, b;。也看看上一行末尾是不是少了分号。"
},
],
[
/^missing terminating (["']) character/,
() => "引号没有配对:文字要用两个英文双引号包起来,看看是不是少了一个,或者混进了中文引号。",
],
[
/^stray '\\' in program/,
() => '\\n 这类带反斜杠的写法要放在双引号里面,比如 printf("%d\\n", a);。',
],
[
/^stray '#' in program/,
() => "#include 要单独占一行、写在最前面。看看是不是把代码粘贴了两遍。",
],
[
/^stray '/,
() => "这里有一个不能出现在代码里的字符,多半是中文输入法打出来的,删掉后用英文输入法重新打。",
],
[
/^invalid preprocessing directive #(\w+); did you mean #include\?/,
([, name]) => `「#${name}」拼错了,应该是 #include。`,
],
[
/^invalid preprocessing directive/,
() => "# 这一行写错了,引入头文件要写成 #include <stdio.h>。",
],
[
/^(\S+): No such file or directory/,
([, file], { language }) => {
if (language === "C" && /^(iostream|bits\/stdc\+\+\.h|cstdio|cmath|string)$/.test(file!))
return `${file} 是 C++ 的头文件,语言要选 C++。C 语言的输入输出用 #include <stdio.h>。`
return `头文件「${file}」不存在,多半是拼错了:常用的是 stdio.h、math.h、string.h。`
},
],
[/^#include expects/, () => "#include 后面要用尖括号把头文件名括起来:#include <stdio.h>。"],
[
/^'else' without a previous 'if'/,
() => "else 前面没有配对的 if:看看 if 那一行末尾是不是多写了分号,或者 if 的 { } 没有配对。",
],
[
/^invalid operands to binary & /,
() => '格式和变量之间要用逗号隔开,比如 scanf("%d%d", &a, &b)。',
],
[/^invalid operands to binary % /, () => "取余 % 只能用在整数上,小数不能取余。"],
[/^invalid operands to binary/, () => "这两个值不能做这种运算,看看两边的类型对不对。"],
[
/^(unknown type name|'\w+' does not name a type)/,
(match) => {
const name = match.input!.match(/'(\w+)'/)?.[1]
const suggestion = match.input!.match(/did you mean '(\w+)'/)?.[1]
if (name === "string")
return "C 语言没有 string 类型,字符串要用 char 数组,比如 char s[100];。"
if (name === "bool") return "用 bool 要在程序开头加上 #include <stdbool.h>。"
// C++ 的 does not name a type:语句写在函数外面
if (/does not name a type/.test(match.input!)) return OUTSIDE_MAIN
const typo = name ? keywordTypo(name) : null
if (typo && !typo.glued) return `「${name}」拼错了,是不是想写「${typo.keyword}」?`
if (name && suggestion) return `「${name}」不是类型名,是不是想写「${suggestion}」?`
if (name && /^[a-z]\w*$/.test(name) && name.length <= 8)
return `「${name}」不是类型名:常用的类型是 int、float、double、char,也看看上一行末尾是不是少了分号。`
return OUTSIDE_MAIN
},
],
[
/^expected declaration specifiers|^expected identifier or '\(' before|^expected unqualified-id|^expected constructor, destructor, or type conversion/,
(_, { source }) =>
// int main(); 下一行才是 {
source.startsWith("{")
? "上一行末尾多了分号:int main() 后面直接跟 {,不能加分号。"
: OUTSIDE_MAIN,
],
[
/^expected '\)' before/,
(_, { source }) =>
/\b(printf|scanf)\s*\(\s*"[^"]*"\s*[^\s,)]/.test(source)
? '格式和变量之间要用逗号隔开,比如 printf("%d", a)。'
: "括号没有配对,或者括号里几个值之间少了逗号。",
],
[/^expected '\(' before/, () => "if、while、for 后面的条件要用圆括号括起来,比如 if (a > 0)。"],
[/^expected ':' before/, () => "case 后面要用冒号 :,比如 case 1:。"],
[/^empty character constant/, () => "两个单引号中间不能是空的;要输出文字,用双引号。"],
[/^no match for 'operator(<<|>>)'/, () => "cin 要用 >>,cout 要用 <<,方向不能写反。"],
[
/^expected primary-expression before/,
() => "这里少了一个值:比如 cout << 的后面不能空着,运算符的两边都要有东西。",
],
[/^array size missing/, () => "数组要写上大小,比如 int a[10];。"],
[
/^initializer element is not constant/,
() => "main 外面定义的变量只能给固定的值:要用输入或者计算的结果,就把它挪到 main 里面。",
],
[
/^request for member/,
() => "只有结构体才能用点 . 取里面的东西。像 s.split() 这种是 Python 的写法,C 语言里没有。",
],
[/^duplicate case value/, () => "switch 里有两个 case 的值是一样的。"],
[
/^lvalue required as left operand of assignment/,
() => "等号 = 的左边只能是一个变量。判断相等要用两个等号 ==。",
],
[
/^(?:redeclaration|redefinition) of '(\w+)'/,
([, name]) => `「${name}」定义了两次:同一个变量只能定义一次,后面再用的时候直接写名字。`,
],
[
/^invalid suffix "(.+?)" on (?:integer|floating) constant/,
() => "数字后面紧跟着字母了:比如 2x 要写成 2*x;变量名也不能用数字开头。",
],
[
/^too (?:few|many) arguments to function '(\w+)'/,
([, name]) => `调用「${name}」时括号里给的值的个数不对。`,
],
[
/^(break|continue) statement not within loop/,
([, keyword]) => `${keyword} 只能写在循环${keyword === "break" ? "或者 switch " : ""}里面。`,
],
[
/^conflicting types for '(\w+)'/,
([, name]) => `「${name}」前后的类型不一致,或者和自带的函数重名了,换一个名字试试。`,
],
]
function translate(detail: string, context: Context) {
for (const [pattern, render] of RULES) {
const match = detail.match(pattern)
const text = match && render(match, context)
if (text) return text
}
return null
}
/** 没有 #include、却有 print( / input( / def / elif:交的是 Python */
function looksLikePython(code: string) {
if (/\b(printf|scanf|puts|cout|cin)\b/.test(code)) return false
return /\b(print|input)\s*\(|^\s*(def|elif)\b|^\s*for\s+\w+\s+in\b/m.test(code)
}
function looksLikeCpp(code: string) {
return /#\s*include\s*<(iostream|bits\/stdc\+\+\.h)>|\bcout\s*<<|\bcin\s*>>|\busing\s+namespace\b/.test(
code,
)
}
/** 东亚宽字符(中文、全角标点)在 gcc 的列里占两格 */
const WIDE =
/[\u1100-\u115f\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe30-\ufe4f\uff00-\uff60\uffe0-\uffe6]|\p{Extended_Pictographic}/u
/**
* gcc 的 `^` 按显示宽度对齐(-fdiagnostics-column-unit 默认是 display):中文占两格。
* 换算成字符下标;摘录里的 Tab 已经展开成空格,所以不用管 Tab
*/
function displayToIndex(text: string, column: number) {
let width = 0
let index = 0
for (const char of text) {
if (width >= column) return width === column ? index : null
width += WIDE.test(char) ? 2 : 1
index += char.length
}
return width >= column ? index : null
}
/** 第 `line` 行 gcc 摘录的那一行,和 `^` 标的范围 */
function snippet(lines: string[], index: number, line: number) {
for (let i = index + 1; i < Math.min(lines.length, index + 3); i++) {
const match = lines[i]!.match(/^\s*(\d+) \| (.*)$/)
if (!match || Number(match[1]) !== line) continue
const text = match[2]!
const indent = text.length - text.trimStart().length
const source = text.trimStart()
const marks = lines[i + 1]?.match(/^\s*\| (.*)$/)?.[1] ?? ""
const from = marks.indexOf("^")
const width = marks.slice(from).match(/^[\^~]+/)?.[0].length ?? 1
const start = displayToIndex(text, from)
const end = displayToIndex(text, from + width)
const caret =
from >= 0 && start !== null && end !== null && start >= indent
? { from: start - indent, to: Math.max(end, start + 1) - indent }
: null
return { source, caret }
}
return { source: "", caret: null }
}
/** 翻不出来就返回 null,界面照旧显示原文 */
export function explainCCompileError(
errInfo: string,
code: string,
language: string,
): PythonErrorExplanation | null {
const text = errInfo.trim()
const where = text.match(FIRST_ERROR_RE)
if (!where) return null
const base = { sourceLine: null, caret: null, punctuation: null }
if (looksLikePython(code)) {
return {
...base,
line: null,
message: `这是 Python 的写法,可提交时选的语言是 ${language}。要么把语言换成 Python,要么改成 ${language} 的写法:输入输出用 ${language === "C++" ? "cin 和 cout" : "scanf 和 printf"}。`,
}
}
if (!/\bmain\s*\(/.test(code)) {
return {
...base,
line: null,
message: "程序要从 int main() { 开始写,最后是 },这里没有 main。",
}
}
if (language === "C" && looksLikeCpp(code)) {
return {
...base,
line: null,
message: "这是 C++ 的写法(iostream、cout、cin),提交时语言要选 C++。",
}
}
const punctuation = findChinesePunctuation(code, language)[0]
if (punctuation) {
const char = code[punctuation.from]!
const name = PUNCTUATION_NAMES[char] ?? "标点"
return {
...base,
line: code.slice(0, punctuation.from).split("\n").length,
message: `这里的${name}是中文输入法打出来的,要换成英文的${name}。写代码的时候先切换到英文输入法。`,
punctuation: char,
}
}
// 链接错误:函数名拼错、没有 main。没有行号,到代码里找它第一次出现的地方
const linked = where[4]
if (linked) {
if (linked === "main")
return { ...base, line: null, message: "程序要从 int main() 开始写,这里没有找到 main。" }
const at = code.search(new RegExp(`\\b${linked}\\s*\\(`))
return {
...base,
line: at < 0 ? null : code.slice(0, at).split("\n").length,
message: IO_TYPO.test(linked)
? `「${linked}」这个函数不存在,多半是拼错了:输出是 printf,输入是 scanf。`
: `「${linked}」这个函数不存在:检查一下拼写,或者是不是忘了定义它。`,
}
}
let line = Number(where[1])
const detail = unescapeGcc(where[3]!)
const lines = text.split("\n")
const index = lines.findIndex((item) => item.includes(where[0]))
const { source, caret } = snippet(lines, index, line)
const token = detail.match(/before '([^']+)'/)?.[1]
const atLineStart = !!token && source.startsWith(token)
const message = translate(detail, { source, atLineStart, code, language })
if (!message) return null
// 报在下一句开头:往回找到真正少分号的那一行(跳过空行)
if (message === PREVIOUS_SEMICOLON) {
const codeLines = code.split("\n")
let previous = line - 1
while (previous > 1 && !codeLines[previous - 1]?.trim()) previous--
if (previous >= 1) return { ...base, line: previous, message }
}
if (line > code.split("\n").length) line = code.split("\n").length
return { line, sourceLine: caret ? source : null, caret, message, punctuation: null }
}
@@ -21,8 +21,12 @@ export interface PunctuationFix {
* 不是完整的词法分析:只认引号、三引号、字符串前缀、反斜杠转义和 `#` 注释,
* 这对学生代码够用。单引号字符串遇到换行就当它结束,免得一个没配对的引号把
* 后面整篇都当成字符串。
*
* C / C++ 换一套注释:`//` 和 `/* *\/`,`#` 是预处理指令不是注释;没有三引号和前缀。
* 单引号的字符常量(`','`)和字符串一样原样留着。
*/
export function findChinesePunctuation(code: string): PunctuationFix[] {
export function findChinesePunctuation(code: string, language = "Python"): PunctuationFix[] {
const isC = language === "C" || language === "C++"
const fixes: PunctuationFix[] = []
function fix(at: number) {
@@ -68,15 +72,25 @@ export function findChinesePunctuation(code: string): PunctuationFix[] {
let i = 0
while (i < code.length) {
const ch = code[i]!
if (ch === "#") {
if (isC && code.startsWith("//", i)) {
const end = code.indexOf("\n", i)
i = end === -1 ? code.length : end
continue
}
if (isC && code.startsWith("/*", i)) {
const end = code.indexOf("*/", i + 2)
i = end === -1 ? code.length : end + 2
continue
}
if (!isC && ch === "#") {
const end = code.indexOf("\n", i)
i = end === -1 ? code.length : end
continue
}
if (ch === '"' || ch === "'") {
// 前缀是紧挨着引号的 f / r / b / u 组合,前面不能再连着别的标识符字符
const prefix = code.slice(0, i).match(/(?<![\w])[rRbBfFuU]{1,2}$/)?.[0] ?? ""
const quote = code.startsWith(ch.repeat(3), i) ? ch.repeat(3) : ch
const prefix = isC ? "" : (code.slice(0, i).match(/(?<![\w])[rRbBfFuU]{1,2}$/)?.[0] ?? "")
const quote = !isC && code.startsWith(ch.repeat(3), i) ? ch.repeat(3) : ch
i = skipString(i + quote.length, quote.length === 3, /f/i.test(prefix), [quote])
continue
}
+1 -1
View File
@@ -120,7 +120,7 @@ export const useSubmissionStore = defineStore("submission", () => {
} catch (e) {
if (stale()) return
if (errorCode(e) === "syntax-error") {
// 仅 Python 会出现:message 是 CPython 的报错原文,交给 PythonErrorExplain 翻译
// 仅 Python 会出现:message 是 CPython 的报错原文,交给 CompileErrorExplain 翻译
syntaxErrorInfo.value = errorMessage(e)
revealResult()
return
@@ -2,6 +2,7 @@
import { Icon } from "@iconify/vue"
import { useThemeVars } from "naive-ui"
import { getSubmission } from "oj/api"
import { explainCCompileError } from "oj/problem/utils/cError"
import { explainPythonCompileError } from "oj/problem/utils/pythonError"
import { explainRuntimeError } from "oj/problem/utils/runtimeError"
import { useUserStore } from "shared/store/user"
@@ -125,7 +126,7 @@ function stripJudgePath(text: string) {
/**
* 编译失败 / 运行时错误说清楚「错在第几行、为什么」—— 翻译表和题目页结果区同一份
* (pythonError.ts / runtimeError.ts)。原来的看代码弹框压根不显示报错。
* (pythonError.ts / cError.ts / runtimeError.ts)。原来的看代码弹框压根不显示报错。
*/
const explain = computed(() => {
const s = shown.value
@@ -134,7 +135,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, s.code) : null
const ex = !err
? null
: s.language === "Python"
? explainPythonCompileError(err, s.code)
: s.language === "C" || s.language === "C++"
? explainCCompileError(err, s.code, s.language)
: null
if (ex) return { line: ex.line, text: ex.message, raw: err }
return { line: null, text: "编译没通过,看看下面的原始报错。", raw: err }
}
@@ -190,7 +190,7 @@ B 看得到。
|---|---|---|
| 答案错误,例子上就错了 | ✕ 答案错误 · 在例子 N 上就错了 | 「例子 N 没有通过:」+ 中文原因(`WrongAnswerExplain` 那套)+ 输入 / 正确输出 / 你的输出三栏,差异标红 + 「题目要求的输出:……」+ 让 AI 分析 |
| 答案错误,例子都对 | ✕ 答案错误 · 通过 x/y 个测试点 + 进度条 | 「题目里的例子都对了,没通过的是隐藏的测试点……」+ 题目原话 + 让 AI 分析 +「去自己输入试试」 |
| 运行时错误 / 编译错误 / 超时 / 内存超限 | 原样 | `RuntimeErrorExplain` / `PythonErrorExplain` 原样搬进;编辑器标红出错行。超时、内存超限各给一句中文说明(常见原因:死循环、多写了 input;列表开太大、循环里不停往里加)—— 题面不写限制,只能在这里讲 |
| 运行时错误 / 编译错误 / 超时 / 内存超限 | 原样 | `RuntimeErrorExplain` / `CompileErrorExplain` 原样搬进;编辑器标红出错行。超时、内存超限各给一句中文说明(常见原因:死循环、多写了 input;列表开太大、循环里不停往里加)—— 题面不写限制,只能在这里讲 |
| Python 语法没过(没交上去) | ! 代码有语法错误,还没有交上去;分段写「交之前检查」 | 中文说明 +「把中文标点都换成英文(N 处)」+ 折叠的「原始报错(英文)」;**所有**中文标点都在编辑器里标出来,和 N 对得上 |
| 代码要求没满足(AST 未通过) | 答案正确,但语法未通过(现有文案) | 逐条 ✓ / ✗。**按已解决记账,但没有烟花、下一题、点评** —— 和现在一致,要单独一个画面 |
| 答案正确 | ✓ 答案正确 · 所有测试点都对了 | 课堂里:「这节课还剩 N 道」+「下一题:题号 题目」;课堂题都做完:「……都做完了」+「去题单继续练」;不在课堂里:相似题推荐 |
@@ -269,7 +269,7 @@ B 看得到。
1. **提交状态上移到 store**:当前提交、判题跟踪(WebSocket + 轮询)、AI 提示、失败计数、
流程图评分(含 WebSocket 订阅与 3 分钟超时)都不再挂在按钮组件上。结果页签只负责显示。
这是修掉「编辑器一卸载结果就丢」的前提。
2. **错误标记不能跟着页签消失**:`PythonErrorExplain` / `RuntimeErrorExplain` 挂载时标红、卸载时清掉,
2. **错误标记不能跟着页签消失**:`CompileErrorExplain` / `RuntimeErrorExplain` 挂载时标红、卸载时清掉,
原来切到「题目」页签红线就没了。实际做法没挪进 store:「结果」页签第一次打开之后就一直挂着、
只是藏起来(`detail.vue` 的 `resultMounted`),标红跟着结果走,换题时由换题重置清掉。
3. **三种入口用同一个编辑器**:合并 `ProblemEditor` 与 `BasicEditor`,协作、错误标记、中文标点替换