From 4ed1a15364d7524c87bee8b437098f6e9494d578 Mon Sep 17 00:00:00 2001 From: papaya0112 Date: Thu, 22 Oct 2026 16:40:38 +0800 Subject: [PATCH 1/3] Refuse a copied random list that loops back Every random-list serializer stopped at the first revisited node, so a deep copy whose last node links back to the first printed as the acyclic list it loops over, and the exact checker accepted it in C++, Java, JavaScript and Python. C walked the ring until its 10000-node buffer filled and failed without saying why. Report the revisit with the message the plain list serializers already use; the seen set stays so serialization still terminates. --- tests/browser/compiler-explorer.test.js | 107 ++++++++++++++++++++++++ tests/browser/runners.test.js | 24 ++++++ web/compiler-explorer.js | 6 +- web/runners.js | 8 +- 4 files changed, 140 insertions(+), 5 deletions(-) diff --git a/tests/browser/compiler-explorer.test.js b/tests/browser/compiler-explorer.test.js index fc409b2c..fb3d9fd4 100644 --- a/tests/browser/compiler-explorer.test.js +++ b/tests/browser/compiler-explorer.test.js @@ -1371,6 +1371,113 @@ public: ); } } + + // A deep copy whose last node links back to the first copies every value + // and every random pointer, so only a serializer that refuses a revisited + // node can tell it from a correct answer. "empty random list" has no node. + const cyclicCopies = { + c: `struct Node* snapshotOutline(struct Node* head) { + int n = 0; + for (struct Node* p = head; p; p = p->next) n++; + if (!n) return NULL; + struct Node** copies = calloc(n, sizeof(*copies)); + struct Node* p = head; + for (int i = 0; i < n; i++, p = p->next) { + copies[i] = calloc(1, sizeof(*copies[i])); + copies[i]->val = p->val; + } + p = head; + for (int i = 0; i < n; i++, p = p->next) { + copies[i]->next = copies[(i + 1) % n]; + struct Node* q = head; + for (int j = 0; j < n; j++, q = q->next) + if (q == p->random) copies[i]->random = copies[j]; + } + struct Node* copy = copies[0]; + free(copies); + return copy; +}`, + cpp: `class Solution { +public: + Node* snapshotOutline(Node* head) { + if (!head) return nullptr; + unordered_map copies; + for (Node* p = head; p; p = p->next) copies[p] = new Node(p->val); + for (Node* p = head; p; p = p->next) { + copies[p]->next = p->next ? copies[p->next] : copies[head]; + copies[p]->random = p->random ? copies[p->random] : nullptr; + } + return copies[head]; + } +};`, + java: `class Solution { + public Node snapshotOutline(Node head) { + if (head == null) return null; + java.util.HashMap copies = new java.util.HashMap<>(); + for (Node p = head; p != null; p = p.next) copies.put(p, new Node(p.val)); + for (Node p = head; p != null; p = p.next) { + copies.get(p).next = p.next != null ? copies.get(p.next) : copies.get(head); + copies.get(p).random = p.random != null ? copies.get(p.random) : null; + } + return copies.get(head); + } +}`, + }; + const cyclicCopyRuns = { + c: { skip: noCc, file: "copy.c" }, + cpp: { skip: noGpp, file: "copy.cpp" }, + java: { skip: noJava, file: "Main.java" }, + }; + for (const [language, { skip, file }] of Object.entries(cyclicCopyRuns)) { + it( + `${language} harness refuses a copied random list that loops back into itself`, + { skip }, + async () => { + const spec = judges["copy-list-with-random-pointer"]; + const dir = mkdtempSync(join(tmpdir(), `codetrial-${language}-copy-`)); + try { + const source = join(dir, file); + const binary = join(dir, "copy"); + writeFileSync( + source, + generateHarness(language, spec, cyclicCopies[language]), + ); + let command = binary; + let args = []; + if (language === "c") + await run("cc", ["-std=c17", source, "-o", binary]); + if (language === "cpp") + await run("g++", ["-std=c++20", source, "-o", binary]); + if (language === "java") { + await run("javac", [source]); + command = "java"; + args = ["-cp", dir, "Main"]; + } + // A serializer that walks the ring forever fails here instead of + // hanging the lane. + const { stdout } = await run(command, args, { + encoding: "utf8", + timeout: 10000, + }); + const { results } = parseCompilerResults(stdout); + spec.cases.forEach((testCase, index) => { + const result = results[index]; + if (testCase.expected.length === 0) { + assert.deepEqual(result.actual, [], testCase.label); + return; + } + assert.match( + result.error ?? "", + /Returned list must not reach a node twice\./, + testCase.label, + ); + }); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }, + ); + } }); test("harness generator handles output parameter and output prefix specs", () => { diff --git a/tests/browser/runners.test.js b/tests/browser/runners.test.js index ae1dd9ae..c8eaf9eb 100644 --- a/tests/browser/runners.test.js +++ b/tests/browser/runners.test.js @@ -682,6 +682,30 @@ test("both runners refuse a returned list that loops back into itself", () => { assert.doesNotMatch(python, /id\(node\) not in seen/); }); +test("both runners refuse a copied random list that loops back into itself", () => { + // A copy whose last node links back to the first holds every value and + // random pointer, so stopping at the revisit would print a correct answer. + const javascript = runners.slice( + runners.indexOf("const randomListToArray"), + runners.indexOf("};", runners.indexOf("const randomListToArray")), + ); + assert.match( + javascript, + /if \(seen\.has\(node\)\)\s*throw new TypeError\("Returned list must not reach a node twice\."\)/, + ); + assert.doesNotMatch(javascript, /while \(node && !seen\.has\(node\)\)/); + + const python = runners.slice( + runners.indexOf("def _random_list_to_array"), + runners.indexOf("def _graph_to_adjacency"), + ); + assert.match( + python, + /if id\(node\) in seen:\s*raise TypeError\("Returned list must not reach a node twice\."\)/, + ); + assert.doesNotMatch(python, /id\(node\) not in seen/); +}); + test("the runner evaluates no string, so the page needs no unsafe-eval", () => { // The candidate's code is the first statements of the worker, not an // argument to eval inside it. This is what lets the page's diff --git a/web/compiler-explorer.js b/web/compiler-explorer.js index bd462ee2..c599a58f 100644 --- a/web/compiler-explorer.js +++ b/web/compiler-explorer.js @@ -399,7 +399,7 @@ static void json_char_matrix(FILE* out, char** values, int size, int* colSize) { static void json_string_matrix(FILE* out, char*** values, int size, int* colSize) { fputc('[', out); for (int i = 0; i < size; i++) { if (i) fputc(',', out); json_string_array(out, values[i], colSize[i]); } fputc(']', out); } static void json_list(FILE* out, struct ListNode* node) { for (struct ListNode *slow = node, *fast = node; fast && fast->next; ) { slow = slow->next; fast = fast->next->next; if (slow == fast) { fputs("null,\\"error\\":", out); json_string(out, "Returned list must not reach a node twice."); return; } } fputc('[', out); for (int i = 0; node; node = node->next, i++) { if (i) fputc(',', out); fprintf(out, "%d", node->val); } fputc(']', out); } static void json_tree(FILE* out, struct TreeNode* root) { if (!root) { fputs("[]", out); return; } struct TreeNode* queue[4096]; const char* values[4096]; char numbers[4096][32]; int head = 0, tail = 0, count = 0; queue[tail++] = root; while (head < tail && tail < 4096) { struct TreeNode* node = queue[head++]; if (!node) { values[count++] = "null"; continue; } for (int i = 0; i < head - 1; i++) if (queue[i] == node) { fputs("null,\\"error\\":", out); json_string(out, "Returned tree must not reach a node twice."); return; } snprintf(numbers[count], sizeof(numbers[count]), "%d", node->val); values[count] = numbers[count]; count++; queue[tail++] = node->left; queue[tail++] = node->right; } while (count > 0 && strcmp(values[count - 1], "null") == 0) count--; fputc('[', out); for (int i = 0; i < count; i++) { if (i) fputc(',', out); fputs(values[i], out); } fputc(']', out); } -static void json_random_list(FILE* out, struct Node* node) { struct Node* nodes[10000]; int count = 0; for (struct Node* cur = node; cur && count < 10000; cur = cur->next) { if (isOriginalRandomNode(cur)) { json_string(out, "Returned list must be a deep copy."); return; } nodes[count++] = cur; } fputc('[', out); for (int i = 0; i < count; i++) { if (i) fputc(',', out); int random = -1; for (int j = 0; j < count; j++) if (nodes[i]->random == nodes[j]) random = j; fprintf(out, "[%d,", nodes[i]->val); if (nodes[i]->random && random < 0) { json_string(out, "Returned random pointer must stay inside the copied list."); fputc(']', out); continue; } if (random < 0) fputs("null", out); else fprintf(out, "%d", random); fputc(']', out); } fputc(']', out); } +static void json_random_list(FILE* out, struct Node* node) { for (struct Node *slow = node, *fast = node; fast && fast->next; ) { slow = slow->next; fast = fast->next->next; if (slow == fast) { fputs("null,\\"error\\":", out); json_string(out, "Returned list must not reach a node twice."); return; } } struct Node* nodes[10000]; int count = 0; for (struct Node* cur = node; cur && count < 10000; cur = cur->next) { if (isOriginalRandomNode(cur)) { json_string(out, "Returned list must be a deep copy."); return; } nodes[count++] = cur; } fputc('[', out); for (int i = 0; i < count; i++) { if (i) fputc(',', out); int random = -1; for (int j = 0; j < count; j++) if (nodes[i]->random == nodes[j]) random = j; fprintf(out, "[%d,", nodes[i]->val); if (nodes[i]->random && random < 0) { json_string(out, "Returned random pointer must stay inside the copied list."); fputc(']', out); continue; } if (random < 0) fputs("null", out); else fprintf(out, "%d", random); fputc(']', out); } fputc(']', out); } static int compare_graph_neighbor_ids(const void* a, const void* b) { int left = *(const int*)a; int right = *(const int*)b; return (left > right) - (left < right); } static void json_graph(FILE* out, struct Node* node) { if (!node) { fputs("[]", out); return; } struct Node* nodes[1024]; struct Node* seen[1024] = {0}; int count = 0; nodes[count++] = node; for (int i = 0; i < count && count < 1024; i++) { struct Node* cur = nodes[i]; if (isOriginalGraphNode(cur)) { json_string(out, "Returned graph must be a deep copy."); return; } if (cur->val <= 0 || cur->val >= 1024) { json_string(out, "Returned graph node IDs must be between 1 and 1023."); return; } seen[cur->val] = cur; for (int j = 0; j < cur->numNeighbors; j++) { struct Node* next = cur->neighbors[j]; if (!next) { json_string(out, "Returned graph has a NULL neighbor."); return; } if (next->val <= 0 || next->val >= 1024) { json_string(out, "Returned graph node IDs must be between 1 and 1023."); return; } if (isOriginalGraphNode(next)) { json_string(out, "Returned graph must be a deep copy."); return; } if (seen[next->val] && seen[next->val] != next) { json_string(out, "Returned graph must not contain distinct nodes with the same ID."); return; } if (!seen[next->val]) { seen[next->val] = next; nodes[count++] = next; } } } fputc('[', out); for (int value = 1, printed = 0; value < 1024; value++) { struct Node* cur = NULL; for (int i = 0; i < count; i++) if (nodes[i]->val == value) cur = nodes[i]; if (!cur) continue; if (printed++) fputc(',', out); int degree = cur->numNeighbors; int* neighborIds = NULL; if (degree > 0) { neighborIds = malloc((size_t)degree * sizeof(*neighborIds)); if (!neighborIds) exit(EXIT_FAILURE); for (int j = 0; j < degree; j++) neighborIds[j] = cur->neighbors[j]->val; qsort(neighborIds, (size_t)degree, sizeof(*neighborIds), compare_graph_neighbor_ids); } json_int_array(out, neighborIds, degree); free(neighborIds); } fputc(']', out); } static void json_next_tree(FILE* out, struct Node* root) { if (!root) { fputs("[]", out); return; } struct Node* queue[4096]; int head = 0, tail = 0, firstLevel = 1; queue[tail++] = root; fputc('[', out); while (head < tail) { int end = tail; if (!firstLevel) fputc(',', out); firstLevel = 0; fputc('[', out); for (int i = head; i < end; i++) { if (i > head) fputc(',', out); fprintf(out, "%d", queue[i]->val); if (queue[i]->next != (i + 1 < end ? queue[i + 1] : NULL)) { fputs("]", out); fputs("]", out); return; } if (queue[i]->left) queue[tail++] = queue[i]->left; if (queue[i]->right) queue[tail++] = queue[i]->right; } fputc(']', out); head = end; } fputc(']', out); } @@ -560,7 +560,7 @@ ListNode* listNode(vector values, int pos) { ListNode* head = listNode(valu TreeNode* treeNode(vector> values) { if (values.empty() || !values[0]) return nullptr; auto* root = new TreeNode(*values[0]); queue q; q.push(root); size_t i = 1; while (!q.empty() && i < values.size()) { auto* node = q.front(); q.pop(); if (i < values.size() && values[i]) { node->left = new TreeNode(*values[i]); q.push(node->left); } ++i; if (i < values.size() && values[i]) { node->right = new TreeNode(*values[i]); q.push(node->right); } ++i; } return root; } TreeNode* findTreeNode(TreeNode* root, int value) { queue q; if (root) q.push(root); while (!q.empty()) { auto* node = q.front(); q.pop(); if (node->val == value) return node; if (node->left) q.push(node->left); if (node->right) q.push(node->right); } throw runtime_error("Could not find tree node value."); } Node* randomList(vector>> items) { vector nodes; for (auto item : items) nodes.push_back(new Node(item.first)); originalRandomNodes = set(nodes.begin(), nodes.end()); for (size_t i = 0; i < nodes.size(); ++i) { nodes[i]->next = i + 1 < nodes.size() ? nodes[i + 1] : nullptr; if (items[i].second) nodes[i]->random = nodes[*items[i].second]; } return nodes.empty() ? nullptr : nodes[0]; } -string randomListToJson(Node* node) { vector nodes; map index; set seen; for (auto* cur = node; cur && !seen.count(cur); cur = cur->next) { if (originalRandomNodes.count(cur)) throw runtime_error("Returned list must be a deep copy."); seen.insert(cur); index[cur] = nodes.size(); nodes.push_back(cur); } string out = "["; for (size_t i = 0; i < nodes.size(); ++i) { if (i) out += ","; if (nodes[i]->random && !index.count(nodes[i]->random)) throw runtime_error("Returned random pointer must stay inside the copied list."); out += "[" + to_string(nodes[i]->val) + "," + (nodes[i]->random ? to_string(index[nodes[i]->random]) : string("null")) + "]"; } return out + "]"; } +string randomListToJson(Node* node) { vector nodes; map index; set seen; for (auto* cur = node; cur; cur = cur->next) { if (!seen.insert(cur).second) throw runtime_error("Returned list must not reach a node twice."); if (originalRandomNodes.count(cur)) throw runtime_error("Returned list must be a deep copy."); index[cur] = nodes.size(); nodes.push_back(cur); } string out = "["; for (size_t i = 0; i < nodes.size(); ++i) { if (i) out += ","; if (nodes[i]->random && !index.count(nodes[i]->random)) throw runtime_error("Returned random pointer must stay inside the copied list."); out += "[" + to_string(nodes[i]->val) + "," + (nodes[i]->random ? to_string(index[nodes[i]->random]) : string("null")) + "]"; } return out + "]"; } Node* graphNode(vector> adjacency) { if (adjacency.empty()) { originalGraphNodes.clear(); return nullptr; } vector nodes; for (size_t i = 0; i < adjacency.size(); ++i) nodes.push_back(new Node(i + 1)); originalGraphNodes = set(nodes.begin(), nodes.end()); for (size_t i = 0; i < adjacency.size(); ++i) for (int value : adjacency[i]) nodes[i]->neighbors.push_back(nodes[value - 1]); return nodes[0]; } string graphToJson(Node* node) { if (!node) return "[]"; map byValue; queue q; q.push(node); while (!q.empty()) { auto* cur = q.front(); q.pop(); if (!cur) continue; if (originalGraphNodes.count(cur)) throw runtime_error("Returned graph must be a deep copy."); if (byValue.count(cur->val)) { if (byValue.at(cur->val) != cur) throw runtime_error("Returned graph must not contain distinct nodes with the same ID."); continue; } byValue[cur->val] = cur; for (auto* next : cur->neighbors) { if (!next) throw runtime_error("Returned graph has a NULL neighbor."); q.push(next); } } string out = "["; bool first = true; for (auto& [value, cur] : byValue) { if (!first) out += ","; first = false; vector neighbors; for (auto* next : cur->neighbors) neighbors.push_back(next->val); sort(neighbors.begin(), neighbors.end()); out += toJson(neighbors); } return out + "]"; } Node* nextTree(vector> values) { if (values.empty() || !values[0]) return nullptr; auto* root = new Node(*values[0]); queue q; q.push(root); size_t i = 1; while (!q.empty() && i < values.size()) { auto* node = q.front(); q.pop(); if (i < values.size() && values[i]) { node->left = new Node(*values[i]); q.push(node->left); } ++i; if (i < values.size() && values[i]) { node->right = new Node(*values[i]); q.push(node->right); } ++i; } return root; } @@ -729,7 +729,7 @@ class Main { static TreeNode treeNode(Integer[] values) { if (values.length == 0 || values[0] == null) return null; TreeNode root = new TreeNode(values[0]); ArrayDeque q = new ArrayDeque<>(); q.add(root); int i = 1; while (!q.isEmpty() && i < values.length) { TreeNode node = q.remove(); if (i < values.length && values[i] != null) { node.left = new TreeNode(values[i]); q.add(node.left); } i++; if (i < values.length && values[i] != null) { node.right = new TreeNode(values[i]); q.add(node.right); } i++; } return root; } static TreeNode findTreeNode(TreeNode root, int value) { ArrayDeque q = new ArrayDeque<>(); if (root != null) q.add(root); while (!q.isEmpty()) { TreeNode node = q.remove(); if (node.val == value) return node; if (node.left != null) q.add(node.left); if (node.right != null) q.add(node.right); } throw new IllegalArgumentException("Could not find tree node value."); } static Node randomList(int[][] items) { ArrayList nodes = new ArrayList<>(); for (int[] item : items) nodes.add(new Node(item[0])); originalRandomNodes = new HashSet<>(nodes); for (int i = 0; i < nodes.size(); i++) { nodes.get(i).next = i + 1 < nodes.size() ? nodes.get(i + 1) : null; if (items[i][1] >= 0) nodes.get(i).random = nodes.get(items[i][1]); } return nodes.isEmpty() ? null : nodes.get(0); } - static String randomListJson(Node node) { ArrayList nodes = new ArrayList<>(); HashMap index = new HashMap<>(); HashSet seen = new HashSet<>(); for (Node cur = node; cur != null && !seen.contains(cur); cur = cur.next) { if (originalRandomNodes.contains(cur)) throw new IllegalArgumentException("Returned list must be a deep copy."); seen.add(cur); index.put(cur, nodes.size()); nodes.add(cur); } StringJoiner out = new StringJoiner(",", "[", "]"); for (Node cur : nodes) { if (cur.random != null && !index.containsKey(cur.random)) throw new IllegalArgumentException("Returned random pointer must stay inside the copied list."); out.add("[" + cur.val + "," + (cur.random == null ? "null" : index.get(cur.random)) + "]"); } return out.toString(); } + static String randomListJson(Node node) { ArrayList nodes = new ArrayList<>(); HashMap index = new HashMap<>(); HashSet seen = new HashSet<>(); for (Node cur = node; cur != null; cur = cur.next) { if (!seen.add(cur)) throw new IllegalArgumentException("Returned list must not reach a node twice."); if (originalRandomNodes.contains(cur)) throw new IllegalArgumentException("Returned list must be a deep copy."); index.put(cur, nodes.size()); nodes.add(cur); } StringJoiner out = new StringJoiner(",", "[", "]"); for (Node cur : nodes) { if (cur.random != null && !index.containsKey(cur.random)) throw new IllegalArgumentException("Returned random pointer must stay inside the copied list."); out.add("[" + cur.val + "," + (cur.random == null ? "null" : index.get(cur.random)) + "]"); } return out.toString(); } static Node graphNode(int[][] adjacency) { if (adjacency.length == 0) { originalGraphNodes = new HashSet<>(); return null; } ArrayList nodes = new ArrayList<>(); for (int i = 0; i < adjacency.length; i++) nodes.add(new Node(i + 1)); originalGraphNodes = new HashSet<>(nodes); for (int i = 0; i < adjacency.length; i++) for (int value : adjacency[i]) nodes.get(i).neighbors.add(nodes.get(value - 1)); return nodes.get(0); } static String graphJson(Node node) { if (node == null) return "[]"; TreeMap byValue = new TreeMap<>(); ArrayDeque q = new ArrayDeque<>(); q.add(node); while (!q.isEmpty()) { Node cur = q.remove(); if (cur == null) continue; if (originalGraphNodes.contains(cur)) throw new IllegalArgumentException("Returned graph must be a deep copy."); if (byValue.containsKey(cur.val)) { if (byValue.get(cur.val) != cur) throw new IllegalArgumentException("Returned graph must not contain distinct nodes with the same ID."); continue; } byValue.put(cur.val, cur); for (Node next : cur.neighbors) q.add(next); } StringJoiner out = new StringJoiner(",", "[", "]"); for (Node cur : byValue.values()) { ArrayList row = new ArrayList<>(); for (Node next : cur.neighbors) row.add(next.val); Collections.sort(row); out.add(json(row)); } return out.toString(); } static Node nextTree(Integer[] values) { if (values.length == 0 || values[0] == null) return null; Node root = new Node(values[0]); ArrayDeque q = new ArrayDeque<>(); q.add(root); int i = 1; while (!q.isEmpty() && i < values.length) { Node node = q.remove(); if (i < values.length && values[i] != null) { node.left = new Node(values[i]); q.add(node.left); } i++; if (i < values.length && values[i] != null) { node.right = new Node(values[i]); q.add(node.right); } i++; } return root; } diff --git a/web/runners.js b/web/runners.js index db93dc47..be607101 100644 --- a/web/runners.js +++ b/web/runners.js @@ -697,7 +697,9 @@ const JS_RUNNER_SOURCE = ` }; const randomListToArray = (node) => { const nodes = [], seen = new Set(); - while (node && !seen.has(node)) { + while (node) { + if (seen.has(node)) + throw new TypeError("Returned list must not reach a node twice."); if (originalRandomNodes.has(node)) throw new TypeError("Returned list must be a deep copy."); seen.add(node); nodes.push(node); @@ -1026,7 +1028,9 @@ def _quad_tree_to_preorder(root): def _random_list_to_array(node): nodes = [] seen = set() - while node is not None and id(node) not in seen: + while node is not None: + if id(node) in seen: + raise TypeError("Returned list must not reach a node twice.") if id(node) in _original_random_nodes: raise TypeError("Returned list must be a deep copy.") seen.add(id(node)) From 7752c85f708e2706f2c69ec2c3e5ee6b358f6a98 Mon Sep 17 00:00:00 2001 From: papaya0112 Date: Fri, 23 Oct 2026 13:31:09 +0800 Subject: [PATCH 2/3] Report both C random-list refusals as errors The C random-list serializer printed a shallow copy's refusal as the answer itself and put a stray random pointer's refusal inside the array, so neither case carried an error and C showed a wrong answer with no reason, while C++ and Java throw both. Check every random pointer before the opening bracket and report both refusals through the error field, as the cycle check now does. The cycle check runs after the deep-copy walk, so a shallow copy that also loops is refused as a shallow copy, the same answer the other four give. --- tests/browser/compiler-explorer.test.js | 74 +++++++++++++++++++++++++ web/compiler-explorer.js | 2 +- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/tests/browser/compiler-explorer.test.js b/tests/browser/compiler-explorer.test.js index fb3d9fd4..d7f304b9 100644 --- a/tests/browser/compiler-explorer.test.js +++ b/tests/browser/compiler-explorer.test.js @@ -1478,6 +1478,80 @@ public: }, ); } + + // C used to print these two refusals where the answer goes, so the case + // failed as a wrong answer with no reason. C++, Java, JavaScript and Python + // throw them, and C reports them through the same error field. A shallow + // copy that also loops is refused as a shallow copy, as those four refuse + // it, because the original head comes before any revisit. + const deepCopy = "Returned list must be a deep copy."; + const strayRandom = + "Returned random pointer must stay inside the copied list."; + const strayCopies = [ + { + name: "a shallow copy", + message: deepCopy, + candidate: `struct Node* snapshotOutline(struct Node* head) { return head; }`, + }, + { + name: "a shallow copy that loops", + message: deepCopy, + candidate: `struct Node* snapshotOutline(struct Node* head) { + struct Node* tail = head; + while (tail && tail->next) tail = tail->next; + if (tail) tail->next = head; + return head; +}`, + }, + { + name: "a random pointer into the original", + message: strayRandom, + candidate: `struct Node* snapshotOutline(struct Node* head) { + struct Node dummy = { 0 }; + struct Node* tail = &dummy; + for (struct Node* p = head; p; p = p->next) { + tail->next = calloc(1, sizeof(*tail->next)); + tail = tail->next; + tail->val = p->val; + tail->random = p->random; + } + return dummy.next; +}`, + }, + ]; + for (const { name, message, candidate } of strayCopies) { + it(`c harness reports ${name} as an error`, { skip: noCc }, async () => { + const spec = judges["copy-list-with-random-pointer"]; + const dir = mkdtempSync(join(tmpdir(), "codetrial-c-stray-")); + try { + const source = join(dir, "copy.c"); + const binary = join(dir, "copy"); + writeFileSync(source, generateHarness("c", spec, candidate)); + await run("cc", ["-std=c17", source, "-o", binary]); + const { stdout } = await run(binary, [], { + encoding: "utf8", + timeout: 10000, + }); + const { results } = parseCompilerResults(stdout); + spec.cases.forEach((testCase, index) => { + const result = results[index]; + const nodes = testCase.input[0]; + const strays = + message === deepCopy + ? nodes.length > 0 + : nodes.some(([, random]) => random !== null); + if (!strays) { + assert.deepEqual(result.actual, testCase.expected, testCase.label); + return; + } + assert.equal(result.actual, null, testCase.label); + assert.equal(result.error, message, testCase.label); + }); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + } }); test("harness generator handles output parameter and output prefix specs", () => { diff --git a/web/compiler-explorer.js b/web/compiler-explorer.js index c599a58f..ee179411 100644 --- a/web/compiler-explorer.js +++ b/web/compiler-explorer.js @@ -399,7 +399,7 @@ static void json_char_matrix(FILE* out, char** values, int size, int* colSize) { static void json_string_matrix(FILE* out, char*** values, int size, int* colSize) { fputc('[', out); for (int i = 0; i < size; i++) { if (i) fputc(',', out); json_string_array(out, values[i], colSize[i]); } fputc(']', out); } static void json_list(FILE* out, struct ListNode* node) { for (struct ListNode *slow = node, *fast = node; fast && fast->next; ) { slow = slow->next; fast = fast->next->next; if (slow == fast) { fputs("null,\\"error\\":", out); json_string(out, "Returned list must not reach a node twice."); return; } } fputc('[', out); for (int i = 0; node; node = node->next, i++) { if (i) fputc(',', out); fprintf(out, "%d", node->val); } fputc(']', out); } static void json_tree(FILE* out, struct TreeNode* root) { if (!root) { fputs("[]", out); return; } struct TreeNode* queue[4096]; const char* values[4096]; char numbers[4096][32]; int head = 0, tail = 0, count = 0; queue[tail++] = root; while (head < tail && tail < 4096) { struct TreeNode* node = queue[head++]; if (!node) { values[count++] = "null"; continue; } for (int i = 0; i < head - 1; i++) if (queue[i] == node) { fputs("null,\\"error\\":", out); json_string(out, "Returned tree must not reach a node twice."); return; } snprintf(numbers[count], sizeof(numbers[count]), "%d", node->val); values[count] = numbers[count]; count++; queue[tail++] = node->left; queue[tail++] = node->right; } while (count > 0 && strcmp(values[count - 1], "null") == 0) count--; fputc('[', out); for (int i = 0; i < count; i++) { if (i) fputc(',', out); fputs(values[i], out); } fputc(']', out); } -static void json_random_list(FILE* out, struct Node* node) { for (struct Node *slow = node, *fast = node; fast && fast->next; ) { slow = slow->next; fast = fast->next->next; if (slow == fast) { fputs("null,\\"error\\":", out); json_string(out, "Returned list must not reach a node twice."); return; } } struct Node* nodes[10000]; int count = 0; for (struct Node* cur = node; cur && count < 10000; cur = cur->next) { if (isOriginalRandomNode(cur)) { json_string(out, "Returned list must be a deep copy."); return; } nodes[count++] = cur; } fputc('[', out); for (int i = 0; i < count; i++) { if (i) fputc(',', out); int random = -1; for (int j = 0; j < count; j++) if (nodes[i]->random == nodes[j]) random = j; fprintf(out, "[%d,", nodes[i]->val); if (nodes[i]->random && random < 0) { json_string(out, "Returned random pointer must stay inside the copied list."); fputc(']', out); continue; } if (random < 0) fputs("null", out); else fprintf(out, "%d", random); fputc(']', out); } fputc(']', out); } +static void json_random_list(FILE* out, struct Node* node) { struct Node* nodes[10000]; int randoms[10000]; int count = 0; for (struct Node* cur = node; cur && count < 10000; cur = cur->next) { if (isOriginalRandomNode(cur)) { fputs("null,\\"error\\":", out); json_string(out, "Returned list must be a deep copy."); return; } nodes[count++] = cur; } for (struct Node *slow = node, *fast = node; fast && fast->next; ) { slow = slow->next; fast = fast->next->next; if (slow == fast) { fputs("null,\\"error\\":", out); json_string(out, "Returned list must not reach a node twice."); return; } } for (int i = 0; i < count; i++) { randoms[i] = -1; for (int j = 0; j < count; j++) if (nodes[i]->random == nodes[j]) { randoms[i] = j; break; } if (nodes[i]->random && randoms[i] < 0) { fputs("null,\\"error\\":", out); json_string(out, "Returned random pointer must stay inside the copied list."); return; } } fputc('[', out); for (int i = 0; i < count; i++) { if (i) fputc(',', out); fprintf(out, "[%d,", nodes[i]->val); if (randoms[i] < 0) fputs("null", out); else fprintf(out, "%d", randoms[i]); fputc(']', out); } fputc(']', out); } static int compare_graph_neighbor_ids(const void* a, const void* b) { int left = *(const int*)a; int right = *(const int*)b; return (left > right) - (left < right); } static void json_graph(FILE* out, struct Node* node) { if (!node) { fputs("[]", out); return; } struct Node* nodes[1024]; struct Node* seen[1024] = {0}; int count = 0; nodes[count++] = node; for (int i = 0; i < count && count < 1024; i++) { struct Node* cur = nodes[i]; if (isOriginalGraphNode(cur)) { json_string(out, "Returned graph must be a deep copy."); return; } if (cur->val <= 0 || cur->val >= 1024) { json_string(out, "Returned graph node IDs must be between 1 and 1023."); return; } seen[cur->val] = cur; for (int j = 0; j < cur->numNeighbors; j++) { struct Node* next = cur->neighbors[j]; if (!next) { json_string(out, "Returned graph has a NULL neighbor."); return; } if (next->val <= 0 || next->val >= 1024) { json_string(out, "Returned graph node IDs must be between 1 and 1023."); return; } if (isOriginalGraphNode(next)) { json_string(out, "Returned graph must be a deep copy."); return; } if (seen[next->val] && seen[next->val] != next) { json_string(out, "Returned graph must not contain distinct nodes with the same ID."); return; } if (!seen[next->val]) { seen[next->val] = next; nodes[count++] = next; } } } fputc('[', out); for (int value = 1, printed = 0; value < 1024; value++) { struct Node* cur = NULL; for (int i = 0; i < count; i++) if (nodes[i]->val == value) cur = nodes[i]; if (!cur) continue; if (printed++) fputc(',', out); int degree = cur->numNeighbors; int* neighborIds = NULL; if (degree > 0) { neighborIds = malloc((size_t)degree * sizeof(*neighborIds)); if (!neighborIds) exit(EXIT_FAILURE); for (int j = 0; j < degree; j++) neighborIds[j] = cur->neighbors[j]->val; qsort(neighborIds, (size_t)degree, sizeof(*neighborIds), compare_graph_neighbor_ids); } json_int_array(out, neighborIds, degree); free(neighborIds); } fputc(']', out); } static void json_next_tree(FILE* out, struct Node* root) { if (!root) { fputs("[]", out); return; } struct Node* queue[4096]; int head = 0, tail = 0, firstLevel = 1; queue[tail++] = root; fputc('[', out); while (head < tail) { int end = tail; if (!firstLevel) fputc(',', out); firstLevel = 0; fputc('[', out); for (int i = head; i < end; i++) { if (i > head) fputc(',', out); fprintf(out, "%d", queue[i]->val); if (queue[i]->next != (i + 1 < end ? queue[i + 1] : NULL)) { fputs("]", out); fputs("]", out); return; } if (queue[i]->left) queue[tail++] = queue[i]->left; if (queue[i]->right) queue[tail++] = queue[i]->right; } fputc(']', out); head = end; } fputc(']', out); } From 7874eb55417b514f19a1e659fa621d49432a092d Mon Sep 17 00:00:00 2001 From: papaya0112 Date: Fri, 23 Oct 2026 14:42:26 +0800 Subject: [PATCH 3/3] Share one helper across compiled harness tests The cyclic-copy and stray-copy tests repeated the compile-and-run steps of the flatten tests, with their own compiler flags and timeout. Give that helper the judge id as a parameter so every harness test builds and bounds its run in one place. The stray-copy comment also claimed JavaScript and Python throw a stray random pointer, which they do not until #372 lands. --- tests/browser/compiler-explorer.test.js | 152 +++++++++--------------- 1 file changed, 59 insertions(+), 93 deletions(-) diff --git a/tests/browser/compiler-explorer.test.js b/tests/browser/compiler-explorer.test.js index d7f304b9..e441d2d1 100644 --- a/tests/browser/compiler-explorer.test.js +++ b/tests/browser/compiler-explorer.test.js @@ -1187,24 +1187,18 @@ public: ); } - // Compiles a flatten candidate into the harness and returns its results. - // The timeout keeps a serializer that walks forever from hanging the lane. - async function runFlatten(language, candidate) { - const dir = mkdtempSync(join(tmpdir(), `codetrial-${language}-flatten-`)); + // Compiles a candidate into the named judge's harness and returns its + // results. The timeout keeps a serializer that walks forever from hanging + // the lane. + async function runJudge(language, id, candidate) { + const dir = mkdtempSync(join(tmpdir(), `codetrial-${language}-${id}-`)); try { const source = join( dir, - language === "java" ? "Main.java" : `flatten.${language}`, - ); - const binary = join(dir, "flatten"); - writeFileSync( - source, - generateHarness( - language, - judges["flatten-binary-tree-to-linked-list"], - candidate, - ), + language === "java" ? "Main.java" : `candidate.${language}`, ); + const binary = join(dir, "candidate"); + writeFileSync(source, generateHarness(language, judges[id], candidate)); if (language === "java") await run("javac", [source]); else await run(language === "c" ? "cc" : "g++", [ @@ -1289,7 +1283,11 @@ public: { skip }, async () => { const judge = judges["flatten-binary-tree-to-linked-list"]; - const results = await runFlatten(language, cyclicFlatten[language]); + const results = await runJudge( + language, + "flatten-binary-tree-to-linked-list", + cyclicFlatten[language], + ); assert.equal(results.length, judge.cases.length); judge.cases.forEach((testCase, index) => { const result = results[index]; @@ -1344,8 +1342,9 @@ public: { skip }, async () => { const judge = judges["flatten-binary-tree-to-linked-list"]; - const results = await runFlatten( + const results = await runJudge( language, + "flatten-binary-tree-to-linked-list", twinChildren[language](shared), ); assert.equal(results.length, judge.cases.length); @@ -1423,66 +1422,42 @@ public: } }`, }; - const cyclicCopyRuns = { - c: { skip: noCc, file: "copy.c" }, - cpp: { skip: noGpp, file: "copy.cpp" }, - java: { skip: noJava, file: "Main.java" }, - }; - for (const [language, { skip, file }] of Object.entries(cyclicCopyRuns)) { + const cyclicCopyRuns = [ + ["c", noCc], + ["cpp", noGpp], + ["java", noJava], + ]; + for (const [language, skip] of cyclicCopyRuns) { it( `${language} harness refuses a copied random list that loops back into itself`, { skip }, async () => { const spec = judges["copy-list-with-random-pointer"]; - const dir = mkdtempSync(join(tmpdir(), `codetrial-${language}-copy-`)); - try { - const source = join(dir, file); - const binary = join(dir, "copy"); - writeFileSync( - source, - generateHarness(language, spec, cyclicCopies[language]), - ); - let command = binary; - let args = []; - if (language === "c") - await run("cc", ["-std=c17", source, "-o", binary]); - if (language === "cpp") - await run("g++", ["-std=c++20", source, "-o", binary]); - if (language === "java") { - await run("javac", [source]); - command = "java"; - args = ["-cp", dir, "Main"]; + const results = await runJudge( + language, + "copy-list-with-random-pointer", + cyclicCopies[language], + ); + spec.cases.forEach((testCase, index) => { + const result = results[index]; + if (testCase.expected.length === 0) { + assert.deepEqual(result.actual, [], testCase.label); + return; } - // A serializer that walks the ring forever fails here instead of - // hanging the lane. - const { stdout } = await run(command, args, { - encoding: "utf8", - timeout: 10000, - }); - const { results } = parseCompilerResults(stdout); - spec.cases.forEach((testCase, index) => { - const result = results[index]; - if (testCase.expected.length === 0) { - assert.deepEqual(result.actual, [], testCase.label); - return; - } - assert.match( - result.error ?? "", - /Returned list must not reach a node twice\./, - testCase.label, - ); - }); - } finally { - rmSync(dir, { recursive: true, force: true }); - } + assert.match( + result.error ?? "", + /Returned list must not reach a node twice\./, + testCase.label, + ); + }); }, ); } // C used to print these two refusals where the answer goes, so the case - // failed as a wrong answer with no reason. C++, Java, JavaScript and Python - // throw them, and C reports them through the same error field. A shallow - // copy that also loops is refused as a shallow copy, as those four refuse + // failed as a wrong answer with no reason. C++ and Java throw them, and C + // reports them through the same error field. A shallow copy that also + // loops is refused as a shallow copy, as the other four harnesses refuse // it, because the original head comes before any revisit. const deepCopy = "Returned list must be a deep copy."; const strayRandom = @@ -1522,34 +1497,25 @@ public: for (const { name, message, candidate } of strayCopies) { it(`c harness reports ${name} as an error`, { skip: noCc }, async () => { const spec = judges["copy-list-with-random-pointer"]; - const dir = mkdtempSync(join(tmpdir(), "codetrial-c-stray-")); - try { - const source = join(dir, "copy.c"); - const binary = join(dir, "copy"); - writeFileSync(source, generateHarness("c", spec, candidate)); - await run("cc", ["-std=c17", source, "-o", binary]); - const { stdout } = await run(binary, [], { - encoding: "utf8", - timeout: 10000, - }); - const { results } = parseCompilerResults(stdout); - spec.cases.forEach((testCase, index) => { - const result = results[index]; - const nodes = testCase.input[0]; - const strays = - message === deepCopy - ? nodes.length > 0 - : nodes.some(([, random]) => random !== null); - if (!strays) { - assert.deepEqual(result.actual, testCase.expected, testCase.label); - return; - } - assert.equal(result.actual, null, testCase.label); - assert.equal(result.error, message, testCase.label); - }); - } finally { - rmSync(dir, { recursive: true, force: true }); - } + const results = await runJudge( + "c", + "copy-list-with-random-pointer", + candidate, + ); + spec.cases.forEach((testCase, index) => { + const result = results[index]; + const nodes = testCase.input[0]; + const strays = + message === deepCopy + ? nodes.length > 0 + : nodes.some(([, random]) => random !== null); + if (!strays) { + assert.deepEqual(result.actual, testCase.expected, testCase.label); + return; + } + assert.equal(result.actual, null, testCase.label); + assert.equal(result.error, message, testCase.label); + }); }); } });