Skip to content

Build one CallApplicationPlan per direct call: the conformance seam canonicalized every formal twice - #8431

Merged
briansrls merged 2 commits into
mainfrom
spring/call-application-plan-2
Aug 18, 2026
Merged

briansrls merged 2 commits into
mainfrom
spring/call-application-plan-2

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Root-causes and removes the largest single term in compile.reconcile.

The chain, measured end to end

compile.reconcile          ~9 min
  typecheck_module         89% of it   (collect_parent_envs = 11ms total, dead)
    infer_items            68% of it
      gunbc.host_effect_realize   246-260s = 63% of ALL corpus inference
                                  (p50 module 5ms)
        Call 210.9s + RecordLit 49.6s  ~= 100% of that module
          compat_diags 129.0s + structured_diags 64.5s = 193.5s
            peel_nominal_alias_identity = the whole of it

What the cost actually is

Not comparing types — canonicalizing them. The timer around
direct_call_arg_type_mismatch never entered the top fourteen operations, and
substitute_generics already short-circuits an empty substitution.

compat_diags     129007ms n=330
structured_diags  64544ms n=330
  peel_formal     64538ms n=591   (inside compat_diags)
  st_peel_formal  64537ms n=591   (inside structured_diags)
  peel_actual     64505ms n=562

Two independently placed timers on two separately compiled functions report the
same 591-call population at the same cost to within 1ms, and st_peel_formal is
64537 of structured_diags' 64544 — that function is its peel.

Why, and the fix

direct_call_arg_mismatch_diags and
direct_call_structured_application_mismatch_diags are line-for-line duplicates
of one per-formal prologue, and the single direct-call seam hands both the same
value_params, typed_args and call_subst. Every formal was canonicalized
twice per call. peel_nominal_alias_identity is resolve_node_bounded, an
unmemoized recursive rebuild allocating a fresh Node per level.

build_call_application_plan constructs CallFormalApplication once and both
consumers read it. This removes the work rather than caching it: no map, so
no order-exposing operation enters the v2.std.determinism denominator; no key,
so no soundness surface. The emitted seed carries exactly one peel per consumer,
down from two.

Second commit, kept separate so its effect is attributable alone: peel's alias
branch called resolve_node_bounded on the same target twice, under an
identical guard and match arm. The seed drops 4 calls to 3 in that function.

Bar

Structural identity proven from source — the duplication is decidable by
awk — with the instrument used only to price it. Emitted seed regenerated
(regen_stage0, 0 diagnostics, 145 files); .dag authority and seed agree.

Not claimed

  • No floor before/after receipt yet. The prediction is ~64.5s of a 193.5s
    seam; it is a prediction until CI measures it.
  • Does not touch the actual-side peels. compat peels every actual;
    structured peels one only on its refusal path — different populations, not a
    third duplicate.
  • No memo on resolve_node_bounded, deliberately. Its result depends on five
    declared inputs and its depth > 100 arm returns a fabricated InternalError,
    so a node-identity key can serve an error that is not real. peel is the sound
    boundary — depth/masked are literal constants at all three internal call
    sites, and module_name reaches only make_error_node, whose diagnostics peel
    discards — but that leaves env, unsound by Rc identity and possibly
    self-defeating by content. Separate question, separate change.

Semantics are unchanged: every recomputed term was a pure function of arguments
both callers already shared.

Brian Searls and others added 2 commits August 18, 2026 06:57
peel_nominal_alias_identity's alias branch computes

  structural = resolve_node_bounded(target, env, module_name, depth: 0, masked: false).resolved

and then, in an else-if guarded by the IDENTICAL condition and the IDENTICAL
match arm, calls resolve_node_bounded on the same `target` a second time and
binds it as `target_resolved`. Same pure function, same arguments, same result
— the second call is discarded work, and resolve_node_bounded is an unmemoized
recursive rebuild that allocates a fresh Node at every level.

The arm now reuses `structural`. Behavior-identical by construction: both
bindings are the same call. Emitted seed drops from 4 resolve_node_bounded
calls in that function to 3.

Landed separately from the CallApplicationPlan commit so its effect is
attributable on its own (review request, tidy-pike-117).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PjAhodiDNy96tME6deseFx
direct_call_arg_mismatch_diags and direct_call_structured_application_mismatch_diags
were line-for-line duplicates of one per-formal prologue — param_node_type_expr,
substitute_generics, peel_nominal_alias_identity, authored_name_at, the
named-actual filter-then-first, and the positional skip fallback — and the one
direct-call seam in ExprCall hands BOTH the same value_params, typed_args and
call_subst. So every formal was canonicalized twice per call.

That is the whole cost of the seam. peel_nominal_alias_identity is
resolve_node_bounded, an unmemoized recursive rebuild allocating a fresh Node
per level. Measured on the required floor, gunbc.host_effect_realize (63% of
all corpus type inference, p50 module 5ms):

  compat_diags     129007ms n=330
  structured_diags  64544ms n=330
    peel_formal     64538ms n=591   (inside compat_diags)
    st_peel_formal  64537ms n=591   (inside structured_diags)
    peel_actual     64505ms n=562

Two independently placed timers on two separately compiled functions report the
same 591-call population at the same cost to within 1ms, and st_peel_formal is
64537 of structured_diags' 64544 — that function IS its peel. Meanwhile the
timer around direct_call_arg_type_mismatch never entered the top fourteen
operations, and substitute_generics already short-circuits an empty
substitution: the type COMPARISON is free, the canonicalization is not.

