Skip to content

PERF: eliminate O(N²) HashMap cloning in complexity analysis - #316

Merged
briansrls merged 6 commits into
mainfrom
quick-ant-623
Apr 5, 2026
Merged

briansrls merged 6 commits into
mainfrom
quick-ant-623

Conversation

@briansrls

Copy link
Copy Markdown
Contributor

Summary

  • Complexity analyzer's cache_summary/lookup_summary redirected to a thread_local! RefCell<HashMap> cache, eliminating O(N²) HashMap cloning from Rc::make_mut() on every insert
  • The generated code still threads Rc<CostInternTable> through every call (no structural changes), but it's now a no-op passthrough
  • Bootstrap bridge patch — corresponding complexity.dag change deferred to CX track

Root cause: Every cache_summary() call created a new Rc<CostInternTable> wrapping an rc_map_insert() of the cloned inner HashMap. Since the table Rc was always shared (refcount >1 due to threading through fold chains and cost_of_expr recursion), Rc::make_mut() triggered a full HashMap clone on every insert.

Fix: 3 cache access points (cache_summary, lookup_summary, final extraction in build_complexity_report) now use a thread-local RefCell<HashMap> instead. Zero type changes, zero derive changes.

Test plan

  • cargo test -p v2-compiler-tests — 275 pass, 0 fail
  • All 15 non-ignored complexity tests pass
  • cargo clippy --all-targets -- -D warnings — clean
  • Run complexity_report_scales_to_large_programs (was disabled for OOM — this fix should unblock it)
  • Empirical timing comparison on self-compile

🤖 Generated with Claude Code

briansrls and others added 2 commits April 4, 2026 21:38
The complexity analyzer threads Rc<CostInternTable> through every
cost_of_expr call. Each cache_summary() cloned the entire HashMap
via Rc::make_mut() (refcount always >1 due to table sharing),
creating O(N²) allocations for N functions.

Replace with a thread_local RefCell<HashMap> cache. cache_summary
and lookup_summary now redirect to the thread-local, making the
threaded Rc<CostInternTable> a no-op passthrough. The generated
code structure is unchanged — only the 3 cache access points are
modified.

This is a bootstrap bridge patch (stage0 only). The corresponding
complexity.dag change is deferred to CX track.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2e2805cbae

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +251 to +255
pub fn cache_summary(table: Rc<CostInternTable>, func_name: String, summary: Rc<ComplexitySummary>) -> Rc<CostInternTable> {
Rc::new(CostInternTable {
summaries: v2_rt::rc_map_insert(table.summaries.clone(), func_name, summary),
})
SUMMARY_CACHE.with(|cache| {
cache.borrow_mut().insert(func_name, summary);
});
table // PERF: return same table — real cache is thread-local

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Mirror cache rewrite in complexity.dag generator source

This edits a generated stage0 file directly, but the source generator (src/v2/complexity.dag) still defines cache_summary/lookup_summary with map_insert/map_get. CI runs scripts/check-stage0-freshness.sh via the Stage0 Freshness workflow step, and that gate is designed to fail when committed stage0 differs from regenerated output, so this change will be overwritten on regeneration and will block CI until the .dag source (or regeneration pipeline) is updated too.

Useful? React with 👍 / 👎.

})
});
let summaries_map = result.table.clone().summaries.clone();
let summaries_map = SUMMARY_CACHE.with(|cache| Rc::new(cache.borrow().clone()));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep report intern_table consistent with function_summaries

After this change, summaries are exported from SUMMARY_CACHE instead of result.table.summaries, but ComplexityReport still carries intern_table: result.table. Because cache_summary now returns the input table unchanged (initialized from empty_intern_table()), callers that read report.intern_table.summaries will see an empty map even when report.function_summaries is populated, creating a self-inconsistent report payload.

Useful? React with 👍 / 👎.

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review (INVARIANTS: 2, MODELING: 0+/0-, ROADMAP: 0✓/1!)

INVARIANTS — Violations (2)

ROADMAP — Incomplete

  • Bootstrap D: The PR hand-edits generated stage0 Rust, so it does not satisfy the roadmap gate requiring regeneration-based freshness with zero manual stage0 patches.

