Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions tests/browser/compiler-explorer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 },
Expand Down
181 changes: 181 additions & 0 deletions tests/browser/runners.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<list<integer>>",
"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",
Expand Down
8 changes: 7 additions & 1 deletion web/compiler-explorer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
46 changes: 33 additions & 13 deletions web/runners.js
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Comment thread
hankliuliu marked this conversation as resolved.
throw new Error(`Parameter ${index + 1}${name} holds ${OUT_OF_INT32}.`);
throw new Error(`Parameter ${index + 1}${name} must match ${required}.`);
}
}
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Comment thread
hankliuliu marked this conversation as resolved.
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 (
Expand All @@ -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 &&
Expand Down Expand Up @@ -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.`,
);
Expand Down