build_call_application_plan constructs CallFormalApplication once (index,
param_name, formal_subst, formal, matched_arg) and both consumers read it.
This removes the work rather than caching it: no map is introduced, so nothing
here becomes an order-exposing operation in the v2.std.determinism denominator,
and there is no key and so no soundness surface. Emitted seed carries exactly
one peel per consumer, down from two.

NOT claimed: this does not touch the ACTUAL-side peels. compat peels every
actual; structured peels one only on its refusal path. Different call
populations, not a third duplicate.

Deliberately NOT done here: a memo on resolve_node_bounded. Its result depends
on five declared inputs and its depth > 100 arm returns a fabricated
InternalError, so a node-identity key can serve an error that is not real
(caught in review by tidy-pike-117). peel is the sound boundary — depth and
masked are literal constants at all three internal call sites, and module_name
reaches only make_error_node whose diagnostics peel discards — but that leaves
env, which is unsound by Rc identity and possibly self-defeating by content.
Separate question, separate change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PjAhodiDNy96tME6deseFx
@gunbai-bot

gunbai-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

CI red is pre-existing on main, not introduced here

The base commit this branch forks from — 3abc17b54c — is already failing the same workflow. Run 32097006674 (main @ 3abc17b54c) against run 32109241376 (this PR @ 1caa2f2ce0), identical planned=9418 executed=9418 terminal=9418 known_red_held=794:

total failed budget overrun semantic
main @ 3abc17b54c 40 14 26
this PR @ 1caa2f2ce0 34 17 17

Semantic failures unique to this branch: none. The set-difference over non-budget failures is empty — this PR's semantic failures are a strict subset of its own base's, and it passes 6 more witnesses overall.

The 8 failures present here but not on that main run are all budget overruns within 4% of the line:

505ms wall > 500ms   roadmap_altitude_witness.altitude_routine_section_folds
501ms wall > 500ms   roadmap_authority.witness_declared_graph_has_justified_size
518ms wall > 500ms   roadmap_dispatch_actuator.witness_selected_provider_shapes_codex_tmux_command
502ms wall vs 500ms  roadmap_receipt_continuity_acceptance...contract_holds
504ms wall vs 500ms  where_refinement_cast_literal_positive_int_accepts
511ms cpu  vs 500ms  where_refinement_data_init_non_literal_compile_clean
505ms wall vs 500ms  where_refinement_direct_return_var_compile_clean
504ms wall > 500ms   emit_ingest_python_same_language_round_trip...parse_bridge_holds

Fourteen witnesses that fail on main pass here, including the whole
emit_host_loop_equals_eval / emit_host_match_equals_eval /
emit_host_transport_wire group.

Not claimed

I have not established that this PR causes the 14 recoveries or the 3 extra budget
overruns. Both are consistent with jitter at a 500ms boundary on a shared runner, and a
count moving in my favour is not evidence any more than one moving against me would be. The
load-bearing claim is only the empty set-difference on semantic failures.

I am not pushing a speculative fix: nothing here is attributable to this diff, and editing
unrelated witnesses to chase a red that predates the branch would be changing the subject.
Merge is genuinely blocked, but by main's state, not by this change.

— sent from fierce-raven-463

@gunbai-bot

gunbai-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Why this lands before the memo, even though the memo is the larger number

A follow-on measurement found that ~98.65% of peel_nominal_alias_identity cost on
gunbc.host_effect_realize is repeat calls on an already-computed (node, env) key, with
distinct_env = 1 for the module's inference. That makes a module-scoped memo look like the
bigger repair. It is deliberately not what this PR does, for three reasons.

1. It removes the redundancy rather than caching it (DESIGN §2). A memo landing first
would leave an end state where compat_diags and structured_diags both still peel every
formal and a cache makes the second one cheap — duplicated computation retained and made
fast. §2's argument is that redundancy loses on cost, safety and complexity simultaneously;
a cache addresses only the first.

2. That 98.65% is partly a measurement of the defect this PR deletes. It was taken on the
double-peel tree, so a share of those repeat keys are repeats only because the same formal
is peeled twice per call. Sizing a cache on that number would justify it with a figure this
change invalidates. The memo's real value can only be measured against the post-plan tree.

3. This saving is unconditional; a memo's is contingent. No key, no memory, no eviction,
no scope, no soundness surface — nothing that can later stop working. A memo's saving depends
on continuing to hit, and if its scope or eviction policy changes, the duplicated work returns
silently with no diagnostic. A smaller permanent win before a larger contingent one, whatever
the ratio.

A memo on resolve_node_bounded is separately unsound — five declared inputs, and a
depth > 100 arm returning a fabricated InternalError, so a node-identity key can serve an
error that is not real. peel is the sound boundary (depth/masked literal constants at all
four internal call sites; module_name reaches only make_error_node, whose diagnostics peel
discards). That is a separate change with its own soundness argument, and it may not be worth
building at all once this lands.

Credit: the soundness objection, the cost-split requirement and this ordering argument are
tidy-pike-117's.

— sent from fierce-raven-463

@gunbai-bot

gunbai-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Before/after receipt — same host, sequential, identical probe

Both arms run on one host (srv2), sequentially so they never contend for the machine
they are compared on, with the same probe wired into both trees by the same script and an
exact-equality module filter.

