Repository navigation
Refuse a returned tree that reaches a node twice - #388
Conversation
| `); | ||
| await page.getByRole("button", { name: /Run tests/ }).click(); | ||
| await page.getByRole("button", { name: "Run tests" }).waitFor(); | ||
| await treeReachedTwice.waitFor(); |
There was a problem hiding this comment.
This only proves the refusal text shows up somewhere in #results-body. The description's main claim, that the cases without the defect ([] and [0]) still report their own results, is checked only for C, C++ and Java in tests/browser/compiler-explorer.test.js. For JavaScript and Python, a regression that moved the throw outside the per-case try would still pass this check. Asserting the summary count here, as the two-sum check earlier in this file does, covers that. The Python run at the second treeReachedTwice.waitFor needs the same line with { timeout: 120000 }.
| await treeReachedTwice.waitFor(); | |
| await treeReachedTwice.waitFor(); | |
| await page | |
| .getByText( | |
| `Test results · 2/${scenario(PROBLEM_IDS.flattenTree).caseCount}`, | |
| ) | |
| .waitFor(); |
There was a problem hiding this comment.
Added the summary count after both runs, with the 120 s timeout on the Python one. To check that it bites, I rethrew the refusal from the per-case catch in web/runners.js: the previous check still passed and the new one fails at the JavaScript count. Rebased onto f3ae86c.
3561438 to
deb7acc
Compare
deb7acc to
6278426
Compare
jserv
left a comment
There was a problem hiding this comment.
Rebase latest main branch and resolve conflicts.
6278426 to
ef64238
Compare
ef64238 to
c84bfe0
Compare
| // so the run timed out or was killed and no case reported anything. The | ||
| // single-node and empty cases have no thread to leave, so they still pass. | ||
| // The timeout keeps a regression from hanging the lane. | ||
| const cyclicFlatten = { |
There was a problem hiding this comment.
Every new case builds a cycle, but the description's point is that the visited set also refuses two parents sharing one subtree. Nothing pins that down. Replacing seen with an ancestor-path check would still pass these tests and the browser check while no longer refusing a shared child. Add a case that sets root.left = root.right = child and expects the refusal as the case's error in all five runners. Also add a tree with equal values in distinct nodes that must still serialize, so the check stays keyed on node identity rather than val.
There was a problem hiding this comment.
Added both to all five runners. Every case with a root now also returns two children holding the root's value: the same node on both sides must come back as the case's error, and two distinct nodes must print as [v,v,v]. tests/browser/compiler-explorer.test.js covers C, C++ and Java, and scripts/browser-check.cjs covers JavaScript and Python. Refusing only a link back to the root, which lets a shared child through the way an ancestor-path check would, keeps the cycle tests passing and fails the shared-child test in all five runners. Comparing val instead of the node fails the distinct-children test in all five.
| }; | ||
| const treeFromArray = treeBuilder(TreeNode); | ||
| const nextTreeFromArray = treeBuilder(NextNode); | ||
| // A node reached twice is a cycle or a shared subtree. The walk below |
There was a problem hiding this comment.
This guard covers binaryTree output only. nextTreeToLevels, _next_tree_to_levels and C's json_next_tree walk left/right with no visited set. A linkRowNeighbors that sets root.left = root and leaves root.next = null passes the next-pointer check on every level and loops until the worker times out. That is the same failure #386 reported. quadTreeToPreorder and its C/C++ counterparts recurse into a self-linked child until the stack overflows, which kills every C/C++ case. Worth a follow-up issue that applies the same identity check to those serializers.
There was a problem hiding this comment.
Filed #428 for the next-pointer and quad tree serializers, leaving C's json_next_tree to #373. For the quad side it proposes refusing only a node that is its own ancestor, because an answer that reuses one leaf for every uniform quarter passes all five Terrain Mask Tiling cases today and a visited set would refuse it.
The tree serializers in all five runners walked the returned tree breadth first without remembering which nodes they had visited, so a tree that linked back to an earlier node kept the walk going until the run was cut off. Browser runs timed out with advice to look for an infinite loop in code that had already returned, Java was killed by Compiler Explorer, and C overran its output limit, so no case reported anything. Each serializer now refuses the second visit with the same message. That also refuses two parents sharing one subtree, which is not a tree either and costs nothing more to catch, so the message names the repeated visit rather than a cycle. Closes sysprog21#386
c84bfe0 to
2c57f99
Compare
|
Thank @quill0224 for contributing! |
A returned binary tree that links back to an earlier node used to stall every runner: the tree serializers walk breadth first with no record of visited nodes, so JavaScript and Python timed out with "check for an infinite loop" in code that had already returned, Java was killed with SIGKILL, and C overran Compiler Explorer's output limit, with no case reporting anything.
treeToArray,_tree_to_array, C'sjson_tree, C++'stoJson(TreeNode*)and Java'sjson(TreeNode)now refuse a node they reach a second time with "Returned tree must not reach a node twice.", and the cases without that defect still report their own results. C++ was missing from the issue's table but has the same serializer, so it is included.The issue proposed "Returned tree must not contain a cycle.". A visited set cannot tell a cycle from two parents sharing one subtree, and neither is a tree, so the check refuses both and the message names what it saw rather than claiming a cycle. That settles follow-up 4 of the issue. Rejecting only cycles would need the ancestor path in all five serializers. C has no set, so
json_treescans the nodes it has already dequeued, and it reports the refusal as the case's error the wayjson_listdoes since #405. The scan is quadratic but bounded by the 4096-entry queue: a 2047-node chain, the largest the queue holds, takes about 13 ms at -O0.The tests run the issue's cyclic flatten, the last Morris thread left in place, and expect the message as the error of the three cases with more than one node and the normal result on the empty and single-node cases. Two more flattens give every case with a root two children holding the root's value. One hangs the same node on both sides and must be refused, which is the shared child a cycle-only check would let through. The other uses two distinct nodes and must print as
[v,v,v], which keeps the check keyed on the node rather than onval.tests/browser/compiler-explorer.test.jscompiles and runs all three locally with cc, g++ and javac and checks the case count.scripts/browser-check.cjsruns them on the page in JavaScript and Python and checks the summary count.Validation on macOS 26.5 over 3198a99:
node --test --test-name-pattern='returned tree' tests/browser/compiler-explorer.test.js: 9 pass. With the serializers frommain, the three cycle tests fail (C aborts with SIGABRT, C++ and Java are killed by the 10 s timeout) and so do the three shared-child tests. Refusing only a link back to the root, which lets a shared child through as an ancestor-path check would, still passes the cycle tests and fails the three shared-child tests. Comparingvalinstead of the node fails the three distinct-children tests. Printing C's refusal as the case's value instead of its error fails both C refusal tests.BROWSER_CHECK_AGENT=offline scripts/browser-check.shpasses. Reverting the JavaScript check fails it atscripts/browser-check.cjs:1658and reverting only the Python check atscripts/browser-check.cjs:1702, the two cycle runs. Rethrowing the refusal from the per-case catch inweb/runners.jsstill shows the message but fails the summary count atscripts/browser-check.cjs:1663. Refusing only a link back to the root fails the shared-child runs called at lines 1664 (JavaScript) and 1708 (Python), and comparingvalfails the distinct-children runs called at lines 1673 and 1718../scripts/test.shpasses. Not checked by this run: commentflow, ruff, shfmt, shellcheck, cargo-audit and actionlint are not installed, and the pinned Clang lane runs only on Linux x86_64.Left out: the bound on
json_tree's queue, tracked in #408 and fixed in #422. A returned tree with 2048 or more nodes still writesqueue[4096]here, cycle or not, before the walk dequeues a repeated node. The next-pointer and quad tree serializers have no visited check either, tracked in #428.Closes #386
Summary by cubic
Tree serializers in all five runners now refuse a returned tree that reaches a node twice, instead of walking it forever and leaving every case unreported or the run killed.
The serializers walked breadth first with no record of visited nodes. A visited set cannot tell a cycle from two parents sharing one subtree, and neither is a tree, so the check refuses both and the message names the repeated visit rather than claiming a cycle.
Notes
Closes #386.
Written for commit c84bfe0. Summary will update on new commits.