From 535c4ba6872e1ec3dadd5929f4666ca701ce990a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BD=A5=E8=AA=A0=20=E5=8A=89?= Date: Wed, 21 Oct 2026 21:05:46 +0800 Subject: [PATCH 1/2] Refuse input integers outside 32 bits A candidate case was typed as an integer whenever Number.isInteger held, but the C, C++ and Java harnesses write every integer into an int, so 3000000000 reached C and C++ as -1294967296 and made javac refuse the whole run with a note blaming the signature. Integers and the integer values of lists, trees and random lists are now bounded to 32 bits when the case is added, the same for every language, and the range is named only when it is the sole fault. Closes #387 --- tests/browser/runners.test.js | 181 ++++++++++++++++++++++++++++++++++ web/runners.js | 46 ++++++--- 2 files changed, 214 insertions(+), 13 deletions(-) diff --git a/tests/browser/runners.test.js b/tests/browser/runners.test.js index 4ccaa764c..ae1dd9aee 100644 --- a/tests/browser/runners.test.js +++ b/tests/browser/runners.test.js @@ -32,6 +32,187 @@ test("a malformed candidate case is refused", () => { ); }); +test("a candidate integer outside the 32-bit range is refused", () => { + const spec = { + kind: "function", + paramNames: [ + "count", + "nums", + "grid", + "rows", + "head", + "lists", + "root", + "next", + "random", + ], + paramTypes: [ + "integer", + "integer[]", + "integer[][]", + "list>", + "ListNode", + "ListNode[]", + "TreeNode", + "Node", + "Node", + ], + argTypes: [ + null, + null, + null, + null, + "linkedList", + "linkedListArray", + "binaryTree", + "nextTree", + "randomList", + ], + }; + const place = (value) => [ + value, + [value], + [[value]], + [[value]], + [value], + [[value]], + [value], + [value], + [[value, null]], + ]; + for (const bound of [2147483647, -2147483648]) { + const input = place(bound); + assert.deepEqual(parseCandidateCase(spec, JSON.stringify(input)), input); + } + for (const outside of [2147483648, -2147483649]) { + for (const [index, name] of spec.paramNames.entries()) { + const input = place(0); + input[index] = place(outside)[index]; + assert.throws( + () => parseCandidateCase(spec, JSON.stringify(input)), + new RegExp( + `Parameter ${index + 1} \\(${name}\\) holds an integer outside -2147483648 to 2147483647\\.`, + ), + ); + } + } + const queue = { + kind: "class", + className: "EventQueue", + cases: [ + { + input: [ + ["EventQueue", "push"], + [[2], [7]], + ], + }, + ], + }; + const cursor = { + kind: "class", + className: "OrderedCursor", + constructorArgTypes: ["binaryTree"], + cases: [ + { + input: [ + ["OrderedCursor", "next"], + [[[7]], []], + ], + }, + ], + }; + for (const [judge, operation, input] of [ + [ + queue, + "push", + (value) => [ + ["EventQueue", "push"], + [[2], [value]], + ], + ], + [ + queue, + "EventQueue", + (value) => [ + ["EventQueue", "push"], + [[value], [7]], + ], + ], + [cursor, "OrderedCursor", (value) => [["OrderedCursor"], [[[value]]]]], + ]) { + for (const bound of [2147483647, -2147483648]) { + assert.deepEqual( + parseCandidateCase(judge, JSON.stringify(input(bound))), + input(bound), + ); + } + for (const outside of [2147483648, -2147483649]) { + assert.throws( + () => parseCandidateCase(judge, JSON.stringify(input(outside))), + new RegExp( + `Arguments for operation ${operation} hold an integer outside -2147483648 to 2147483647\\.`, + ), + ); + } + } + // The range is named only when it is the one fault: a number given for a + // string, or a string for an integer, is still a type mismatch. + assert.throws( + () => parseCandidateCase(queue, '[["EventQueue", "push"], [[2], ["x"]]]'), + /Arguments for operation push do not match its parameter types\./, + ); + assert.throws( + () => + parseCandidateCase( + { kind: "function", paramNames: ["s"], paramTypes: ["string"] }, + "[3000000000]", + ), + /Parameter 1 \(s\) must match string\./, + ); +}); + +test("a cycle position, node reference and graph neighbor keep their checks", () => { + const spec = { + kind: "function", + paramNames: ["head", "pos", "root", "node", "graph"], + paramTypes: ["ListNode", "integer", "TreeNode", "TreeNode", "Node"], + argTypes: [ + "linkedList", + "cyclePos", + "binaryTree", + "treeNodeValue", + "graphNode", + ], + cyclePosParam: 1, + nodeRefRootParam: 2, + }; + for (const [input, refusal] of [ + [ + [[1], 2147483648, [1], 1, [[]]], + /Parameter 2 must name a list position or -1\./, + ], + [ + [[1], -1, [1], 2147483648, [[]]], + /Parameter 4 must name a value in the tree\./, + ], + // Membership bounds a node reference only because the tree's own values + // are bounded now: before, both this tree and the reference into it passed. + [ + [[1], -1, [2147483648], 2147483648, [[]]], + /Parameter 3 \(root\) holds an integer outside -2147483648 to 2147483647\./, + ], + [ + [[1], -1, [1], 1, [[2147483648]]], + /Parameter 5 \(graph\) must match graphNode\./, + ], + ]) { + assert.throws( + () => parseCandidateCase(spec, JSON.stringify(input)), + refusal, + ); + } +}); + test("a candidate case is typed by paramTypes", () => { const spec = { kind: "function", diff --git a/web/runners.js b/web/runners.js index c5d343657..8bd938499 100644 --- a/web/runners.js +++ b/web/runners.js @@ -73,6 +73,8 @@ export function parseCandidateCase(spec, input) { const required = spec.argTypes?.[index] ?? type; if (!candidateTypeMatches(args[index], required)) { const name = names[index] ? ` (${names[index]})` : ""; + if (candidateTypeMatches(args[index], required, Number.isInteger)) + throw new Error(`Parameter ${index + 1}${name} holds ${OUT_OF_INT32}.`); throw new Error(`Parameter ${index + 1}${name} must match ${required}.`); } } @@ -120,7 +122,19 @@ function reachableTreeValues(values) { return reachable; } -function candidateTypeMatches(value, type) { +/// The C, C++ and Java harnesses write every `integer` into an `int`: past +/// this range C wraps it, C++ wraps it or refuses to compile, and javac +/// refuses the whole run. +const OUT_OF_INT32 = "an integer outside -2147483648 to 2147483647"; + +function isInt32(value) { + return Number.isInteger(value) && value >= -(2 ** 31) && value < 2 ** 31; +} + +/// A caller that refuses a case asks again with `Number.isInteger` as +/// `isInteger`: a value that passes then is wrong only by its range, and the +/// refusal says so rather than naming a type the value already has. +function candidateTypeMatches(value, type, isInteger = isInt32) { // Refuse rather than admit: a judge whose type is not a name is a judge this // cannot speak for, and `type.endsWith` below would throw on it anyway. if (typeof type !== "string") return false; @@ -131,32 +145,34 @@ function candidateTypeMatches(value, type) { if (type.endsWith("[]")) return ( Array.isArray(value) && - value.every((item) => candidateTypeMatches(item, type.slice(0, -2))) + value.every((item) => + candidateTypeMatches(item, type.slice(0, -2), isInteger), + ) ); const list = /^list<(.*)>$/.exec(type); if (list) return ( Array.isArray(value) && - value.every((item) => candidateTypeMatches(item, list[1])) + value.every((item) => candidateTypeMatches(item, list[1], isInteger)) ); - if (type === "integer") return Number.isInteger(value); + if (type === "integer") return isInteger(value); if (type === "double" || type === "number") return Number.isFinite(value); if (type === "string") return typeof value === "string"; if (type === "boolean") return typeof value === "boolean"; if (type === "character") return typeof value === "string" && [...value].length === 1; if (["linkedList", "ListNode"].includes(type)) - return Array.isArray(value) && value.every(Number.isInteger); + return Array.isArray(value) && value.every(isInteger); if (["binaryTree", "nextTree", "TreeNode"].includes(type)) { return ( Array.isArray(value) && - value.every((item) => item === null || Number.isInteger(item)) + value.every((item) => item === null || isInteger(item)) ); } if (type === "linkedListArray") return ( Array.isArray(value) && - value.every((item) => candidateTypeMatches(item, "linkedList")) + value.every((item) => candidateTypeMatches(item, "linkedList", isInteger)) ); if (type === "randomList") { return ( @@ -165,7 +181,7 @@ function candidateTypeMatches(value, type) { (item) => Array.isArray(item) && item.length === 2 && - Number.isInteger(item[0]) && + isInteger(item[0]) && (item[1] === null || (Number.isInteger(item[1]) && item[1] >= 0 && @@ -258,13 +274,17 @@ function parseClassCandidateCase(spec, input) { `Operation ${operation} does not accept ${args.length} arguments.`, ); } - if ( - !sameArity.some((types) => + const matches = (isInteger) => + sameArity.some((types) => types.every((type, argument) => - candidateTypeMatches(args[argument], type), + candidateTypeMatches(args[argument], type, isInteger), ), - ) - ) { + ); + if (!matches(isInt32)) { + if (matches(Number.isInteger)) + throw new Error( + `Arguments for operation ${operation} hold ${OUT_OF_INT32}.`, + ); throw new Error( `Arguments for operation ${operation} do not match its parameter types.`, ); From 7b2ce549e1ae3cfb4264a8079836b85110401b72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BD=A5=E8=AA=A0=20=E5=8A=89?= Date: Wed, 21 Oct 2026 22:29:55 +0800 Subject: [PATCH 2/2] Write whole-number doubles as double literals nativeLiteral wrote a double as String(value), so 2147483648 came out with no point. javac reads such a numeral as an int literal whatever it is assigned to and refuses one past the int range, so the whole run failed with the signature note. A double whose text has no point or exponent now gets ".0", which C, C++ and Java all read as the same double. --- tests/browser/compiler-explorer.test.js | 44 +++++++++++++++++++++++++ web/compiler-explorer.js | 8 ++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/tests/browser/compiler-explorer.test.js b/tests/browser/compiler-explorer.test.js index a024e535b..95eb1d4ab 100644 --- a/tests/browser/compiler-explorer.test.js +++ b/tests/browser/compiler-explorer.test.js @@ -89,6 +89,13 @@ test("literal emitter covers every non-void function type in the bank", () => { nativeLiteral([1, null, 2], "TreeNode", "java"), "treeNode(new Integer[]{1, null, 2})", ); + assert.equal(nativeLiteral(2147483648, "double", "java"), "2147483648.0"); + assert.equal(nativeLiteral(1.5, "double", "java"), "1.5"); + assert.equal(nativeLiteral(1e21, "double", "java"), "1e+21"); + assert.equal( + nativeLiteral([2, 0.5], "double[]", "java"), + "new double[]{2.0, 0.5}", + ); for (const type of bankFunctionTypes()) { if (type === "void" || type === "Node") continue; @@ -637,6 +644,43 @@ public: }, ); + // 2147483648 is a valid double but too large for an int, and javac refuses + // it as an int literal even when the variable it initializes is a double. + it( + "Java harness passes a whole-number double past the int range", + { skip: noJava }, + async () => { + const dir = mkdtempSync(join(tmpdir(), "codetrial-java-double-")); + try { + const source = join(dir, "Main.java"); + const spec = judges["powx-n"]; + writeFileSync( + source, + generateHarness( + "java", + { + ...spec, + cases: [{ label: "Your case 1", input: [2147483648, 1] }], + }, + `class Solution { + public double ${spec.entry}(double x, int n) { return x; } +}`, + ), + ); + await run("javac", [source]); + assert.deepEqual( + parseCompilerResults( + (await run("java", ["-cp", dir, "Main"], { encoding: "utf8" })) + .stdout, + ).results.map((result) => result.actual), + [2147483648], + ); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }, + ); + it( "C++ Construct Quad Tree harness can produce passing Compiler Explorer JSON", { skip: noGpp }, diff --git a/web/compiler-explorer.js b/web/compiler-explorer.js index ce0840bc8..0586f8605 100644 --- a/web/compiler-explorer.js +++ b/web/compiler-explorer.js @@ -70,7 +70,13 @@ export function nativeLiteral(value, type, language) { if (type === "string") return JSON.stringify(value); if (type === "character") return charLiteral(value); if (type === "boolean") return value ? "true" : "false"; - if (type === "integer" || type === "double") return String(value); + if (type === "integer") return String(value); + // javac types a numeral without point or exponent as int even for a double. + // From 1e21 String() writes an exponent, which a trailing ".0" would break. + if (type === "double") { + const text = String(value); + return /[.e]/.test(text) ? text : `${text}.0`; + } if (type === "ListNode") return helperCall("listNode", value, "integer[]", language); if (type === "Node")