peel calls peel total compile.reconcile
base 3abc17b54c 2,900 253.9 s 8 min
this branch (regen'd) 2,200 170.9 s 6 min
−700 (−24.1%) −83.0 s (−32.7%) 8 → 6 min

The call count is the falsifiable half, and it held. Stated in advance: if calls did not
drop, the change does not do what its source says. The timers measured 591 formal peels per
consumer, so deleting one consumer's copy predicts ~591 fewer calls; observed 700 at 100-call
sampling granularity.

Not claimed

  • I predicted ~64.5 s and measured 83.0 s. I am not banking the extra 18.5 s. The 64.5 s
    came from a different base (the floor-cut tree, not main), and per-call peel cost is
    host- and load-sensitive. A result above prediction earns the same scepticism as one
    below; I have no mechanism for the excess.
  • The reconcile line is the weakest number here and should not lead. Whole-minute
    granularity puts the true delta anywhere in ~1.1–2.9 min, at N=1 per arm. It is consistent
    with the peel saving, not independent evidence of it.
  • N=1 per arm; peel counts are ±100; this measures one module, not corpus-wide reconcile.
  • The probe wraps peel identically in both trees, so instrument overhead is present on both
    sides and favours neither.

No-regression

Separately established on run
32109241376: identical
planned/executed/terminal = 9418, and the set-difference over non-budget FAIL identities
against base run 32097006674 is
empty. The floor red is pre-existing on main at this branch's own base — 16
non-exhaustive (unverified half of #8400), 9 cargo (per-host reachable, absent on that
runner), remainder budget-marginal at a 500 ms line.

Measurement design (same-host sequential pair, report both absolutes not the delta) is
tidy-pike-117's.

— sent from fierce-raven-463

@gunbai-bot

gunbai-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Receipt provenance — pinned, so the pair survives the regen_stage0 cut

gunbc#8406 deletes regen_stage0.rs (−1485). Both halves of the pair above were produced
before that lands, in one session, by the same tool — but the reproduction path is pinned
here so the receipt stays checkable against a pinned ref after the binary is gone.

Both arms, same base commit, byte-identical instrument:

arm      worktree base   plan present   probe sha256(added lines)
base     3abc17b54c9f    0              01ef82bbf6dde3d0
branch   3abc17b54c9f    2              01ef82bbf6dde3d0

Measured tree vs reviewed commit 1caa2f2ce006c379d7d29c806863e58c06682ffd, per file:

src/v1/04_infer.dag                        23be8db6f7622ed1  MATCH
src/v1/04_resolve.dag                      ead485b21667d9ad  MATCH
src/v1/stage0/src/v1_compiler_infer.rs     cbceb06867022955  MATCH
src/v1/stage0/src/v1_compiler_infer_resolve.rs               DIFFERS — instrument only

The one difference is the measurement probe: +16 lines, insertions only, a timing wrapper
around peel_nominal_alias_identity renaming the original to ..._inner. No semantic change,
and it is present identically in both arms (same sha above), so its overhead is on both
sides and favours neither. The three files carrying the actual change are byte-identical to
the reviewed commit.

Regen provenance: the branch tree's emitted seed was produced by regen_stage0 built from
3abc17b54c + this change — 0 diagnostics, 145 generated files. After #8406 lands, regen is
claim_executor --required-regen; reproducing this pair requires the tool at the pinned SHA,
from history.

The trap this avoids, stated so nobody walks into it later: do not re-run one half of this
pair against a later tree. If the before is reused and the after re-run after #8406, the
pair looks intact and is not — the two halves would have been produced by different tools.
Same-host was necessary; same-tool is equally so.

Hazard flagged by tidy-pike-117.

— sent from fierce-raven-463

@gunbai-bot

gunbai-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Correction: the 18.5 s overshoot IS explained — I compared totals across different call counts

An earlier comment said "I predicted ~64.5 s and measured 83.0 s… I have no mechanism for the
excess."
That was too pessimistic. There is a mechanism, and I hid it from myself by comparing
totals when the two samples had different call counts. Normalized to the invariant:

predicted per removed call : 109.1 ms   (64.5 s / 591)
observed  per removed call : 118.6 ms   (83.0 s / 700)
disagreement               : 8.6%

8.6% apart — smaller than the host sensitivity already measured in this lane. Prediction and
measurement agree once compared on the quantity that is supposed to be stable. The 18.5 s
decomposes exactly:

more calls removed than predicted : 1.18x   (591 -> 700, inside the +/-100 band)
higher per-call cost              : 1.09x
product                           : 1.29x   vs actual 83.0/64.5 = 1.29x

Neither term needs a new explanation. Still not banked as a larger win — N=1 per arm and the
per-call figure remains host-sensitive — but "unexplained" was the wrong word.

A coherence check the receipt contains and I had not claimed

Calls fell 24.1% while cost fell 32.7%. Those differ because the removed calls were
more expensive than typical:

removed calls  : 83.0 s / 700  = 118.6 ms
base mean      : 253.9 s / 2900 =  87.6 ms
ratio          : 1.35x

This independently agrees with a separate measurement built for a different question. Cost
bucketing on the base tree found repeat peels cost ~4.9× first-occurrence peels; this change
deletes a repeat pass (the second consumer peeling an already-peeled formal); so the pair
should remove above-average-cost calls — and it removed them at 1.35× the mean. Two
instruments, different questions, agreeing on a consequence neither was measuring.

Both readings are tidy-pike-117's; the arithmetic is reproduced above and checks out.

— sent from fierce-raven-463

@gunbai-bot

gunbai-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Correction to my own receipt: the measured binary carried the plan only, not the duplicate-resolve fix

Self-caught, and it narrows what the before/after receipt above establishes.

The measured branch tree's .dag authority carried both commits, but its emitted seed did not.
peel_nominal_alias_identity in the measured binary has 4 resolve_node_bounded calls; the
reviewed commit has 3. Cause: I ran git checkout -- src/v1/stage0/src/v1_compiler_infer_resolve.rs
to strip a measurement probe before rewiring it, and that also reverted the regenerated seed
carrying the duplicate-resolve deletion. The .dag stayed patched, so nothing looked wrong.

What this changes:

  • The pair (−700 peel calls, −83.0 s) measures the CallApplicationPlan alone. It does
    not include the duplicate-resolve fix. That is a cleaner attribution for the plan than I
    claimed — but it is not what I labelled it.
  • The post-plan census (repeat share 0.9801) is likewise plan-only.
  • My earlier provenance comment said the measured tree differed from the reviewed commit in that
    file "instrument only". That was wrong: it differed by the instrument and by the missing
    fix. The three files carrying the plan still hash-match the reviewed commit exactly.

What it does not change: the duplicate-resolve deletion is a proven equivalence — the arm
recomputed resolve_node_bounded(target) under a guard identical to the one that already bound
structural to the same call — so it cannot alter behavior, and both reviewers confirmed it by
reading. It simply has no execution receipt yet, where I implied it had one.

This is the .dag-versus-emission trap in the direction that flatters: the authority is correct,
the realization silently is not, and only the emitted seed answers a question about what actually
ran. Same class as the env: TypeEnv / Rc<TypeEnv> error earlier in this lane.

I will re-measure the second commit separately rather than restate the pair as covering both.

— sent from fierce-raven-463

@gunbai-bot

gunbai-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Re-run: the three branch-only failures did not reproduce

Job re-run at the identical head 1caa2f2ce:

required-floor: planned=9418 executed=9418 terminal=9418 passed=8605 known_red_held=786 failed=27

The three wall-budget rows flagged as branch-only are absent — 0 failing rows each for
roadmap_altitude_witness, roadmap_dispatch_actuator, and
roadmap_receipt_continuity_acceptance. Same code, different outcome ⇒ host variance, settled
by measurement rather than by argument.

One flip worth naming, and why it is not evidence for this PR

test.claim.record_literal_call_arg_handoff_witness.record_literal_container_element_mismatch_at_call_arg_still_refuses
is enrolled as expected-red. It was red on main (515 ms) and red on this PR's first run
(547 ms); on the re-run it passed (465 ms), tripping "enrolled as expected-red and PASSED".

That witness is about record literals at call arguments — precisely the seam this PR touches, so
the tempting reading is that the change fixed it. It did not. Same head, red then green across
two runs of identical code: the flip is nondeterministic, and its timings sit where the flakiness
would. Claiming it would be attributing a coin-toss to the diff.

The check itself currently carries no signal

main has not been green since 2026-08-16T21:33 (last green 31973836963 at 7d06e3783), with
60 commits landed since and every one of the 20 most recent runs failing. With ~158 modules
failing on every branch, the witnesses check returns FAIL for clean and broken branches alike.
The only instrument that works today is a set difference against a main run at the same base
— not the check state, not the failure count, not the job verdict.

Measured that way, at module grain against main tip 3abc17b54 (zero commits landed since, so
same-base rather than approximately-same-base): this PR has 0 branch-only failing modules.

Re-run measurement and the main-greenness history are tidy-pike-117's; the three-row
non-reproduction and the expected-red flip are verified above against the job logs.

— sent from fierce-raven-463

@gunbai-bot

gunbai-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Two corrections to my own comments above

1. The ~158 failing modules framing was inflated. I quoted a module count that was mostly
the expected-red roster doing its job. The number that decides anything is the floor's own
failed count:

this PR, re-run   failed=27
main 3abc17b54    failed=40

2. A claim I made and am now withdrawing until re-measured. I wrote that distinct_n was
unmoved (176 → 177) while 700 calls disappeared, and called it the strongest evidence here —
behavioral equivalence green by execution. That comparison was cross-base: 177 came from
main+plan, 176 from the floor-cut tree. The same-base figure is 179 → 177, a decrease, so
"the plan deleted no distinct work" does not survive as stated. It is also not yet comparable —
the two probes printed every 20 vs every 200 calls, so the post-plan row omits up to 199 calls.

Withdrawn pending a rebuild with one identical probe on both arms. If it holds as a decrease I
will report it as a decrease and drop the conservation argument rather than reshape it.

This is the same error I refused one paragraph earlier when declining a cross-base subtraction.
I applied the rule to the number that flattered my prediction and skipped it on the number that
flattered my framing.

Provenance standard for the re-measurement

Cost and identity claims are answered by the emitted seed, not the .dag — that is what let
an earlier receipt measure one commit while the authority showed two. The pending pair therefore
carries binary provenance, captured before the runs finished:

base   HEAD 3abc17b54   binary 179375783af59779
       seed v1_compiler_infer.rs         7be8214b29df8681
       seed v1_compiler_infer_resolve.rs cfb2ecb2411dc24d
       peel _inner resolve_node_bounded  4    build_call_application_plan 0

branch HEAD 1caa2f2ce   binary 60cee20962576c7d
       seed v1_compiler_infer.rs         cbceb06867022955   (== reviewed commit, no probe)
       seed v1_compiler_infer_resolve.rs b6d885e7eccd0e6d
       peel _inner resolve_node_bounded  3    build_call_application_plan 1

The two structural facts — 4-vs-3 resolve calls, plan absent-vs-present — make the hashes
checkable rather than decorative. Both arms are checked out from real refs; the earlier receipt's
defect came from hand-patching a tree, which let its seed diverge from its .dag silently.

Grain correction and the seed-provenance construction are tidy-pike-117's.

— sent from fierce-raven-463

@gunbai-bot

gunbai-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Definitive re-measurement — the withdrawn claim returns in a corrected form

Both arms checked out from real refs (no hand-patched reconstruction), one identical probe,
same host, sequential, both runs terminal, zero invariant violations.

arm     HEAD        binary            seed infer.rs      seed infer_resolve.rs  peel resolve  plan
base    3abc17b54   179375783af59779  7be8214b29df8681   cfb2ecb2411dc24d       4             0
branch  1caa2f2ce   60cee20962576c7d  cbceb06867022955   b6d885e7eccd0e6d       3             1
calls distinct distinct-work cost repeat cost peel total reconcile
base 2,900 179 3.2419 s 245.99 s 249.24 s 8 min
branch 2,280 177 3.2440 s 177.76 s 181.01 s 6 min
−620 (−21.4%) −2 +0.064% −68.2 s −68.2 s (−27.4%) 8→6

The behavioral-equivalence receipt, stated correctly this time

I earlier claimed distinct_n was unmoved and withdrew it as cross-base. Same-base, same
instrument, it did move: 179 → 177. So the conservation argument does not hold on the count,
and I am not restating it.

It holds on cost, which is the quantity that matters and a stronger statement:

  • 618 of 620 removed calls (99.7%) were repeats; 2 were distinct.
  • Distinct-work cost is invariant to +0.064% — 3.2419 s vs 3.2440 s, on two separately
    compiled binaries.

A change that removed real distinct work would move that number. It does not move. That is
behavioral equivalence green by execution rather than confirmed by reading the diff — and it
covers both commits, since this branch arm is the reviewed commit with the duplicate-resolve
fix present in its seed (3 resolve calls, verified above), which the earlier receipt lacked.

Not claimed: the −2 distinct calls. Rows print every 20, so up to 19 calls after the final row
are unreported in each arm; −2 of 179 (1.1%) is within that tail. I am not asserting it is
sampling, only that it is not established as real.

Corpus-wide env result

distinct_env > 1 was observed for zero modules across both full runs. Every module peel was
called with saw exactly one env pointer. That is the corpus-wide version of a fact previously
established for one module, and it is what a module-scoped memo would rest on.

What remains

Post-plan repeat_share_of_cost = 0.9821 — 177.8 s of 181.0 s on this module is still repeat
keys. The plan removed the duplication; it did not remove the reuse. The memo remains a separate
change with its own soundness argument.

— sent from fierce-raven-463

@gunbai-bot

gunbai-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Closing the follow-on question: the remaining 177.8 s should not be pursued

The receipt above leaves repeat_share_of_cost = 0.9821 — 177.8 s of 181.0 s on that module is
still repeat keys, which reads as an obvious follow-on memo. It should not be built, and the
reason is decidable from source rather than from a measurement.

A pure .dag has no mutable cache. The precedent is TypeEnv.intern_table: built once,
threaded read-only. So the honest shape is a precomputed derived table — good, until the key.

The key has no sound cheap option over v1's carrier:

candidate sound cheap expressible in .dag
Rc pointer identity (what the measurement used) yes yes no — a fact about the seed
SourceSpan no yes yes
content hash yes no yes

The span option fails on soundness, proven from one line: substitute_generics_apply
constructs Node { name: n.name, span: n.span, … }, preserving the span while substituting
children — and the call site is peel(substitute_generics(formal_raw, call_subst)). Two calls to
one generic function at different type arguments therefore produce same-span, different-content
nodes. A span-keyed table would hand one call site's substituted type to another's, silently, and
would have passed review by anyone reasoning from "spans are unique source positions."

Content hashing is sound but must walk the node at lookup — roughly the work resolve_node_bounded
does. src/v1/00_core.dag's Node carries no hash field, so there is nothing to read instead.

And the seed-only shortcut is what DESIGN forbids. v1 is frozen, and a frozen X's growth
surfaces accept no new rows; a seed-only memo is also authority drift, since the .dag would stop
describing what runs.

So the cost dies with v1. v2.std.node's Node carries
occurrence_id: NodeOccurrenceId = SyntheticOccurrence | MintedOccurrence { id } — a cheap, stable,
.dag-expressible identity that is the direct analogue of the pointer this measurement keyed on,
with exactly the conservative shape wanted (MintedOccurrence can hit; SyntheticOccurrence must
miss — misses, never wrong answers). The capability exists on the carrier that replaces this one.

Which makes this PR's shape right rather than merely safe: it removes the duplication with
no key at all, and the keyed alternative turns out to be unsound on the cheap key and
self-defeating on the sound one over this carrier.

Not claimed: that v2's occurrence_id survives v2's analogue of substitute_generics — the v1 span
key died exactly there, and the same question must be asked before anyone leans on it. Nothing here
is measured on v2.

The v1-versus-v2 reframe is tidy-pike-117's; the mechanism correction and the span proof are
verified above against source.

— sent from fierce-raven-463

@gunbai-bot

gunbai-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Correction to my previous comment: the "conservative by construction" claim is false for the minted arm

I wrote that v2's NodeOccurrenceId has "exactly the conservative shape wanted
(MintedOccurrence can hit; SyntheticOccurrence must miss — misses, never wrong answers)."
That is wrong for the minted arm, and it is wrong for the same reason the v1 span key was.

v2.compiler.resolve canonical_atom constructs
TypeNode { connective: Atom { identity: canonical } } — a different identity — while carrying
occurrence_id straight through from its input, and its use site passes n.occurrence_id. So
two different contents can carry one minted occurrence id. A minted-keyed lookup could return
the pre-canonicalization node for a post-canonicalization query: a wrong answer, not a miss.

Measured, not inferred: 17 sites in src/v2 construct a Node copying occurrence_id from an
input
, at least one of which changes identity while doing so (v2.compiler.resolve
canonical_atom; also v2.std.compilers.sugar, v2.std.node, v2.std.node_minimal).

I had flagged this as "not verified — the v1 span key died exactly there, and the same question
must be asked before anyone leans on it." It is now verified as a live hazard. Only the
SyntheticOccurrence arm is conservative by construction; the minted arm would need qualifying by
whatever distinguishes canonical_atom's output from its input.

The conclusion is unchanged: nobody builds this memo — not in the seed, not in v1's .dag, not
as a new substrate capability. The 177.8 s dies with v1. And this PR's shape is unaffected: it uses
no key at all, which is why none of this reaches it.

Hazard measured by tidy-pike-117; verified above against source.

— sent from fierce-raven-463

@gunbai-bot

gunbai-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Retraction: "a pure .dag has no mutable cache" is false, and so is what I built on it

Two comments above I wrote "A pure .dag has no mutable cache" and concluded that a precomputed
derived table was "the honest shape," then that "the keyed alternative is not expressible."
Retracting all three. They conflate four separate questions:

  1. whether a function has implicit ambient mutation — the only one purity decides;
  2. whether cache state is explicitly modeled;
  3. whether the current carrier has a sound cheap key;
  4. whether frozen-v1 policy permits the implementation.

A cache is a modeled state transition, CacheState × Request → CacheState × Response, and is
perfectly expressible purely — old state is not mutated, a new state is returned, and a compiler may
realize it as &mut HashMap without changing .dag semantics. Observationally-pure internal
memoization is also legitimate provided hits and misses give the same result, eviction and iteration
order cannot affect output, a cache failure falls back to computation rather than fabricating, and
the key covers every semantic dependency.

TypeEnv.intern_table proves only that eager precomputation is used. It says nothing about
whether lazy, state-threaded, or scope-local caches are available. Citing it as the bound on what
.dag can express was the error.

What survives, unchanged and still load-bearing

  • Span is an unsound key. v1.compiler.infer substitute_generics_apply preserves the span
    while substituting children — same span, different type.
  • occurrence_id is not semantic revision identity. v2.compiler.resolve canonical_atom
    changes identity while carrying it through; 17 v2 sites copy it from an input.
  • A node key alone is insufficient regardless. The same node resolves differently in different
    environments, so a sound key is closer to
    (node_semantic_revision, type_env_identity, substitution_identity, mode, bound) — unless the
    memo is scoped to one fixed environment.
  • The realization-versus-language distinction: Rc<Node> is evidence about a realization; the
    .dag should model the semantic equivalent, not expose "pointer" as a concept.

And one correction to the conclusion's status

"The 177.8 s dies with v1" is a migration and economics argument, not an expressibility theorem.
The honest decomposition:

expressible?                                    yes
sound with span or occurrence_id alone?         no
possible with explicit state + composite key?   yes
admissible inside frozen v1?                    probably not, under freeze policy
economically worthwhile before v1 deletion?     depends on v1's remaining lifetime

I collapsed the last two into "nobody should build this," which overstates what I established.

None of this reaches this PR, which uses no key and no cache state at all — but the claims were
wrong where they stood and the corrections belong beside them.

Refutation from the operator's review thread.

— sent from fierce-raven-463

gunbai-bot Bot pushed a commit that referenced this pull request Aug 18, 2026
…uled

Two defects in the admitted-population note, both found by review rather than
by me, and both of the same kind -- the record claiming more than the work.

The memo row described a semantic formal-instantiation key. That is no longer
the ruled design: the ruling selected an observationally transparent
realization keyed on exact host allocation identity over a narrow wrapper at
the measured seam, and declined the semantic key because its prize is bounded
by the 1.3% first-call cost while non-idempotence makes its soundness proof
expensive. The row also listed the memo among what the standing HAS admitted,
before it exists. A standing that counts unbuilt work among its admissions is
inflating its own record, which is the failure this carrier was written to
avoid. It now reads as ruled-but-not-landed with no equivalence receipt.

The 8-to-6-minute figure was stated unqualified, while gunbc#8431 says it has
no floor before/after receipt. Both are true -- that pair is a whole-corpus
compile measurement, not a floor run -- but unqualified it reads as the floor
receipt, making this row the sole authority for a result nobody measured.
Qualified, and it now says explicitly that #8431 carries no floor receipt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PjAhodiDNy96tME6deseFx
gunbai-bot Bot pushed a commit that referenced this pull request Aug 18, 2026
Two corrections from review, both making the change safer rather than larger.

SUBJECT NARROWED. The contract named peel_nominal_alias_identity itself, which
has 15 call sites in v1.compiler.infer -- so memoizing it would have changed
allocation sharing at every one, and would have sat underneath the
record-literal path whose SECOND peel takes an already-peeled node. That
double application is measurably not idempotent (21 counterexamples over
600,000+ calls, six structural). The contract now names peel_direct_call_formal,
a pure wrapper that forwards unchanged and is called only from
build_call_application_plan. Uncached it trivially denotes the operation it
wraps; the memo population becomes exactly the measured population; and the
dissolution surface is one call seam rather than a general v1 canonicalization
cache. No general "memoize any function" registry: in a semantics-frozen seed
that registry is itself a growth surface.

SCOPE GENERATION ADDED, closing a hole I had left open. The uncached
computation must run with no borrow held, because it re-enters compiler
machinery and a held borrow would panic. During that window the bound scope
can change. The previous code then inserted unconditionally, which would plant
a result computed under the old scope into its replacement -- a wrong hit
later, the exact failure this design exists to prevent. enter_scope now returns
a generation, and insert_if_current refuses when it no longer matches. A miss
is always correct, so refusing to insert costs a recomputation and nothing else.

RcIdentity now OWNS the reference it compares rather than keying on a bare
address, so the pointer used as evidence cannot outlive what it points at.

Stacked on #8431, which supplies build_call_application_plan.
Still unverified: no regen, no equivalence receipt, no controls executed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PjAhodiDNy96tME6deseFx
@briansrls
briansrls merged commit f480246 into main Aug 18, 2026
0 of 2 checks passed
@briansrls
briansrls deleted the spring/call-application-plan-2 branch August 18, 2026 23:24
briansrls added a commit that referenced this pull request Aug 18, 2026
…#8450)

* Reclassify v1 from frozen to semantics-frozen with active maintenance

The v1 seed was carried under DESIGN 3's frozen-X carve-out: no new
investment, no completion work, quarry-in-place. That state is correct for
an X whose replacement is advancing toward a bounded cutover. It is the
wrong state for a production-critical X whose replacement was paused
indefinitely, which is now v1's situation -- gunbc#8352 closed unmerged and
integration/v1-cut is quarry, so no bounded event retires v1 from the
required-floor path.

Three things cannot be held together: v1 stays on the critical path
indefinitely, v1 deletion has no schedule, and v1 accepts no maintenance
regardless of recurring cost. This changes the third, deliberately and
visibly, rather than letting maintenance happen under a label that forbids
it -- which would be rung inflation applied to a lifecycle word, and worse
than either honest option because it makes the maintenance invisible.

gunbc.v1_maintenance_standing carries the status as a typed carrier rather
than DESIGN prose, per 4c: four admitted change classes and five refused
ones as closed vocabularies, the refused list carrying exactly the growth
surfaces the carve-out names.

Stated honestly in the carrier itself: this sits at mitigatable. Nothing
mechanically refuses a refused-class change; the vocabulary is consumed by
review diligence, which is strictly weaker than the freeze it qualifies.
The next-rung trigger names the two refused classes that are decidable
from a diff against the prior seed emission and does not claim the
undecidable half will climb. The carrier also names its own standing
danger -- an active-maintenance arm becomes an absorbing one if every
proposal classifies as admissible -- and records a refused proposal as
evidence the vocabulary discriminates.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PjAhodiDNy96tME6deseFx

* Reconcile the standing's record with what was actually measured and ruled

Two defects in the admitted-population note, both found by review rather than
by me, and both of the same kind -- the record claiming more than the work.

The memo row described a semantic formal-instantiation key. That is no longer
the ruled design: the ruling selected an observationally transparent
realization keyed on exact host allocation identity over a narrow wrapper at
the measured seam, and declined the semantic key because its prize is bounded
by the 1.3% first-call cost while non-idempotence makes its soundness proof
expensive. The row also listed the memo among what the standing HAS admitted,
before it exists. A standing that counts unbuilt work among its admissions is
inflating its own record, which is the failure this carrier was written to
avoid. It now reads as ruled-but-not-landed with no equivalence receipt.

The 8-to-6-minute figure was stated unqualified, while gunbc#8431 says it has
no floor before/after receipt. Both are true -- that pair is a whole-corpus
compile measurement, not a floor run -- but unqualified it reads as the floor
receipt, making this row the sole authority for a result nobody measured.
Qualified, and it now says explicitly that #8431 carries no floor receipt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PjAhodiDNy96tME6deseFx

* Extend the documentary-refs fixture to the fifth generated ref; order the maintenance vocabulary

The witness is a controlled fixture pinned at four generated refs, and this
branch added a fifth. All four failures fell out of that one fact: the union
claim reads length(generated) == 4, the intact-population claim found no
planted fact or module for the new ref and so counted a refusal, and both
perturbation controls read the clean count as their baseline and shifted with
it. The fixture is extended on both sides -- the module and the declaration are
planted, and the expected count is authored -- so it stays a controlled fixture
rather than becoming a measurement of the current tree. The new ref is also
added to the renders-the-rows claim, so the row is proven load-bearing rather
than merely enrolled for resolution.

Two vocabulary corrections from review. ChangeCarryingDeletionTrigger is
REMOVED from the admission coproduct rather than relocated: a change that
carries its own deletion trigger is not thereby semantically admissible, and
treating the trigger as an admission reason is the self-authorized-dissolution
failure DESIGN section 5 names -- a dissolution condition describes how
admitted debt ends, it does not authorize creating the debt. It would have been
the widest arm in the list, since almost any v1 change can be narrated as
carrying a trigger.

And the two vocabularies are ordered rather than peer: any refusal dominates
every admission. Without an order, a proposal matching both an admitted and a
refused class had no stated outcome, which is exactly how MaintenanceActive
becomes the absorbing arm the ruling note warns about. The ordering is stated
and declared UNENFORCED, for the same reason there is no verdict type.

The design paragraph said "four admitted"; it now says three and carries the
ordering.

* Regenerate the DESIGN.md projection the carrier paragraph is authored into

DESIGN.md is a tracked generated artifact -- gunbc.generated_artifact maps
DesignArtifact to CommitRequired{ProjectDocumentation} -- and this branch
edited dag/gunbc/design_document.dag without regenerating it, so the authority
carried the qualifying paragraph and the rendered document did not.

The witness cannot catch this and is not at fault: documentary_refs_generated_
document_renders_the_rows asserts against expected_design_md(), the COMPUTED
value, so it is structurally unable to observe that the committed file has
drifted from it. What established the omission was the corpus's own practice
rather than a reading -- each of the last eight commits touching
design_document.dag also updates DESIGN.md, and this branch was the only
exception.

Regenerated through the artifact gate's own writer (dag/tools/generated_
artifact_gate.dag main_wet) rather than hand-edited, which is what the
generated-artifact rule requires. The diff is the two lines the new paragraph
adds. That run also reported drift in docs/plans/discrete-cost-derivation.md,
which is pre-existing on main and belongs to whoever edited that plan carrier;
it is deliberately NOT included here rather than swept into a carrier PR.

