and others added 5 commits
UnifiedIR is one IR data structure intended to span the compiler pipeline and external compilers (design specification in UnifiedIR/docs/design.md): a flat statement table with hybrid regions over a shared storage core (AttrGraph: kind column + packed two-mode operand words + one tagged operand pool + open attribute-column universes), layout states (builder/dense/editable/floating) with exactly two renaming points, a namespaced kind registry with statically reserved dialect ids for the bootstrap stack, a generic tree porcelain (Tree/NodeList cursors, construction, mapchildren/copy_ast, provenance walks over graph-qualified :source chains), compact!-as-GC (compact_graph!/collect_syntax!), verifier, printer/parser, and a reference interpreter. Zero dependencies. Wiring: UnifiedIR joins TOP_LEVEL_PKGS (symlinked to the julia share directory), is bootstrapped into Base immediately before JuliaSyntax (base/Base.jl) — JuliaSyntax's SyntaxGraph runs on this substrate — and its sources join COMPILER_FRONTEND_SRCS so sysbase rebuilds on change. The Base-baked instance keeps its kind registry clean of the test dialect (registration is gated to package mode). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One storage core: SyntaxGraph wraps UnifiedIR.AttrGraph — kind is a real
core column (the :kind attribute is a Dict-shaped view of it), children are
STMT-tagged words in the shared operand pool addressed by packed range
words, and the historical edge_ranges/edges/attributes properties are
preserved exactly (identity-stable views; is_compatible_graph semantics
kept). compact_graph! gives lowering the GC over syntax graphs it always
wanted.
One porcelain: SyntaxTree{Attrs} = UnifiedIR.Tree{SyntaxGraph{Attrs}} and
SyntaxList = UnifiedIR.NodeList — child indexing, attribute properties,
newnode/newleaf/mknode/mkleaf/mktree, copy_attrs!/mapchildren/copy_ast,
provenance walks, structural isapprox and printing are UnifiedIR generics;
JuliaSyntax keeps only the genuinely syntax-specific parts (the Kind
registry wrapper, SourceRef/source-text machinery, the leaf payload
convention, parser integration, prune/unalias/@stm).
One kind registry: the historical module-id mechanism re-plumbs into the
shared UnifiedIR registry through register_kinds! — kind modules become
dialects claiming contiguous opcode blocks (range predicates and
BEGIN_/END_ markers preserved), the bootstrap stack claims statically
reserved dialect ids (JuliaSyntax=1, JuliaLowering=2, formatter=3; core=0)
so K"..." literals stay compile-time constants, and syntax K"call" and
core K"call" are distinct kinds sharing one numbering space. Kinds
re-register in __init__ (the registry is UnifiedIR session state);
numbering is deterministic, keeping baked constants valid.
Dual-mode: bootstrapped into Base it binds Base.UnifiedIR (included just
before it); as a package it depends on the UnifiedIR package.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
JuliaLowering.UnifiedBackend (additive; zero changes to existing sources): reuses the front half unchanged — macro expansion, desugaring, scope analysis, closure conversion — and emits structured UnifiedIR region form directly (if/loop/try region ops, cells for frame variables, sealed exit terminators) instead of goto linear IR, with graph-qualified provenance: every emitted statement carries a :source column entry holding the originating syntax tree cursor, so the generic provenance walk crosses from optimized IR statements back to surface text (highlightable diagnostics), and collect_syntax! can GC the lowering graph against live IR. Pure definitions; nothing calls it during bootstrap, and it bakes cleanly in the optional sys-JL sysimage stage. Bindings resolve through the enclosing JuliaLowering (same JuliaSyntax and hence same UnifiedIR instance in both Base-baked and package modes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on UnifiedIR The provider side of the substrate (package-mode ONLY; the sysimage bootstrap never sees it): CodeInfo <-> UnifiedIR boundary converters (cfg-wrap entry incl. exception handlers; goto/typed exits with phi synthesis), an inference port running natively on UnifiedIR (differentially validated at 100% equal-or-better return types against the stock pipeline on session MethodInstances), optimizer passes (SROA, inlining via splice_body!, ADCE, structurization, cell promotion), the Queries API, and activation through the ordinary compiler-replacement mechanism (Core.OptimizedGenerics.CompilerPlugins.typeinf owner hook + with_unified_compiler), with every cached result round-tripped through a verified UnifiedIR shadow. Loading: Compiler.load_unified!() includes the port on demand into a Main-rooted carrier module and binds it as Compiler.Unified. On demand because the stdlib stub fast-path bypasses module evaluation when the package matches the sysimage copy; Main-rooted because this baremodule rebinds getproperty to raw getfield for its nested tree, which would break property-forwarding types. The plugin overload pins invoke_in_world to an end-of-module sentinel world so the shadow hooks are visible under both pkgimage and interpreted loading. Tests in Compiler/test/unified/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- UnifiedIR/test wired into the test harness (choosetests + the runtests_vendored project-activation pattern of the other top-level packages): substrate, porcelain, kind registry, provenance walk, compact!/collect_syntax! GC, acceptance/fuzz/splice suites. - UnifiedIR/demo/provenance_demo.jl — the one-stack demonstration under the built binary: parse -> JuliaLowering front half -> UnifiedBackend.lower_to_ir (graph-qualified :source cursors) -> Compiler.Unified inference + UnifiedIR optimization -> highlight() of the exact surface text an optimized statement came from -> print_ir with per-statement source excerpts -> collect_syntax! (conservative and prune policies) -> the same highlight, byte-identical after AST GC. - NEWS.md entry under compiler/runtime improvements. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keno
mentioned this pull request
typed_ir returned the IR handle but its REPL display was the compact one-liner, which is useless for exploration. IR now shows as the full print_ir listing under MIME text/plain by default; truncation is opt-in, globally via display_maxlines!(n) or per-stream via IOContext(io, :ir_maxlines => n), with a tail note. Core.Const lattice types render as Const(value) instead of the opaque lattice fallback. @code_unified is the @code_typed-shaped entry point: argument values or bare ::T annotations, optimize=false for inference-only IR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keno and others added 20 commits
In Julia, `yield` reads as task suspension (Base.yield), and the name should stay available for genuine suspension points in the eventual await/continuation form (design doc §5.6). The region-value terminator is now `result`, pairing with `region_arg`: regions take region_args and feed their owner through result terminators. Mechanical rename across the core dialect, printer/parser, converters, optimizer, the lowering backend, and the design document; no semantic change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add promote_arm_cells!, the if-join leg of §6 cell promotion, by store sinking: conditional stores in sibling arms become per-arm result values plus one unconditional post-join cell_set (extract-indexed when several cells tuple through one if), with the incoming value materialized as a cell_get before the if under definite assignment. Diverging arms contribute nothing; handler-observable, maybe-undef and token cells refuse per the §6 inviolables. In-arm reads reached by arm stores are rewritten through the reaching store, re-read at transform time so the rewrite is order-invariant across cells. optimize_ir! now runs arm and loop promotion to a joint fixpoint: arm sinking produces exactly the unconditional store shapes the dominating and loop passes consume, so each round can expose new cases for the others. This makes the conditional-swap shape in Base.gcd (a, b = b, a under an if) promote fully: typed_ir(gcd, (Int,Int)) retains zero cell ops, and the corpus optimizer wall drops ~18% (the earlier promotion shrinks the IR for every later round). The passes record their join placements in PROMOTION_TRACE when the completeness harness asks for them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the fourth §6 join-point class: liveness-pruned, IDF-placed SSA construction for frame cells over cfg-op block graphs. Phis are block region args; incoming values ride the edge bundles of block terminators and of sealed exits of nested islands that land on our blocks mid-block (§5.5) — a mid-block edge exports the reaching definition at the sealed sub-op's member position, which stays well-defined through handler- nested exits because candidate stores are all direct block members and cannot execute mid-try. The incoming value is a cell_get inserted before the cfg op under definite assignment, folded by the dominating pass next round. Entry-leading cell_new (NewvarNode form) is honored by invalidating the incoming value. Soundness rule found by the DF harness: when a loop body lies strictly between the cfg op and the cell declaration, cell memory is carried ACROSS iterations through the island's sealed continue/break exits, so deleting island stores would leave the next iteration's incoming read stale — such cells refuse unless the island is store-free for them. Dissolving that class needs exit-value threading through sealed exits (deferred; the dominant remaining stock-oracle exception). promote_arm_cells! now also fires inside island blocks — its transform is local to the if's region subtree, and its post-join store is exactly what the island and block passes consume — and optimize_ir!'s promotion fixpoint includes the island pass. Stock-oracle violations on the Base/stdlib corpus drop from 38 bodies to 16, all in the documented exit-threading class; corpus cell-op elimination rises from 20% to 55%. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Substantiate that the §6 join vocabulary — if results, loop region args, loop break values, island block args — projects the iterated dominance frontier onto the region tree, three ways: - classify_residual_cells: every cell surviving the isolated promotion fixpoint (promotion_fixpoint!, the five promotion passes to a joint fixpoint plus dead-cell dropping) carries a documented exception class (escape/token, throw-edge/handler, maybe-undef, island exit-threading, refused multi-level exit); UNCLASSIFIED is a completeness bug. The stock oracle demands the classes match stock's own outcome: where code_typed fully mem2reg's (slot-, Box-, and undef-guard-free), no stray residuals may remain. - CellFuzz: a structured fuzzer emitting interpretable core-dialect bodies with random nested if/loop/try, sibling-arm swaps, multi-level exits, guarded maybe-undef reads, and handler interactions; each case runs a semantic differential (same values, same thrown errors) across the fixpoint, plus classification totality. 10k cases clean. - df_correspondence: flatten the pre-promotion body through the exit converter, compute stock slot2ssa's liveness-pruned IDF per cell, and check the recorded promotion placements (PROMOTION_TRACE, id-translated round-by-round through the compaction RemapSets, region ids for island phis) land exactly on those blocks; statically dead code is excluded on both sides. Zero missing placements on the corpus and fuzz samples. The DF leg caught two real bugs during bring-up: an order-dependent stale-operand rewrite in the arm pass and the island backedge-staleness unsoundness — both fixed in the pass commits. bench/unified_completeness.jl runs all three legs at scale (600-body corpus, 10k fuzz, seeded); test/unified/completeness.jl carries the regression shapes: the gcd swap, nested depth 3, all-diverging-but-one, maybe-undef and throw-edge refusals, island phi placement and the staleness refusal, and a seeded 800-case battery. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The claim, the five promotion passes and their IDF projection, the backedge-staleness rule, the machine-checkable exception classes, and the three-legged verification harness (classifier + stock oracle, structured differential fuzzing, dominance-frontier correspondence). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The extract index was 0-based (an LLVM extractvalue habit). This is Julia: the index is now exactly getfield's, so the extract<->getfield mapping at every boundary (canonicalization, converters, tfunc) is the identity instead of a +-1 that each site had to remember. Producers, consumers, tests, and the design document flipped together. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
promote_cells! refused any cell touched inside a cfg island wholesale. The dominating case is region-local, so the refusal was stronger than needed: dominates_for_cell only walks region nesting, meaning a store proves dominance either from outside the island (it executes before the whole cfg) or from the same block subtree (blocks execute their members in order, and re-entry re-executes from the top, so the store re-executes before the get on every iteration). Cross-block flow never establishes region-static dominance, so nothing unsound gets through. The one genuine island hazard is a positionally-later store reaching an earlier get across island back edges — invisible to shares_loop, which only knows structured loop bodies. The blanket refusal was load-bearing for exactly that case; it is now guarded precisely by shares_island in the backedge-reach test. This lets single-arm store/read clusters inside island blocks promote, composing with the arm and island passes. The interpreter's undefined-cell-read diagnostic now names the reading statement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The one deferred capability behind every remaining stock-oracle violation: a loop whose backedge originates inside a cfg island (the escape_string shape — body collapsed to a goto island, continues carrying no values) kept its cells in memory because deleting the island stores would leave the next iteration's incoming read stale. Island side (promote_island_cells!): - When the island-entry value is OBSERVED (a symbolic resolution over reads, live phi edges, and the entry phi decides), the value THREADS: the loop grows a carried arg whose init is the store reaching the loop, every continue targeting it appends the reaching definition at its exit point (the pass's own per-block dataflow, including mid-block sealed exits and exits through handlers), and island reads take the arg. An UNOBSERVED entry value needs no threading at all. - Post-loop reads keep memory, fed by ONE unconditional store of the loop's exit value (the exit result is the carried tuple; the loop's result must be otherwise unconsumed, and breaks refuse). - cell_new generalizes from entry-leading to per-block KILL events in the dataflow; outside cell_news in the declaration pattern are accepted. - The island entry block counts its implicit in-edge as a join: with a single internal backedge the entry phi was previously missed, reads collapsed to the pre-island constant, and the island spun forever. Loop side (promote_loop_cells!), the compositional half: - Loops that already carry values compose: new args/inits/continue values append after the existing ones; the exit tuple indexes past slots an earlier promotion claimed (breaks pad with nothing); a loop whose result some user consumes only takes pure carried promotion. - Post-loop reads that cannot anchor for direct exit threading — and post-loop STORES, previously a wholesale refusal — SINK one unconditional store of the exit value right after the loop, keeping pre-loop stores for the paths that skip it. Each fixpoint round then consumes the sunk store at the enclosing level (countlines' inner levels resolve this way). - The carried-arg INIT gets a backedge-hazard rule: any other store sharing an enclosing loop with L reaches L across that backedge, so the region-static init is stale from iteration 2 unless it re-executes inside the INNERMOST loop that store shares with L. Pregets use the same per-store innermost rule (a dominating store inside the shared loop shadows the carried-back value). Doubly-carried cells (the inner init would need the outer carried value) refuse, classified :refused_multilevel_exit, and execute correctly through memory. Soundness rules the extended fuzzer differential forced, all with seeded regressions: - store sinking refuses when the rewritten region sits in a try body while the cell has uses outside it (§6 throw-edge: a swallowed exception exposes mid-try memory to post-try readers) — applied uniformly to the arm, loop, and island passes; - a JOINING arm can leak mid-arm through a nested sealed continue/break whose target lies outside the arm: its stores stay in memory (the post-join store merely re-stores the same value on the join path); exits to loops inside the arm and frame-ending returns do not leak; - _reach_ed's diverge-cut (a diverging terminator on the store's side cuts fall-through) applies only to the loop pass's per-iteration queries, where the carried arg covers backedge flow — memory-semantics callers keep seeing diverged stores. Stock-oracle violations drop from 16 bodies to 2, each an analyzed boundary: code_lowered (definite assignment provable only path-sensitively — stock proves it during inference constprop, and §6's maybe-undef rule is inviolable) and countlines (one doubly-carried cell). escape_string's kwbody reaches ZERO residual cells; corpus cell elimination rises from 55% to 70%. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CellFuzz grows genisland!: cfg islands of 2-3 blocks with cell traffic, forward br_if/goto edges, bounded internal backedges, result joins, structured ifs/loops inside blocks (islands nest through them), and — under an enclosing loop — sealed continue exits straight out of a block (the escape_string shape). Termination stays guaranteed: private counters gate every backedge, and backedge blocks always keep a forward false edge. The differential immediately earned its keep, catching four real promotion miscompiles (the missed entry phi on single-backedge islands, the stale carried-arg init on doubly-carried cells, store sinking across swallowed-exception boundaries, and joining arms leaking through nested sealed exits) — fixed in the pass commit, pinned here as seeded regressions. Interpreter diagnostics carry statement ids now, so outcome() strips them before differential comparison. completeness.jl adds the threading testset (the escape_string textual shape, the single-internal-backedge entry-phi regression, the doubly-carried refusal) and the seeded miscompile-regression battery. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move exit-value threading from deferred to implemented: the threading rules, the init backedge hazard, the unobserved-entry shortcut, loop store sinking, and the narrowed exception rows (:island = path-sensitive definite assignment; :refused_multilevel_exit = doubly-carried cells). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
promote_cells!'s backedge-reach hazards (a later store reaching an earlier get across a loop backedge or island re-entry) learn the innermost-shadowing rule: the hazard is void only when the get's reaching store re-executes before the get on every such re-entry — inside the INNERMOST loop, and under the INNERMOST cfg island, that the interfering store shares with the get. Matching an outer island was not enough: an island counter initialized in an outer island's block and carried around an inner island's backedge was constant-folded to its initializer, spinning the inner island forever (caught by the fuzzer differential as a live non-termination, seed 24242 case 153). fold_constant_branches! only inlines JOIN (result-terminated) arms: splicing a diverging arm mid-region leaves two terminators, and truncating the tail behind it silences the verifier exactly when a fold acted on unstable mid-round inference. The interpreter gains an optional execution budget (_FUEL): harnesses set it per run so a non-terminating miscompile becomes a diagnosable failure naming the statement instead of a hung batch. Undefined-cell reads throw UndefVarError(:cell) — the same error the definedness-as-data guards throw — so differentials compare thrown errors exactly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
structurize!'s structural rebuilds move statements wholesale, and the moved statements' order keys could end up inconsistent with the (correct) edit lists: comes_before then lied about pre-loop stores vs in-loop reads, silently corrupting every order-based analysis downstream. On real bodies this manifested as bogus maybe-undef verdicts, unbounded definedness re-splitting (67 carried args on a 2-arg loop), premature constant folds of definedness guards, and a miscompiled s_mypow. One relabel_okeys! from the edit lists at the end of the pass restores the invariant. This was a latent landmine for every editable-order consumer since structurize! existed; the promotion machinery merely made it visible. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d args Three mechanisms close the remaining ordinary-slot residual classes; the 600-body stock-oracle violation list is EMPTY. promote_undef_cells! (definedness as data): maybe-undef cells rewrite into a definitely-assigned value cell (dummy-initialized at the declaration) plus a parallel Bool definedness cell — stores set it, cell_new clears it, cell_isdefined reads it, and undominated reads acquire a joining guard (if !def; throw(UndefVarError); end). Both cells dissolve through the ordinary join machinery, placing the Bool joins on exactly the classical dominance-frontier points; guards constant-fold wherever definedness is provable. Kill-aware (with non-declaration cell_news every read is guarded) and idempotent (cells split only when the declaration-point initializer will dominate every rewritten use — editable order, not dense ids). Island totality: non-dominating outside uses BEFORE a threaded loop are plain memory traffic (conditional pre-loop stores just poison the carried init); outside declaration-pattern cell_news accepted; store→site dominance may cross guard regions (a guard evaluates entirely after the store whenever the site runs at all). Nested carried args (MEMORY-INIT): a carried init that is stale across an enclosing backedge becomes a cell_get immediately before the loop, with the exit store keeping that memory current — the enclosing loop's next fixpoint round consumes the get/store pair as ordinary body traffic, so doubly-carried cells (countlines) dissolve one nesting level per round. Refused when a multi-level exit inside the body can skip the exit store (caught as a live miscompile by the differential). Loops whose result some user already consumes rebase scalar consumers through extract(L, 1) before the exit tuple grows. optimize_ir! runs the promotion fixpoint once more after the round budget (late-inlined callee cells were arriving after the last round), and the round-internal fixpoint includes the undef split. The residual taxonomy is two-category (completeness.jl): the v1 representation choices :handler_crossing (pending exception SSA), :gc_token (pending pairing-rule values), :box_capture (producerless) — everything else is a bug the harness fails on. The DF harness credits the definedness Bool's placements to the original cell, counts NewvarNode kills as stock defs, and the stock oracle machine-checks PhiC/Upsilon (or stock undef guards) for :handler_crossing and Box for :box_capture. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…able CellFuzz gains uninitialized cells and mid-flow cell_new re-undefines (the definedness-as-data shapes); interpretation runs under a fuel budget so hangs become diagnosable failures; statement ids are stripped from error strings before differential comparison. Severity split: classes the machinery eliminates by design (:maybe_undef_read, :escape, :UNCLASSIFIED) fail the run; :island/:refused_multilevel_exit — the two open classes on adversarial fuzz shapes, zero corpus-wide — are counted and reported as open work. bench/unified_fresh.jl prints the acceptance table from one cold process: all ten escape_string kwbody specializations, gcd/_gcd, countlines, code_lowered — every headline number reproducible by a single command, with an in-process determinism check. Tests updated to the new contracts, and the seeded miscompile-regression battery pins every bug family found this cycle (stale carried inits, sealed-exit skips, order-key corruption, inner-island constant-folds, definedness kill-awareness). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`if %6 :: Float64 {` read as an assertion about the condition (and
`loop (inits) :: T {` as one about the init tuple). The statement's
result type now trails the final closing brace — `} :: Float64` —
where it unambiguously belongs to the owner. Printer, parser, golden
tests, and the design document moved together.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
index (orientation + where things live), tour (hands-on from @code_unified through the builder, text round-trip, and the reference interpreter), concepts (statements/regions/layout states/kinds/columns/ cells), the text-format reference, and a pass-writing guide (mutation vocabulary per layout state, the RemapSet discipline, visibility rules, a checklist). The normative spec remains docs/design.md; these pages are the accessible entry points. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Move the join-point cell-promotion passes out of Compiler/src/unified/sroa.jl into UnifiedIR/src/promote.jl: forward_if_results!, promote_block_cells!, promote_arm_cells!, promote_loop_cells!, promote_island_cells!, promote_undef_cells!, their dominance/reachability helpers, PROMOTION_TRACE, and is_diverge_kind (from structurize.jl), plus a new promote_fixpoint! driver (the promotion-only joint fixpoint optimize_ir! previously inlined). The passes are Julia-semantics-free; the one type-lattice consultation (loop-carried promotion's unconditional-continue check) becomes a stmt_value hook, with a substrate-default operand_static_value that reads inline/pool/global constants only. Compiler.Unified binds the suite back under its old names and passes its inferred-Const reader as the hook, so the optimizer pipeline is unchanged; lowering (JuliaLowering's capture analysis) can now run the SAME machinery — shared functions, not a copy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AecTAWDkYTxeaTQYT8NEhM
The exception-join slice of the PhiC/Upsilon successor plan (§6): a frame cell stored in a try's body and/or handler gains one unconditional post-try store of the try's threaded result. Nothing moves or disappears — on every path reaching the join the threaded value equals the cell's memory (last direct arm store, or the incoming value for a storeless joining body under definite assignment), so the inserted store is a semantic no-op whose only effect is giving promote_cells! a dominating definition below the throw edge. Handler reads keep observing memory (§6 throw-edge rule untouched); joining handlers without a store are refused while the body stores (an exception between body stores and the terminator would reach the join with memory the thread cannot name); the idempotence trigger requires a post-try read the new store would dominate, guaranteeing one firing per (try, cell). Wired into optimize_ir!'s joint promotion fixpoint, promote_fixpoint!, and the completeness harness's promotion_fixpoint! (the cellfuzz differential covers it: handler-store-read-after-try shapes now promote — fuzz residual :handler_crossing 1037 -> 1031 per 800 cases, execution differential green). This is what turns try/catch definite assignment into value captures for the closure-capture analysis (julia#15276). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AecTAWDkYTxeaTQYT8NEhM
Decide boxed-vs-value capture with the shared UnifiedIR cell-promotion machinery instead of the syntactic assigned-once rule. resolve_scopes now runs analyze_captures_precise! after the flisp-parity def/use pass; it only WIDENS BindingInfo.unboxed (monotone over stock — never a regression). Per lambda, per captured native variable v and creation site C, value capture is legal iff (a) no lambda capturing v stores to it (tree fact), (b) no store to v can execute after C — forward order via the shared comes_before / _may_reach helpers plus the multi-shot loop-backedge rule, where a re-declaration inside the shared loop (fresh binding per iteration) cancels the hazard, and (c) a single defined value reaches C, joins included — which is precisely cell promotion: the lambda body is lowered to throwaway analysis IR by the UnifiedBackend emitter in a new capture-analysis mode (candidates forced to frame cells; creation sites emit CAPTURE_SITE markers holding one cell_get per captured candidate; nested lambda bodies skipped; unknown forms become checked-opaque calls), and the verdict is whether UnifiedIR.promote_fixpoint! — the SAME fixpoint Compiler.Unified runs — resolved the site's cell_get to a reaching definition. The fixpoint runs without definedness-as-data so maybe-undef captures keep the shared container and use-time UndefVarError semantics. Newly value-captured (boxes gone, type instability with them): variables assigned in both if-arms before capture, try/catch definite assignment (via the new promote_try_cells!), conditional assignment feeding comprehensions/do-blocks. Stays shared, exactly as before: mutation after capture, multi-shot loops, closure-written captures, maybe-undef captures, recursive local functions. Any form the analysis emitter does not model bails to the stock verdicts (UnsupportedForm; ACP_STRICT rethrows internal errors for tests). Test suite parity: 8624 pass / 113 broken / 0 fail, both sides; the new capture_analysis testset seeds the must-stay-shared cases as executable soundness sentinels. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AecTAWDkYTxeaTQYT8NEhM
Keno and others added 19 commits
When the capture analysis proves a variable must stay shared, type the
container: BindingInfo.box_type carries a container type (Base.RefValue{T})
that closure conversion threads everywhere Core.Box appeared — the closure
type field (eval_closure_type accepts a Type alongside the box flags), the
declaration-point allocation, setfield!/getfield/isdefined accesses (field
:x instead of :contents). Eligibility is decided on the same analysis IR as
the boxing verdicts: T must be provable at lowering time (a declared type
resolving to a constant — conversion already funnels every store through
convert — or the join of all store RHS literal types, collected across
nested lambda bodies too), and the variable must be undef-safe (every home
read/query and capture site store-dominated per the shared §6
dominates_for_cell checker, declaration-position cell_news only) because a
typed container's empty state can be unobservable — maybe-undef captures
keep Core.Box and their use-time UndefVarError.
This types the boxes precision cannot remove: mutation-after-capture with
literal stores becomes RefValue{Int} (the whole function then infers
concretely), declared-type counters and multi-shot loop captures become
RefValue{T} while keeping shared-mutation semantics. Untypeable stores
(x = x + 1 on an undeclared variable) honestly keep Core.Box.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AecTAWDkYTxeaTQYT8NEhM
lower_to_ir already covers ordinary closures (conversion lifts closure types and methods to the toplevel thunk, so enclosing bodies and closure bodies lower as plain methods); the module docstring claimed otherwise — fix it and seed an in-tree corpus (test/unified_backend.jl): 16 sources, closure cases included (value captures, Core.Box, typed RefValue containers, generator, do-block), each lowered, L1-verified, and differentially executed on the reference interpreter against native definitions, plus undef-semantics and no-Box-in-IR assertions. UnifiedIR/demo/capture_zoo.jl is the cold-runnable julia#15276 demonstration: per-case capture decisions and inferred return types (stock flisp vs the mem2reg analysis), a 12-point execution differential (mutation visibility, multi-shot loops, undef errors — must match), and microbenchmarks (escaping join capture 29x, typed counter 14x on this machine). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AecTAWDkYTxeaTQYT8NEhM
Document the implemented pre-closure-op slice of §5.7: the exact value-capture legality criterion (no closure-internal stores; no store executable after the creation site, with sibling-arm exclusivity and the multi-shot backedge/fresh-declaration rule; definite assignment through joins = cell promotion), where it runs, and how maybe-undef and typed containers behave. docs/closures.md is the worked julia#15276 zoo: per-case decisions, why each holds, the regression sentinels, and the honest limits of lowering-time typing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AecTAWDkYTxeaTQYT8NEhM
Add JuliaLowering.ACP_TRACE (an observation hook, nothing = off): the capture analysis hands the callable its throwaway UnifiedIR as emitted and again after UnifiedIR.promote_fixpoint!. The demo prints both listings for three instructive cases: the if-arm join whose CAPTURE_SITE marker operand resolves to the if result (value capture), mutation-after-capture — where the marker also resolves but the criterion-(b) pre-check on the emitted IR (the cell_set visibly AFTER the marker) vetoes value capture — and the maybe-undef capture whose cell survives the fixpoint outright (no definedness-as-data in analysis mode), keeping the shared container. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AecTAWDkYTxeaTQYT8NEhM
…ure ops The obvious question, answered where people will look: in the §5.7 closure region form the captured reads sit behind an activation boundary the §6 promotion rules refuse by design (ACT_IMMEDIATE gates), so decisions only fall out structurally once the P3 late-capture visibility machinery exists. The CAPTURE_SITE marker is the degenerate immediate form — snapshot reads at the creation point, judgeable by the v1 fixpoint as-is — and collapses into the closure-op formulation when that lands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AecTAWDkYTxeaTQYT8NEhM
Give the already-registered K"closure" kind its activation semantics: one ACT_DEFERRED REGION_BODY region whose leading region_args are the params, an optional INLINE flags operand (bit 1 = isva), and the closure value as the op result. The L1 verifier gains the closure deltas: the result-feeding class (a `result` whose region is owned by a closure is an error — bodies exit via return/unreachable), the cell-class boundary rule (a cell op reaching a frame `cell` across an ACT_DEFERRED boundary requires cell_shared; the ACT_RESUME variant is edge-defined for cfg await and deliberately not keyed), closure-op operand/region discipline, an activation check on cfg edge bundles, and the rule that only closures own deferred regions. Dead regions become invisible to verification until compact! drops them, which lets dense-state DCE tombstone whole closure subtrees. Effect composition is now mode-aware (§3.3): closure creation registers as REMOVABLE, UnifiedIR dce! deletes unused closures on their creation-site flags alone, and Compiler.Unified's adce_region_ops!/removable_subtree skip deferred regions instead of refusing them (resume regions stay refused). The reference interpreter executes closures via a callable UClosure: the creation-time snapshot takes the free slots from the new closure_environment(ir, s) analysis (the single source of truth for the derived environment — ordered values and cells referenced from the deferred subtree but defined outside), values by value and CellBox cells by reference, so cell_shared sharing and multi-shot loop snapshots fall out. Call-time exceptions unwind to the call site's handler for free. The textual format prints/parses the closure form (isva as a trailing `...`). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…xpoint The mem2reg suite gains the capture-promotion pass (§5.7): a `cell_shared` cell read inside deferred regions is rewritten to value captures when the precise criterion holds. (a) no write to the cell inside any deferred region and (b) no write can execute after any home-frame closure site — the criterion-(b) machinery (comes_before + _may_reach sibling-arm exclusivity + the multi-shot backedge rule) relocated from the capture-analysis sidecar onto IR positions, with the fresh-binding-per- iteration cancellation in its structural form: the backedge hazard is cancelled only when the CELL ITSELF is declared inside the shared loop (a fresh shared box per iteration). A `cell_new` deliberately does not cancel — on a shared cell it re-undefines the one box every extant closure aliases, so it counts as an observable write instead. (c) — a single defined value reaches each site, joins included — is judged by the standard fixpoint itself on a throwaway scratch copy: the candidate is demoted to a frame cell there, one probe cell_get per site is planted at the creation point under a marker call (the sidecar's device relocated onto real IR), and the fixpoint runs without definedness-as-data; an unresolved probe means maybe-undef or ambiguity and the cell keeps the shared container with use-time UndefVarError. Committing mirrors the judged copy — probe before each site, in-deferred reads rewritten to it (a legal by-visibility capture), cell demoted to frame class — and later rounds finish the home promotion, so a fully-resolved cell disappears. The `boundary` parameter documents the await seam (resume = live-at- suspension snapshot replacing (b)); only :deferred is implemented. The activation-guard audit found one wrong-altitude gate: _promote_cells_of_loop! refused whenever the loop's OWN region was non-immediate, blocking all activation-LOCAL promotion inside closure bodies; the gate now applies to regions the enclosing-arm chain crosses. All other helpers hold: dominates_for_cell/_cell_dominates_ed/ _body_path_ok/_post_anchor gate boundary CROSSINGS only, and the frame- cell boundary rule (L1) plus the cell_shared kind filter keep every other pass from ever comparing positions across activations. Tests: IR-level mirrors of the JuliaLowering capture sentinels (arm/try joins resolve; store-after-creation, store-inside-closure, multi-shot backedge, maybe-undef all stay shared; fresh-cell-in-loop cancellation; nested transitive captures; activation-local promotion inside a deferred body; no-op on closure-free IR) plus a seeded closure-shape fuzz battery (300 iterations, 517 closures) with interpreter differentials before and after the fixpoint in both configurations, through a textual round trip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ization
lower_to_ir's default path now consumes the PRE-closure-conversion tree
(post-resolve_scopes): supported local functions emit real `closure` region
ops (params as deferred-region args), and the emitter stops deciding
captures — captured mutable locals get `cell_shared` plans (created at
their K"local" site when one exists, so a loop-local declaration yields a
fresh shared box per iteration), assigned-once/dominating captured locals
stay plain SSA values that in-deferred reads reference by visibility, and
unassigned captured arguments capture their region_arg directly. The shared
promotion fixpoint (promote_capture_cells! included) then decides
value-vs-shared structurally. Scan/planning cover nested method lambdas as
conditional subpaths; frames are entered/exited with their own handler,
break-target, sparam, and return-type scopes; is_ssa desugaring temps (which
scope analysis keeps out of locals_capt) are owned by the frame the scan saw
them in.
Materialization (materialize.jl) turns residual closure regions back into
runtime closures: the capture set IS closure_environment; the type comes
from the existing eval_closure_type machinery (value captures
type-parameterized, surviving shared cells Core.Box or the binding's proven
Base.RefValue{T}); the deferred region is extracted into a standalone
method IR (region 1 = #self# + params, captures via getfield, shared-cell
ops via container field ops, nested closures recursively); the op becomes
apply_type + `new`; and a trampoline method on the new type interprets the
extracted IR, so instances are callable and differentials cross the call
boundary inside UnifiedIR with no aliasing to natively-created types.
Surviving shared cells in the frame lower to the same containers, keeping
use-time UndefVarError with the right variable name through the emitted
guards. promote_capture_cells! learned to accept in-deferred cell_isdefined
guards: a resolved probe proves definedness at the creation site, which
under (a)+(b) equals definedness at call time, so the guards rewrite to
`true` on commit (maybe-undef cells still keep them and the shared
container).
v1 bails (multi-method/kwargs local functions, `where` sparams, varargs,
declared return types on local functions, opaque closures, captured_local,
signatures referencing locals, enclosing-sparam captures) fall back
per-top-level-statement to the eager convert_closures path — loudly, per
the fidelity rule. The whole existing corpus lowers through the region path
(REGION=15/EAGER=0), with new region-corpus differentials (joins, try
joins, counters, typed containers, loop-fresh, multi-shot, recursion via
the Box'd self-capture fallback, nested closures, undef-at-use) and bail
tests asserting the fallback fires.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ions analyze_captures_precise! keeps its entry point and BindingInfo/box_type outputs, but the decision machinery is no longer a sidecar: the analysis IR now emits REAL `closure` region ops at creation sites — each deferred body is the site's capture footprint (one cell_get per captured candidate, plus a synthetic cell_set for candidates some capturing lambda stores to, making criterion (a) a structural fact) — with candidates as `cell_shared` cells created at their K"local" site (a fresh shared box per execution, the structural form of per-iteration rebinding the backedge rule keys on). The shared fixpoint's promote_capture_cells! then decides captures on this IR exactly as it does on real lowering IR, and the per-variable verdict is read off structurally: a candidate whose cell_shared survives with an in-deferred use stays shared; a resolved candidate's reads were rewritten to value captures. CaptureSiteMarker, the marker emission, the sidecar's private criterion-(b) pre-check (_acp_store_observable_after + the unsafe_after machinery), and decl_regions are deleted — that logic lives inside the pass now. The undef-safety check for typed containers reformulates capture reads as "store-dominated at the creation site" (in-deferred reads execute after the site and home stores never un-define), counting home-side stores only. ACP_TRACE stays; the capture-zoo demo's IR section shows the closure-region form before/after the fixpoint, with the verdict table unchanged (same wins, same honest limits, execution differential all-MATCH). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
design.md §5.7 (both copies stay byte-identical with the workspace copy): the scoping note now records the implemented v1 closure-op slice — the derived environment (closure_environment), the op discipline, and the L1 activation rules (result-feeding class, the ACT_DEFERRED-keyed cell-class boundary rule with the ACT_RESUME copy-semantics carve-out, exit/edge boundary rules, mode-aware effect composition, call-site throw binding) — plus the structural capture decision (promote_capture_cells!'s (a)/(b)/(c) with the fresh-cell-in-loop cancellation), lowering materialization, the remaining eager fallbacks, and the await sharing seam. §3.3/§5.8 closure mentions updated from "enters at P3" to the implemented slice. closures.md replaces the marker story with the closure-region representation; concepts.md's residual-cell taxonomy now names the must-share classes instead of "pending closure optimization". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ibility) Lowering is not allowed to type a shared capture container: resolving a declared type expression reads the binding table, and joining store RHS literal types is inference — both outside what lowering may do (the envelope rule, now recorded verbatim in UnifiedIR/docs/closures.md: "isdefined->Bool, sinking closure definitions..., and anything else that's visible structurally"). Remove the BindingInfo.box_type mechanism end-to-end: the typed-container eligibility analysis (declared-type resolution, literal-store joins, the undef-safety checker) in capture_analysis.jl, the container-type threading in closure conversion (field flags back to Bool, :contents everywhere), eval_closure_type's Type branch, and the RefValue paths in the region-path materializer. Shares are untyped Core.Box, exactly as flisp emits today; the precise capture DECISIONS (structural mem2reg on the closure-region IR) are untouched — they are squarely inside the envelope. Typed-cell materialization for unavoidable shares belongs to the compiler's late-materialization pipeline after inference. Test migration keeps the semantic content: the must-stay-shared sentinels still assert sharing plus executable semantics (mutation visibility, multi-shot loops, use-time UndefVarError, the declared-type convert funnel); the RefValue field-layout assertions and the typed-counter return_types check are deleted from the lowering suite (they belong to the compiler pipeline). The capture-zoo demo's shared rows honestly show Core.Box again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The envelope rule allows exactly one code motion: "sinking closure definitions (as long as they haven't been used yet)". Creation is pure, so moving a whole pure creation statement down its block to just before the first statement that mentions any binding it assigns is unobservable — and a store that sat between creation and first use now lands BEFORE the sunk creation, where capture criterion (b) holds: `x = 1; f = () -> x; x = 2; f()` becomes a VALUE capture of the post-store 2 (strictly better than the shared box: one snapshot, no allocation, precise inference downstream). `sink_closure_definitions!` runs on the scoped tree inside `resolve_scopes`, before `analyze_captures_precise!` and before EITHER lowering path reads the tree — the capture decision and the emitted creation position cannot disagree, because every consumer (the capture-analysis IR, `convert_closures`, the UnifiedBackend region emitter) consumes the same already-sunk statement order; the position rule has exactly one implementation. v1 is deliberately conservative: whole pure creation statements only (`_sink_match` whitelist over the closure-conversion machinery), same block only, method bodies only (never toplevel thunks, which pin closure-type definition and world-age effects); confinement requires every outside occurrence of the created bindings to sit in a later statement of the same block; skipped statements may not mention them anywhere in their subtree (a capture is a use, `@isdefined` is a use, an alias is a use), may not carry `@label`/`@goto`, and a skipped `function_decl` counts as mentioning every variable its closure CAPTURES — instance materialization happens at the decl and reads each captured binding there, a mention outside the decl's subtree when the closure's methods sit in separate `method_defs` statements (kwargs closures split this way: sinking the kw-body's bare decl past the sorter's decl would make the sorter capture an undefined variable — regression-tested both here and by the pre-existing kwargs boxing test the gap broke). Skipping may-throw/early-exit statements is sound: on such a path control leaves the block and, by confinement, nothing that can observe the binding is reachable; loops re-enter from the top and re-run the sunk creation. Tests assert both directions with executable differentials: the sinking wins (zoo3-class, store inside a skipped `if`, early-exit skip, assigned argument) and the must-stay-shared blockers (use-before-store, `@isdefined` between, mutually-capturing pair), plus a 200-program seeded source-level fuzz battery run differentially against stock lowering with an asserted shape histogram (store-before-first-use ≥ 50 cases) — a decision/emission position mismatch would surface as a differential, never a green run. The capture-zoo demo gains the zoo3b counter-case and shows the sunk closure op in the before/after IR section. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AbNqJDB4j8dy6GqMRUX6DT
The authorized Part 3 representation: a mutably-captured variable owned solely by ONE closure merges its shared container into that closure as an untyped MUTABLE field (the closure type becomes a mutable struct; value captures stay const type-parameterized fields), eliminating the Core.Box — one allocation and one indirection instead of two. Fields are never type-annotated: a later assignment in the closure body could read an updated world table, so the type is unknowable to lowering. analyze_merged_captures! (end of resolve_scopes, after sinking and the precise capture analysis) proves applicability purely structurally, per variable: exactly one capturing closure (tree-checked, so opaque closures and global methods refuse too); not itself a closure binding (recursion keeps Box: the container must exist before the instance); the closure binding a plain local assigned only by its unique function_decl; every home occurrence classified pre-creation (earlier sibling at the ancestor- path divergence, uses the local slot that initializes the field at `new`) or dominated-post-creation (later sibling of a block with a completion- transparent chain down to the decl — compiled to field ops through the closure binding); the nearest enclosing loop of the variable declaration equal to the creation site (one activation, at most one instance — the multi-shot loop shapes keep Box); no labels/gotos in the frame. Maybe-undef variables merge as uninitialized trailing fields: `new` initializes a prefix (ninitialized covers it), creation conditionally setfield!s from the local, guarded reads keep the use-time UndefVarError with the right variable name, and `@isdefined` maps to field isdefined. Declared types keep the store-side convert funnel through setfield!; reads stay unasserted (the deliberate Part 1 parity choice — stock asserts reads of declared boxed locals; runtime semantics identical, typing is the compiler pipeline's job). Both lowering paths apply the verdicts: eval_closure_type takes field-kind codes (value/box/mutable/mutable-undef; const attrs on non-merged fields of mutable structs), convert_closures emits the field operations, and the region path's materializer turns a merged residual cell_shared into the mutable field — in-body cell ops become field ops on #self#, the `new` takes the cell's value at creation, later home cell ops become field ops on the instance, and the demoted init-phase cell is dissolved by a final promotion fixpoint. The region path conservatively keeps Box for maybe-undef merges (conditional init needs mid-surgery if regions). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AbNqJDB4j8dy6GqMRUX6DT
The must-stay-shared sentinels keep asserting sharing semantics; the representation probe becomes two-fold (acp_capture_repr: Core.Box refs + merged mutable-field kinds read off eval_closure_type calls), splitting the shared class into merged (usestore, isdefblocks, mutual, counter, maybe-undef) and Box-keeping (multi-shot loops, backedge stores, recursion) shapes. New Part 3 assertions cover the applicability edges (per-iteration loop merge, cross-closure Box fallback, if-arm creation, argument write captures, label refusal) and named stock differentials run every affected semantic against flisp lowering (mutation visibility both directions, multi-shot loops, UndefVarError naming, @isdefined through the share, convert-funnel InexactError, mutual identity round-trip). A second seeded source fuzz battery (200 programs, fixed seed, histogram asserted) generates the mutable-field shapes heavily: write captures, maybe-undef writes, cross-closure sharing, per-iteration loop mutation, post-creation home traffic, declared types. Golden IR fixtures regenerated for the field-kind codes and merged emission; the region-path corpus gains the :merged expectation (box-free, setfield! on the instance). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AbNqJDB4j8dy6GqMRUX6DT
closures.md records the Part 3 authorization verbatim, the merged mutable-field representation and its structural applicability rules, what still keeps Core.Box and why (cross-closure shares, multi-instance loops, recursion, region-path maybe-undef), the untyped-fields world argument, and the read-typeassert parity choice. design.md §5.7 materialization note updated to the two share representations. The zoo table shows the merged rows honestly (x::Any (mutable field) vs x::Core.Box), keeps every differential MATCHing, and adds bench_mutcap: escaping shared-capture closures in a hot loop, demonstrating the allocation win (Box + closure vs one object) and the removed indirection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AbNqJDB4j8dy6GqMRUX6DT
The experimental §5.7 follow-up: `typed_region_ir!` consumes the
UnifiedBackend's PRE-materialization region IR (closure ops +
cell_shared cells, post capture fixpoint) and runs the inference port
extended in place — no second engine, no λ-lattice element:
- `infer_closure!` types deferred bodies "as if" inside the same frame
fixpoint (value captures SSA, cell captures the content join, params
joined over visible call sites; refinement barrier + rettype/effects
isolation per §3.3 mode-aware composition);
- shared-cell contents are a structural fixpoint over every cell_set
site, home and deferred alike — the same monotone `celltypes`
accumulator and widening escalator frame cells already use;
- calls whose callee def is a visible closure op take the body's
return-type join + effects mask; arity-mismatched calls of
unshifted closures are (Union{}, THROWS);
- NO typed containers, deliberately: a nominal closure's field cannot
be typed — a later-world assignment can produce a type unknowable
at the type's (world-persistent) definition time; that optimization
is reserved for await's late-generated closures. The discipline
that keeps the in-world answer sound: an ESCAPING closure (any use
besides call-callee position) degrades its params and poisons every
cell it captures (reads Any even with monomorphic visible stores —
any holder can set the untyped mutable capture field); a
`latestworld` barrier that may execute after creation makes the
closure unrefinable (`world_hazard`, reusing the capture
criterion-(b) position machinery). Poisoned cells still compute
joins (meta channel) for diagnostics and the future await consumer.
- shared cells are excluded from Conditional/typeassert refinement
subjects (a visible call between test and use may store), matching
the existing store-forwarding-overlay exclusion.
zoo5b's undeclared counter (`x = 0; inc = () -> (x = x + 1)`) infers
Int64 end-to-end (today: Any). Tests: precision + the load-bearing
escape negative + world-barrier trio + arity/isva/unstable-store/
maybe-undef/nested/multi-shot cases, a 300-case seeded
runtime-soundness fuzz (interpreted values inhabit inferred types),
and a source-level corpus differential (region IR interpreted
natively as UClosures vs stock lowering).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbNqJDB4j8dy6GqMRUX6DT
zoo5b / declared-zoo5 / escape variant through Compiler.Unified.typed_region_ir!: inferred rettypes, cell joins, and the execution differential against stock — containers unchanged by design (see closures.md). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AbNqJDB4j8dy6GqMRUX6DT
closures.md gains the late-pipeline section (the world argument for why generic closure fields stay untyped, verbatim; the escape/world refinement discipline; honest limits). design.md §5.7 note extended in both copies (byte-identical). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AbNqJDB4j8dy6GqMRUX6DT
The issue's own example (also the manual's "performance of captured variable" section): an argument conditionally reassigned before the closure exists. Stock's assigned-once test boxes it; the mem2reg criterion proves every store precedes the creation and captures the if-join by value — Core.Box/Any becomes r::Int64, no `let r = r` workaround. Hot-loop bench: ~2000x (the inferred body vectorizes). Also make the bench harness consume results through a global sink: a discarded effect-free loop is dead code once it infers — and ours infers, so the old harness timed DCE, not the loop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AbNqJDB4j8dy6GqMRUX6DT
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