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/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/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") 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.`, );