---------

Co-authored-by: Brian Searls <briansearls1@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
gunbai-bot Bot pushed a commit that referenced this pull request Aug 18, 2026
#8431 and #8450 squash-merged into main while this branch carried their
original commits, so git saw main's squashed versions and this branch's
originals as competing changes. The two conflicts are that artifact,
not a semantic disagreement:

  src/v1/04_infer.dag             main lacks the memo entirely, so both
                                  hunks are pure additions from this
                                  branch and keeping our side is correct.

  v1_compiler_infer.rs           generated. The merge driver refused
                                  rather than picking a side, which is
                                  right: neither side's bytes are the
                                  projection of the merged authorities.
                                  Committed here at the ours side so the
                                  regenerators run against it, and
                                  replaced by regeneration in the next
                                  commit -- not resolved by hand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PjAhodiDNy96tME6deseFx
gunbai-bot Bot pushed a commit that referenced this pull request Aug 19, 2026
Resolving 04_infer by taking main's whole file and running the strip-and-
qualify pass over it cost 483 diagnostics: 119 annotations reported as naming
no subject, and a parse desync that surfaced 3,000 lines later as
`expected Eq, found Dot` in ordinary argument syntax. The file is ten
thousand lines and the pass rewrote 472 names in it, so locating the break by
inspection was the wrong instrument.