This is a real performance hotspot, but the fix lands as a stage0-only hidden-state workaround rather than an upstream source-level authority change.

// The generated code threads Rc<CostInternTable> through every call,
// but cache_summary/lookup_summary redirect here instead of cloning
// the inner HashMap on every insert via Rc::make_mut().
thread_local! {

This comment was marked as resolved.

SUMMARY_CACHE.with(|cache| {
cache.borrow_mut().insert(func_name, summary);
});
table // PERF: return same table — real cache is thread-local

This comment was marked as resolved.

@briansrls

briansrls commented Apr 5, 2026 •

Copy link
Copy Markdown
Contributor Author

ChatGPT Review (extended thinking)

Request changes.

I only see one changed file in this PR: src/v2/stage0/src/v2_compiler_complexity.rs. There are no .dag changes, and ROADMAP.md is not modified. So the .dag modeling and roadmap-update checks are mostly N/A on the surface, but that is also the main architectural issue here: this patch changes the behavior of generated stage0 code without changing the source authority it is supposed to be derived from.

Blocking: this breaks the CostInternTable contract rather than optimizing it.

The source model for complexity analysis is explicit: CostInternTable is the cache, it is threaded through analysis calls, each call may return an updated table, build_complexity_report reads result.table.summaries, and ComplexityReport stores intern_table: result.table.

complexity

complexity

complexity

This PR changes only the stage0 Rust to do something materially different:

introduce thread_local! SUMMARY_CACHE

make cache_summary(...) mutate TLS and return the same table

make lookup_summary(_table, ...) ignore the table parameter entirely

rebuild summaries_map from TLS at the end of build_complexity_report

That means CostInternTable is no longer the authority; it becomes a dummy token while the real state lives off to the side. That violates the repo’s “boundary sufficiency” / “explicit boundary contracts” rules: downstream state is no longer represented in the boundary type, and the same logical fact now exists in two places.

INVARIANTS

INVARIANTS

There is also a likely concrete inconsistency here: the source ComplexityReport still includes intern_table: result.table, but this patch only changes how function_summaries is populated. So function_summaries and intern_table can now disagree by construction. Even if nothing reads intern_table today, that is exactly the kind of silent divergence these invariants are trying to prevent.

complexity

complexity

Blocking: stage0/source authority drift.

The invariants and roadmap are very explicit that stage0 is generated from .dag source, is not supposed to be hand-edited, and the intended bootstrap loop is regenerate → diff → empty. The current bootstrap goal is to make stage0 regeneration authoritative again, not to add more stage0-only logic.

INVARIANTS

ROADMAP

But this PR changes only src/v2/stage0/src/v2_compiler_complexity.rs. It does not change src/v2/complexity.dag, which still defines the old persistent-table semantics. So after this PR:

.dag says the cache is CostInternTable

generated stage0 says the cache is thread-local mutable state

That is a direct dual-authority problem. The next regeneration either drops this fix or forces another manual patch. I would not merge a semantic change like this unless it lands in the .dag source and stage0 is regenerated from that single source of truth.

INVARIANTS

ROADMAP

Major: hidden mutable ambient state introduces reentrancy and lifecycle hazards.

clear_summary_cache() at the start of build_complexity_report() makes the function non-reentrant on a thread. A nested or re-entrant call on the same thread will wipe the outer call’s cache. Even without recursion, TLS is now part of the function’s observable behavior, so this core analysis code is no longer a pure “inputs → outputs” computation.

That runs against the repo’s engineering guidance to prefer returned values over shared mutation and keep core logic pure.

Pasted markdown

There is also a smaller lifetime concern: the cache is cleared on entry, not on exit, so the whole map stays resident in TLS until the next run or thread teardown. That is not necessarily fatal, but it is another consequence of moving state out of the returned value and into ambient storage.

The performance diagnosis is right; the fix location is wrong.

I agree with the PR’s root problem: repeated whole-HashMap cloning in a hot path is exactly the kind of accidental quadratic behavior the repo treats as a correctness bug. The invariants also say the fix should be structural, not a downstream compensation hack.

INVARIANTS

There is even already tracked context pointing at this exact area: the complexity guard for large modules was documented as a temporary workaround, and the stated fix was to move the intern table to arena allocation or RefCell instead of deep Rc cloning.

195

So the direction is sensible, but the RefCell needs to live in the authoritative state, not in thread-local side state. Better options:

make CostInternTable own interior-mutability directly, so the table parameter remains the authority

or make build_complexity_report use explicit build-local mutable state that is still passed structurally

and in either case, land it in src/v2/complexity.dag and regenerate stage0

.dag compositional modeling quality

No .dag files changed, so there is no positive compositional-modeling work to review in this PR.

But from a modeling standpoint, this change is negative: it replaces an explicit modeled boundary (CostInternTable) with hidden runtime state. In this codebase, that is the opposite of compositional modeling.

Roadmap alignment

ROADMAP.md is not modified, so there is no roadmap diff to validate.

Directionally, though, this is misaligned with the current bootstrap work: the roadmap snippets I found emphasize that manual stage0 edits are the productivity failure to eliminate, and that the milestone is making regeneration authoritative again. This patch moves further away from that.

ROADMAP

What I would ask for before merge

Move the fix into src/v2/complexity.dag and regenerate stage0.

Preserve CostInternTable as the single cache authority.

Add a regression test that proves report.function_summaries and report.intern_table stay consistent.

Add a test that repeated build_complexity_report() calls on the same thread do not leak or interfere.

If a temporary stage0-only bootstrap patch is unavoidable, it should still preserve the table contract and come with an explicit removal ratchet. This one does not.

So: real perf bug, not mergeable as implemented. The patch removes the O(N²) symptom by making the cache invisible to the type/API boundary, and that collides with the repo’s invariants hard.


View conversation

…y_unwrap

Model SharingStrategy.fold_accumulator_shared in languages.dag, then emit
Rc::try_unwrap(acc) at fold iteration start so field accesses move instead
of clone. A static safety check (collect_acc_field_moves) rejects the
optimization when any accumulator field is moved more than once, preventing
use-after-move errors. Stage0 regenerated via two-pass bootstrap.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review (INVARIANTS: 3, MODELING: 1+/2-, ROADMAP: 0✓/0!)

INVARIANTS — Violations (3)

MODELING — Strengths

  • src/v2/languages.dag Keeping SharingStrategy inside LanguageSpec places target-specific sharing syntax at the correct language layer instead of in shared compiler IR.

MODELING — Improvements

  • src/v2/languages.dag fold_accumulator_shared: Bool is not compositional; the model should express fold/update ownership through the same sharing/rendering algebra as wrap_template, not a target-global boolean switch.
  • src/v2/04_emit_info.dag EmitGraphInfo is the right boundary for consumed facts, but owned_bindings should be keyed by structural binding identity rather than String so shadowing cannot corrupt ownership decisions.

ROADMAP — Unclaimed

  • PERF track: The PR advances performance by removing clone-heavy paths in stage0 complexity and fold emission, but ROADMAP.md records no corresponding PERF progress, metric change, or ratchet update.

The prior stage0 cache issue is fixed, but the new fold optimization still breaks invariants by adding emitter-local name-keyed ownership authority and a forbidden runtime clone fallback.

Comment thread src/v2/04_emit_info.dag
// Bindings known to be owned (not behind Rc). Field accesses on these
// bindings use move semantics instead of .clone(). Set for fold
// accumulators after Rc::try_unwrap when fold_accumulator_shared is false.
owned_bindings: Map<String, Bool>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invariant violation: owned_bindings: Map<String, Bool> makes ownership depend on binding names at the emit boundary, violating Boundary sufficiency and No duplicate representations; the fact must flow as structural binding identity from ownership analysis.

Comment thread src/v2/05_emit_rust.dag
let needs_unwrap = acc_name != "" && emit_map_has(m: emit_info.owned_bindings, key: acc_name)
if needs_unwrap {
let acc_ident = emit_ident(name: acc_name, target: Rust)
concat("|", params_str, "| { let ", acc_ident, " = Rc::try_unwrap(", acc_ident, ").unwrap_or_else(|rc| (*rc).clone()); ", body_str, " }")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invariant violation: Emitting Rc::try_unwrap(...).unwrap_or_else(|rc| (*rc).clone()) is the exact runtime fallback forbidden by No fallbacks that fabricate, because emit is compensating for missing upstream single-consumer proof instead of consuming it structurally.

Comment thread src/v2/05_emit_rust.dag
match lambda_node.expr_data {
ExprLambda { semantics: _ } =>
let body = lambda_body(texpr: lambda_node)
let terminal = fold_terminal_expr(body: body)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invariant violation: terminal.name == acc_type_name uses Node.name as semantic authority for the fold optimization, violating Boundary sufficiency and Emission is translation, not decision-making; this construction fact should arrive structurally from infer/reconcile.

@briansrls

briansrls commented Apr 5, 2026 •

Copy link
Copy Markdown
Contributor Author

ChatGPT Review (extended thinking)

Request changes.

The clone-reduction idea is good, but this PR is doing two very different things at once: a stage0 perf cleanup, and a new Rust-emitter ownership optimization for fold accumulators. The first looks directionally right. The second is where I think the risk is, and I would not merge it as-is.

  1. Invariant violations

Highest severity: this reintroduces the exact fallback pattern that INVARIANTS.md calls out as invalid. The doc explicitly says ownership should not compile to Rc::try_unwrap(x).unwrap_or_else(|rc| (*rc).clone()); either the compiler proves single ownership and emits a move, or it surfaces that the proof is missing. This PR adds that exact pattern in the new fold-emission path and also in several stage0 folds. That conflicts directly with both “No fallbacks that fabricate” and the repo’s “performance is correctness / facts flow forward” rule.

Pasted markdown

INVARIANTS

The emitter is making a new ownership decision by heuristic, instead of consuming an upstream fact. INVARIANTS.md is very explicit that emit should be pure translation, not semantic decision-making, and that new fact tables must be exact, non-lossy, and consumed as authority. Here the new owned_bindings map is synthesized inside emit from local AST shape checks (fold_body_constructs_acc_struct, collect_acc_field_moves, fold_body_safe_field_moves) and then used to change clone-vs-move behavior. That is exactly the “fact was lost, downstream guessed” pattern the invariants warn against.

INVARIANTS

211

This also looks like a duplicate ownership authority. EmitGraphInfo already had movable, sourced from ownership analysis. This PR adds owned_bindings, also about move semantics, but computed ad hoc in the emitter and keyed only by name. That is a second authority for related semantics, which is the kind of parallel representation the invariants keep warning about.

INVARIANTS

INVARIANTS

  1. Compositional modeling quality for the .dag changes

The .dag modeling quality is not good enough on the new fold ownership path.

04_emit_info.dag adds owned_bindings: Map<String, Bool>. That is a lossy representation: it is keyed by binding name, not binding identity, so it cannot preserve shadowing/scope. The invariants explicitly say a boundary fact table is invalid if it collapses distinct bindings or drops witnesses needed downstream.

INVARIANTS

languages.dag adds fold_accumulator_shared: Bool, which is a plausible target-language knob, but it is not the actual proof the emitter needs. The real question is not just “does this target share fold accumulators?”, it is “for this specific fold body and this specific accumulator binding, do we have an exact ownership witness that allows move semantics?” The current change still answers that second question with Rust-specific emitter logic, so the new LanguageSpec field does not make the design properly data-driven. The repo’s current guidance is that emit should consume boundary facts rather than rediscover them, and recent emission-debt notes make the same point: missing semantic facts belong in LanguageSpec/boundaries, not in emitter heuristics.

Pasted markdown

Pasted markdown

More broadly, the repo’s compositional direction is “resolve → infer → emit, no re-derivation.” The recent fact-composition notes explicitly say emit should read composed facts and never re-resolve or re-derive them. This PR goes the opposite direction by adding another emitter-local proof pass.

211

211

  1. Concrete bugs / correctness risks in the code

Real bug: name collision in the init expression. In emit_rust_fold_method_call, once acc_unwrap is true, the PR builds fold_emit_info by inserting the fold accumulator param name into owned_bindings. But that same fold_emit_info is then used not just for the fold lambda, but also for emitting the init expression. Because owned_bindings is only Map<String, Bool>, any outer binding with the same name as the fold accumulator parameter can be misclassified as “owned” while emitting init:. A very ordinary pattern like fn f(acc: Outer, xs) { xs |> fold(init: use(acc.field), f: (acc, x) => ...) } can therefore change field emission on the outer acc, even though it was never unwrapped. This is a real shadowing bug, not just an architectural concern. (Uploaded diff: 05_emit_rust.dag / stage0 mirror around the fold_emit_info construction and init_str emission.)

Real bug class: owned_bindings collapses shadowed bindings. Because the new fact is keyed only by String, any inner let acc = ... or nested lambda parameter named acc will inherit the “owned” status of the fold accumulator, even when it refers to a different binding. That can cause wrong move emission on unrelated values. This is exactly why name-keyed semantic tables are dangerous.

The safety proof is too narrow to trust. collect_acc_field_moves only understands direct acc.field accesses and only exempts a narrow special case: the first argument of functions in rt_ref_map_functions(). It does not appear to reason about whole-accumulator uses, aliases, method-call borrow contexts, future runtime helpers, or other by-ref paths. A false negative here merely misses the optimization; a false positive changes emitted ownership semantics. Given that asymmetry, the heuristic needs to be much stronger before it is safe.

The body-shape proof is underpowered. fold_body_constructs_acc_struct only accepts terminal ExprRecordLit with terminal.name == acc_type_name. That is a brittle, name-based shape test. It will miss valid structured cases (false negatives), and more importantly it is still using a local string/shape heuristic where an upstream ownership fact should exist.

  1. Tests are too weak for the semantic change

The new tests only grep generated Rust for Rc::try_unwrap and for the presence/absence of .clone() on a field. They do not:

compile the emitted Rust,

exercise shadowing/name-collision cases,

check nested lets / nested lambdas,

verify whole-accumulator uses,

verify method-call borrow contexts,

or prove the intended asymptotic behavior.

For a change that alters move-vs-clone semantics, source-string assertions are not enough. The repo’s testing philosophy distinguishes structure-only checks from value/computation checks; this change needs at least one stronger compile-or-run level regression test.

INVARIANTS

  1. Roadmap alignment

ROADMAP.md is not modified in this diff, so there is no checkbox audit to do.

Indirectly, though, this PR is not aligned with the repo’s current direction. Recent notes already identify “finish EmitContext/boundary migration → emit consumes, not rediscovers” as the right direction, and they frame current emitter heuristics as design debt, not a place to keep adding more logic. This PR adds more emitter-side rediscovery instead of moving the proof upstream.

Pasted markdown (2)

Pasted markdown

  1. What I would do instead

I’d split this into two pieces:

Keep the narrow stage0 perf cleanup if desired: the direct Rc::try_unwrap substitutions in the local fold states (SummaryResult, KahnState, etc.) look mechanically reasonable, though they still conflict with the documented no-fallback rule and don’t encode the bound structurally.

Drop or rework the fold-emitter ownership optimization. If fold accumulators are really linearly threaded, the proof should come from ownership/infer as an exact, scope-aware witness keyed by binding identity, and emit should merely consume that witness. Right now it is the emitter proving its own exception by local AST inspection, which is exactly the pattern the invariants say not to use.

INVARIANTS

211

So my bottom line is: good performance goal, but the current implementation violates repo invariants and has at least one concrete shadowing bug.


View conversation

Integrate main's SharingStrategy clone templates (clone_value, deref_clone,
iter_owned) and try_target_primitive_type alongside our fold_accumulator_shared
and owned_bindings optimization. StoredField now uses apply_type_template1 for
the clone path. Stage0 regenerated via two-pass bootstrap.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@briansrls

briansrls commented Apr 5, 2026 •

Copy link
Copy Markdown
Contributor Author

ChatGPT Review

Two blockers:

src/v2/05_emit_rust.dag: the new Rc::try_unwrap rewrite breaks the emitter’s type invariant. After emit_typed_fold_lambda shadows acc with owned T, only emit_typed_field_access consults owned_bindings; every other acc use still emits as if the binding were the original Rc. fold_body_constructs_acc_struct / fold_body_safe_field_moves do not rule out those non-field uses, so bodies that still pass acc to another helper, capture it in a nested lambda, etc. can pass the new checks and then emit ill-typed Rust.

The .dag modeling here is not compositional: owned_bindings is a flat name map on EmitGraphInfo, and fold_emit_info is threaded beyond the exact let acc = Rc::try_unwrap(...) scope (including nested subexpression emission). That means “owned” status is no longer lexical. A reused binding name in a nested fold/lambda can accidentally inherit the outer fold’s owned semantics and get spurious move field accesses / Rc::try_unwrap even though it is a different binding.


View conversation

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review (INVARIANTS: 3, MODELING: 2+/2-, ROADMAP: 0✓/1!)

INVARIANTS — Violations (3)

MODELING — Strengths

  • src/v2/04_emit_info.dag Keeping emission-only ownership context on EmitGraphInfo instead of pushing Rust policy into core IR is the right layer boundary.
  • src/v2/languages.dag Moving fold-sharing policy into LanguageSpec is the right layering move because target-language behavior belongs in language data, not Rust-only branches.

MODELING — Improvements

  • src/v2/04_emit_info.dag This would compose better if ownership facts were keyed by structural binding identity from ownership.dag rather than String names at the emit boundary.
  • src/v2/languages.dag fold_accumulator_shared is too coarse to faithfully model sharing semantics; the behavior should emerge from the existing sharing/type-rendering authority rather than a standalone Bool.

ROADMAP — Incomplete

  • CG-1 Sharing and ownership: The PR adds one LanguageSpec knob and a Rust-only fold fast path, but single-authority codegen is still unmet because ownership/share decisions are re-derived in emit and partly patched only in stage0.

The PR targets a real performance problem, but it still fixes it in the wrong layer by adding a new codegen flag, new emitter-side ownership heuristics, and new generated-stage0-only optimizations instead of surfacing the needed ownership fact from the upstream authority.

Comment thread src/v2/languages.dag
import_path: String
}

type SharingStrategy {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invariant violation: fold_accumulator_shared introduces a global boolean codegen switch, violating Engineering Standards "No flags in codegen" and "Emission is translation, not decision-making" because fold ownership strategy is not derived from structural facts.

Comment thread src/v2/05_emit_rust.dag
node.children |> flat_map(c => collect_acc_field_moves(node: c, acc_name: acc_name))
}
_ =>
node.children |> flat_map(c => collect_acc_field_moves(node: c, acc_name: acc_name))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invariant violation: Branching on rt_ref_map_functions() inside collect_acc_field_moves re-derives borrow/move semantics by helper name in the Rust emitter, violating Root-Cause Depth, Heuristics indicate lost structure, and No parallel implementations; this ownership fact should come from ownership.dag.

@@ -103,13 +103,13 @@ v2_rt::rc_map_insert(acc.clone(), m.name.clone(), exported_set.clone())
let resolve_accum = modules.clone().iter().cloned().fold(Rc::new(ResolveAccum {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invariant violation: This Rc::try_unwrap optimization pattern, repeated in sibling src/v2/stage0/ files in this diff, patches generated Rust without any corresponding src/v2/*.dag authority, violating No parallel implementations and the hand-editable generated code escape hatch.

briansrls and others added 2 commits April 5, 2026 09:51
# Conflicts:
#	src/v2/05_emit_rust.dag
#	src/v2/stage0/src/v2_compiler_emit_rust.rs
#	src/v2/tests/src/pipeline.rs
The boolean was always false across all targets, making cond_sharing a
dead branch. Fold ownership eligibility is fully determined by structural
checks (Rc-wrapped, struct repr, body constructs acc, safe field moves)
— no codegen flag needed.

Addresses review violation: "No flags in codegen" / "Emission is
translation, not decision-making."

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review (INVARIANTS: 2, MODELING: 2+/2-, ROADMAP: 0✓/1!)

INVARIANTS — Violations (2)

MODELING — Strengths

  • src/v2/04_emit_info.dag The new emit-boundary fact is at least consumed in the same PR, so it does not introduce speculative metadata with no downstream reader.
  • src/v2/languages.dag Keeping target-specific sharing syntax inside SharingStrategy stays at the intended LanguageSpec parameterization layer.

MODELING — Improvements

  • src/v2/04_emit_info.dag Model accumulator ownership by structural binding identity from ownership analysis rather than Map<String, Bool> keyed by names, because ownership is a fact about bindings, not strings.
  • src/v2/languages.dag fold_accumulator_shared is not a language fact or algebraic structure; it is a fold-specific optimization policy and should emerge from ownership/sharing authorities instead of a global boolean in LanguageSpec.

ROADMAP — Incomplete

  • CG-1 Sharing and ownership: The PR targets fold clone cost, but it does not advance the roadmap’s single-authority design because sharing and ownership still live in Rust-emitter heuristics and generated-stage0 patches instead of TypeRendering/ValueContext or ownership facts.

The PR addresses a real performance symptom, but the implementation is still downstream compensation in emit/stage0 rather than the upstream ownership and sharing authority required by the invariants and roadmap.

Comment thread src/v2/05_emit_rust.dag
concat("|", params_str, "| ", body_str)
}
_ => emit_typed_expr(texpr: lambda_expr, registry: registry, scope: scope, depth: depth, rc_types: rc_types, emit_info: emit_info, fuel: 1024)
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invariant violation: Adding fold_terminal_expr and collect_acc_field_moves creates a new emit-side ExprData walk to rediscover fold ownership facts, violating Root-Cause Depth, No parallel implementations, and "Emission is translation, not decision-making"; this authority belongs in ownership/reconcile, not the Rust renderer.

Comment thread src/v2/05_emit_rust.dag
ExprLambda { semantics: _ } => match lambda_param_names(texpr: fold_lambda_node) |> first {
Some { value: n } => n
None => ""
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invariant violation: The new acc_unwrap decision is a renderer-local heuristic bundle (cond_sharing, cond_struct, cond_body, cond_safe) rather than an upstream proof, violating Heuristics indicate lost structure and "Emission is translation, not decision-making".

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review (INVARIANTS: 2, MODELING: 1+/1-, ROADMAP: 0✓/1!)

INVARIANTS — Violations (2)

MODELING — Strengths

  • src/v2/04_emit_info.dag Keeping emission inputs in a single EmitGraphInfo boundary record composes with the existing boundary-contract pattern better than scattering new globals or side caches.

MODELING — Improvements

  • src/v2/04_emit_info.dag owned_bindings: Map<String, Bool> is not a faithful or compositional ownership model because it uses binder names as identity proxies and encodes an emitter-local decision rather than a structural fact from ownership analysis.

ROADMAP — Incomplete

  • PERF: eliminate O(N²) HashMap cloning in complexity analysis: The diff improves generated/stage0 clone behavior and Rust emit heuristics, but it does not add the upstream src/v2/*.dag ownership or complexity authority needed to show the O(N²) class is eliminated at the source level.

The PR targets a real performance problem, but the remaining new work still fixes it in emit/generated artifacts instead of the upstream authority and adds non-behavioral tests that lock in the workaround.

Comment thread src/v2/05_emit_rust.dag
Some { value: summary } => match summary.repr { StructRepr => true _ => false }
None => false
}
let cond_body = fold_body_constructs_acc_struct(lambda_node: fold_lambda_node, acc_type_name: acc_type_name)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invariant violation: emit_rust_fold_method_call fabricates owned_bindings inside the emitter instead of consuming an upstream ownership fact, violating Explicit boundary contracts, the "new semantic boundaries must land end-to-end" rule, and Emission is translation, not decision-making.

assert!(
content.contains("Rc::try_unwrap(acc)"),
"fold should emit Rc::try_unwrap for struct accumulator, got:\n{}",
content

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invariant violation: This test asserts emitted Rust substrings like Rc::try_unwrap(acc) instead of observable compiler behavior, violating Testing Invariants "Behavioral only" and creating a tautological codegen lock-in.

@briansrls
briansrls merged commit f16ecb8 into main Apr 5, 2026
1 check passed
This was referenced Apr 6, 2026
briansrls added a commit that referenced this pull request Apr 7, 2026
… blocker detail

- Tests: 294→316 pass, 41→44 ignored
- Hand-maintained files: 9→2 (7 eliminated in PR #316)
- Expand depth/indent asymmetry blocker with resolution options
- Record PR #338 review feedback (MethodTemplateSpec, method name enum)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant