Repository navigation
Conversation
…ing the rest of the body (bendlang#2041) body_sub pushes onto a persistent list that body_force applies one statement at a time, so n destructures no longer copy the spine n(n+1) times or keep every copy alive. The flattened terms are unchanged, spans included.
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.
Fixes #2041.
The problem. Each destructure (
Box{x1} = x0) becomes a one-row match, andmatch_flattenapplies its two substitutions (the constructor, then the field's own var column) withbody_sub, which walked every later statement and the final expression and copied that spine on each walk: n(n+1) visits for n destructures. Every copy also stayed alive in thematch_flattenframes above it until their default branch was built, so peak live memory grew as n² as well: 25.6M objects (928 MB) at the deepest point of flattening for n=3200.The change.
body_subdefers instead of walking. It pushesx <- vonto aSubslist (newest first, immutable, shared by the rows) held by a{ $: "Subs" }body wrapper, in constant time.body_flattencallsbody_forceon the statement it is about to flatten, which applies the list to that one statement and hands the same list to its rows and its rest, to be forced when flatten reaches them. No spine is copied.subs_findfinds a binder's entries through aPMapindex built lazily along a list and kept on its nodes, so each node is indexed once: n destructures cost O(n log n).body_subkeeps its signature andmatch_flattenis unchanged.Why the terms are the same, spans included. The eager walks changed only three things, and
body_forcedoes each the same way at the statement:patt_term(v, e.s), so it keeps the use's span;Subnesting the walks built.The cached index depends only on the older nodes it points to, so every branch that builds it builds the same one. Flattening does no other work differently, and its fresh-name counter is untouched.
Load and check (
bend <file> --check-only, median of 7, Apple M1, 16 GB)Boxes (the issue's family)Flattening alone (in-process, median of 7): 713 ms → 13 ms at 3200, 3586 ms → 31 ms at 6400, and for each doubling past n=400 it grows 1.4 to 2.6 times where main grows 3.3 to 5.5 (operation counts: linear, plus n log n index steps). Live heap at the deepest point of flattening at 3200: 25.6M objects (928 MB) → 309k (15 MB). Base's flattening, warm: 2.62 ms → 2.66 ms.
After this PR the largest remaining cost on these programs is the parser's scope scan (358 of 439 ms of load at n=12800), which #2064 removes.
Checks
body_flattenresult, or the error it throws, serialized with its spans and hashed, for Base and every.bendundertests/(1853 files, 978,978 results, 24 errors), plus--check-onlyoutput and exit code: identical to main. The same over 1800 generated programs (nested matches, renaming var rows, destructures, lets, reuse of matched binders): identical.body_suband this one, applied to 50,000 random bodies with 220,383 random substitutions (repeated real substitutions of one binder, spanless vars,Sub-wrapped scrutinees), give the same body once forced. It fails on a lookup that takes the newest entry instead of the oldest (14,787 mismatches) and on reversed leaf nesting (44,361). No.bendprogram can tell the first of those apart today: across the corpus and the generated programs, every binder substituted twice had its var column's identity as the older entry (558,014 cases), so this script is what guards the ordering.-o t.js -o t.c -o t.bendttoutput (948 / 896 / 896 files), is byte-identical to main's, excepttests/proof/app/main.bend, which greets forever by design and was cut at the same 60 s timeout on both after a different number of lines. Run locally, 1842 of 1856 tests print their#|lines on both, and the same 14 fail on both (import modules that are not programs, and that loop).tests/flatten/: a 48-statement destructure chain, a binder renamed by a var row and then destructured, a catch-all row destructuring under both branches of the split above it, and the error window of a binder re-matched deep in a chain.gates/repo.ts: 56/56.bend.ts45673 → 46309 ttok (cap 48000); with perf(parse): field names, array updates, namespaces and later match parameters are found without rescans #2062 and perf(parse): a name finds its binder through an index, not a scan of every local #2064 applied as well, 46481. Both apply cleanly on top, and the differential and the generated programs still match with them on both sides.gates/test.tsandgates/perf.ts(no cluster access here),gates/ping.ts(no site checkout), and the Lean kernel of--verdict(Lean is not installed; the.bendtttext it checks is byte-identical, above).A question. The property test below lives outside the repo as reviewer tooling: the allow list admits only
.bendtests, and it compares against a second checkout of main. Should it become part of the repo (a gate, or a check ingates/test.ts), or stay with this PR?subs_prop.ts