Re-applied main's change as a patch onto the branch's copy instead -- 54
insertions against a file this branch had already qualified, rather than a
whole-file replacement needing 472 fresh rewrites. Main's semantics taken at
both conflicts (the CallApplicationPlan seam from #8431), then nine names
qualified in the incoming hunks.

The general lesson for this branch's merges: for a file the branch has
already worked, take the INTERVAL and not the file. A whole-file resolution
throws away the qualification and asks the pass to redo it, and the pass is
the part with the failure modes.

src/v1 529 -> 46; floor unchanged at 68.
briansrls added a commit that referenced this pull request Aug 19, 2026
At the direct-call seam build_call_application_plan stores
peel(formal_subst) as CallFormalApplication.formal. The structured
application predicate was then handed formal_subst and immediately
recomputed exactly that peel -- the same function on the same node, once
per structured record-literal argument, on every path. #8431 built the
value and left this consumer recomputing it.

peel_nominal_alias_identity is resolve_node_bounded, an unmemoized
recursive rebuild allocating a fresh Node per level, and it measured as
essentially the entire cost of this conformance seam. So this is the same
class as #8431: derive once, carry the result, let each consumer project
its own judgment.

WHY A SPLIT RATHER THAN A CHANGED SIGNATURE. The predicate has two call
sites. The direct-call site has the peeled formal in hand; the
record-literal FIELD site (infer_record_lit's arm) does not, and its
nearby formal_peeled is peel(with_required_cardinality-or-resolved(...)),
a DIFFERENT value -- passing that would have altered semantics rather
than only cost. So the peeling entry point is retained for that site and
a _with_peeled variant takes the value the caller already has.

Both formals are carried because the body still needs the unpeeled one
for structured_application_lit_is_declared_optional_variant.

Equivalent by construction: the call now passes formal_peeled: formal,
where formal = app.formal = peel(app.formal_subst), which is exactly what
the callee previously computed from formal_subst. Same expression, same
input, same value.

NOT TOUCHED, deliberately: direct_call_structured_record_literal_resolved_type_mismatch
peels its already-peeled argument a second time. That is peel(peel(x)),
and peel is measurably NOT idempotent (21 counterexamples over 600,000+
calls, six of them structural), so deleting it would change semantics.
It must be carried, not dropped, and that is a separate change.

Diagnostics: 483 errors on this tree and 483 on unmodified main under the
same invocation -- a matched baseline, not a clean typecheck. The
whole-corpus diagnostic multiset, graph fingerprint and floor A/B are the
PR's bar and run remotely.


Claude-Session: https://claude.ai/code/session_01PjAhodiDNy96tME6deseFx

Co-authored-by: Brian Searls <briansearls1@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Brian Searls <11205878+briansrls@users.noreply.github.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