提交前的自动格式化原来每次都先在判题机上编一次(防止 clang-format 把编不过的几行并成 一行)。服务器只有两核、还要和判题抢 CPU,按两核实测:40 人同时交 C,这一步中位 1.8 秒、 最慢 2.7 秒;40 人同时交 C++(iostream),全部撞上 8 秒超时。而且等完之后判题还要再编一遍。 现在先跑 clang-format(十几毫秒),只有它真把学生原来分开的两行并成一行时才去判题机编。 大括号挪到上一行、} else 接上不算并行。拿本机库里的真实提交核过:编得过的 C 只有 1.2%、 C++ 2.1% 还会去编;编不过又被并了行的照样拦住、原样返回。 顺带:格式化不再给 #include 排序、不再把超长的 printf 字符串拆成两截(学生看不懂 "……" "……" 这种写法,它们也是误判的主要来源)。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7881431fa9
commit
797b41751e
2 files changed
+60
-21
No files matched your search
@@ -28,7 +28,7 @@ import {
|
|||||||
requireContestAccess,
|
requireContestAccess,
|
||||||
type ContestEnv,
|
type ContestEnv,
|
||||||
} from "../services/contest"
|
} from "../services/contest"
|
||||||
import { CodeFormatError, formatCode } from "../services/format-code"
|
import { CodeFormatError, formatCode, mergesLines } from "../services/format-code"
|
||||||
import { getBooleanOption } from "../services/options"
|
import { getBooleanOption } from "../services/options"
|
||||||
import { problemSetLockCutoffs } from "../services/problemset"
|
import { problemSetLockCutoffs } from "../services/problemset"
|
||||||
import { consumeToken } from "../services/throttling"
|
import { consumeToken } from "../services/throttling"
|
||||||
@@ -246,28 +246,30 @@ submissionRoutes.post("/submissions/:id/rejudge", requireTeacher, async (c) => {
|
|||||||
submissionRoutes.post("/code/format", requireAuth, async (c) => {
|
submissionRoutes.post("/code/format", requireAuth, async (c) => {
|
||||||
const parsed = await parseBody(c, formatCodeRequestSchema, "Invalid format payload")
|
const parsed = await parseBody(c, formatCodeRequestSchema, "Invalid format payload")
|
||||||
if (!parsed.success) return parsed.response
|
if (!parsed.success) return parsed.response
|
||||||
// C / C++ 编不过就原样返回,不格式化(理由见 judge/run.ts 的 compileFails)。
|
|
||||||
// 代码照常提交、照常判成编译错误,只是不改动学生的代码
|
|
||||||
const { code: source, language, problemId } = parsed.data
|
const { code: source, language, problemId } = parsed.data
|
||||||
if (language === "c" || language === "cpp") {
|
|
||||||
const judgeLanguage = language === "c" ? "C" : "C++"
|
|
||||||
const [problem] =
|
|
||||||
problemId === undefined
|
|
||||||
? []
|
|
||||||
: await db
|
|
||||||
.select({ template: schema.problem.template })
|
|
||||||
.from(schema.problem)
|
|
||||||
.where(eq(schema.problem.id, problemId))
|
|
||||||
.limit(1)
|
|
||||||
const rawTemplate = problem ? templateForLanguage(problem.template, judgeLanguage) : null
|
|
||||||
const template = rawTemplate ? parseProblemTemplate(rawTemplate) : null
|
|
||||||
const full = template ? `${template.prepend}\n${source}\n${template.append}` : source
|
|
||||||
if (await compileFails(judgeLanguage, full)) {
|
|
||||||
return success(c, { code: source } satisfies FormatCodeResponse)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const code = await formatCode(source, language)
|
const code = await formatCode(source, language)
|
||||||
|
// C / C++ 编不过就原样返回,不格式化(理由见 judge/run.ts 的 compileFails)。
|
||||||
|
// 代码照常提交、照常判成编译错误,只是不改动学生的代码。
|
||||||
|
// 只有 clang-format 真把两行并成一行时才去判题机编:判题机和判题抢同一台机器的 CPU,
|
||||||
|
// 全班一起交 C++ 时每人都要排满 8 秒超时;而编得过的代码九成以上根本不并行
|
||||||
|
if ((language === "c" || language === "cpp") && mergesLines(source, code)) {
|
||||||
|
const judgeLanguage = language === "c" ? "C" : "C++"
|
||||||
|
const [problem] =
|
||||||
|
problemId === undefined
|
||||||
|
? []
|
||||||
|
: await db
|
||||||
|
.select({ template: schema.problem.template })
|
||||||
|
.from(schema.problem)
|
||||||
|
.where(eq(schema.problem.id, problemId))
|
||||||
|
.limit(1)
|
||||||
|
const rawTemplate = problem ? templateForLanguage(problem.template, judgeLanguage) : null
|
||||||
|
const template = rawTemplate ? parseProblemTemplate(rawTemplate) : null
|
||||||
|
const full = template ? `${template.prepend}\n${source}\n${template.append}` : source
|
||||||
|
if (await compileFails(judgeLanguage, full)) {
|
||||||
|
return success(c, { code: source } satisfies FormatCodeResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
return success(c, { code } satisfies FormatCodeResponse)
|
return success(c, { code } satisfies FormatCodeResponse)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof CodeFormatError) {
|
if (error instanceof CodeFormatError) {
|
||||||
|
|||||||
@@ -82,6 +82,41 @@ async function checkPythonSyntax(code: string): Promise<string | null | undefine
|
|||||||
return result.exitCode === 1 && result.stdout ? result.stdout : undefined
|
return result.exitCode === 1 && result.stdout ? result.stdout : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 去掉全部空白之后的字符流,以及原来每个换行落在流里的哪个位置(首尾的不算) */
|
||||||
|
function lineBreaks(code: string) {
|
||||||
|
let stream = ""
|
||||||
|
const breaks = new Set<number>()
|
||||||
|
for (const char of code) {
|
||||||
|
if (char === "\n") breaks.add(stream.length)
|
||||||
|
else if (!/\s/.test(char)) stream += char
|
||||||
|
}
|
||||||
|
breaks.delete(0)
|
||||||
|
breaks.delete(stream.length)
|
||||||
|
return { stream, breaks }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* clang-format 有没有把学生原来分开的两行并成一行。并了才值得怀疑代码编不过
|
||||||
|
* (`int n;` 和下一行、少了分号的两行,见 judge/run.ts 的 compileFails)。
|
||||||
|
*
|
||||||
|
* 风格本身就会并的两种不算:`{` 挪到上一行末尾(BreakBeforeBraces: Attach),
|
||||||
|
* `}` 和后面的 `else` / `while` 接上。别的差异(包括 clang-format 动了非空白字符,
|
||||||
|
* 比如排了 #include 的顺序)一律算可疑 —— 宁可多编一次,也不放过一次并行。
|
||||||
|
*/
|
||||||
|
export function mergesLines(original: string, formatted: string) {
|
||||||
|
const before = lineBreaks(original)
|
||||||
|
const after = lineBreaks(formatted)
|
||||||
|
if (before.stream !== after.stream) return true
|
||||||
|
for (const at of before.breaks) {
|
||||||
|
if (after.breaks.has(at)) continue
|
||||||
|
const next = before.stream.slice(at)
|
||||||
|
if (next.startsWith("{")) continue
|
||||||
|
if (before.stream[at - 1] === "}" && /^(else|while)/.test(next)) continue
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
export async function formatCode(code: string, language: "python" | "c" | "cpp" | "sql") {
|
export async function formatCode(code: string, language: "python" | "c" | "cpp" | "sql") {
|
||||||
if (language === "sql") return formatSql(code)
|
if (language === "sql") return formatSql(code)
|
||||||
|
|
||||||
@@ -101,11 +136,13 @@ export async function formatCode(code: string, language: "python" | "c" | "cpp"
|
|||||||
}
|
}
|
||||||
|
|
||||||
const filename = language === "c" ? "code.c" : "code.cpp"
|
const filename = language === "c" ? "code.c" : "code.cpp"
|
||||||
|
// 不排 #include、不拆长字符串:学生的 printf("……") 被拆成 "……" "……" 两截看不懂;
|
||||||
|
// 这两样也是 mergesLines 唯一常见的误报来源(它们改了非空白字符)
|
||||||
const result = await runFormatter(
|
const result = await runFormatter(
|
||||||
[
|
[
|
||||||
config.clangFormatPath,
|
config.clangFormatPath,
|
||||||
`-assume-filename=${filename}`,
|
`-assume-filename=${filename}`,
|
||||||
"-style={BasedOnStyle: LLVM, IndentWidth: 4, BreakBeforeBraces: Attach}",
|
"-style={BasedOnStyle: LLVM, IndentWidth: 4, BreakBeforeBraces: Attach, SortIncludes: Never, BreakStringLiterals: false}",
|
||||||
],
|
],
|
||||||
code,
|
code,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in new issue
Block a user