Repository navigation
Each step of a conversion asks the rigid pass first - #1832
Open
Lorenzobattistela wants to merge 2 commits into
Open
Lorenzobattistela wants to merge 2 commits into
Lorenzobattistela wants to merge 2 commits into
Conversation
compare_go unfolded both sides before comparing them, and the rigid pass
(no def unfolds) ran only once, at the root of the goal. A copy that
lines up only after an outer def unfolds was evaluated on both sides:
{Box{big()} == boxed()} with big() = 2^30 overflowed the stack, and a
type holding a slow closed call next to a part that needs one unfold
re-ran the slow call on both sides.
Before compare_go unfolds anything, compare_call walks both spines. If
both are the same Ref and each pair of arguments is one atom (the same
cell, def, literal or bound variable, seen through cells and
annotations, nothing reduced), the calls are equal by congruence. If
not,
nothing is decided and the comparison goes on as before. A failed try
costs one step per argument. Comparing the arguments recursively instead
made compute_1600 1.4x slower, because the walk down unary chains was
repeated at every level. A non-atomic argument is still caught one
unfold lower, where its own call meets its copy.
The atom shortcut caught only calls of one def on the same atoms, so a
copy that agrees only after a few steps (a def naming another, another
def with the same body, equal arguments written differently, a call or
a constructor as the argument) still overflowed. compare_go now asks
the rigid pass (no def unfolds) at every step of the full pass before
it unfolds anything, and the rigid pass at the root of term_compare is
that first step. Both sides evaluate in step, so once they reach the
same text the walk stops there.
The cost is in proofs by computation over large data: compute_1600 is
1.35x its pin, and {add(a, b) == add(b, a)} on unary numbers grows
quadratically, as each rigid try walks the long common prefix again.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Each step of a conversion asks the rigid pass first, so a copy met past an unfold is not evaluated
Addresses the problem in #1232 with a different, stateless change. Thanks to @Giulio2002 for the report and to @Tor-Saxberg for the minimal case.
Problem
compare_gorunsterm_wnfon both sides before it compares them. The rigid pass (RIGID, the empty book, where no def unfolds) runs only once, at the root of the goal. When two copies of a call line up only after an outer def unfolds, the full pass evaluates both copies:The rigid pass fails at the root (
CtragainstRef). The full pass then unfoldsboxed()and meetsbig()againstbig(): the same def, but two different nodes, so===misses. It unfolds both and walks 2^30Succcells: stack overflow.The same happens in types, where it costs time instead of stack:
Here
Same(Unit)is not rigidlyUnit, so the full pass normalizesHeavy()on both sides. The time doubles with each step ofslow.Solution
compare_goasks the rigid pass at every step of the full pass, before it unfolds anything:term_comparenow just callscompare_go: the old rigid pass at the root is the first of these steps.It catches more than the exact repro. Both sides evaluate in step, so once they reach the same text the walk stops there.
tests/check/conversion_call_after_unfold_near.bendhas five such cases, and each one overflows on main:mid2() = mid());pow2(Nat.add(8n, 8n))againstpow2(16n));An earlier version of this PR (b0e4476) caught only calls of one def whose arguments are the same atoms. It cost nothing (compute_1600 1.03x), but every one of these five cases still overflowed.
Cost
The rigid try walks the two sides until their first difference, and it does so again at every step. Where the two sides are long, nearly equal concrete data that differ deep down, that walk repeats down the chain, which is quadratic. This happens in proofs by computation over large data:
Proofs that move the computation into a lemma do not pay it.
add_commby induction with rewrites, then used asadd_comm(a(), b()), checks at 1.00x for n = 3000.compute_1600 is 1.35x its pin, so
gates/perf.tsfails (slack 1.15). Merging this needs the pin re-recorded.Measurements
Whole runs of
bend <file>(startup, parse and check), median of 5 to 11 interleaved rounds on one M-series laptop, mainaee1b1f2against this change:boxed_eq(2^30)castwithslow(22n)castwithslow(26n)_near.bend(2^16)--check-onlyover the 1,707 test filesadd(a, b) == add(b, a)by{==}, n = 1500add(a, b) == add(b, a)by{==}, n = 3000len(append(xs, ys)) == len(append(ys, xs)), n = 3000bend.tsgoes from 43,767 to 43,798 ttok (cap 48k).Tests
tests/check/conversion_call_after_unfold.bend: the repro, its mirror, and a literal argument (boxed_at(30n)). Stack overflow on main.tests/check/conversion_call_after_unfold_type.bend: thecastcase withslow(26n), through theLEconversion (check-any). Does not finish in 60 s on main.tests/check/conversion_call_after_unfold_near.bend: the five cases above, at 2^16. At 2^30, BendTT runs out of fuel on four of them.tests/check/conversion_call_after_unfold_differ.bend: calls of one def on different arguments must still fail (Box{8n}againstBox{16n}). Same output on main; this test guards against a false accept.Gates
On main
aee1b1f2plus this branch:gates/safe.ts tests: agree 1113, false reject 0, timeout 0.bfsPAR-GPU at 1.18x, a runtime cell the checker does not touch.