Skip to content

PrimitiveCostFact carries the structured cost, and the duplicate carrier goes - #11125

Merged
briansrls merged 2 commits into
mainfrom
cost/primitive-cost-bound
Sep 12, 2026
Merged

briansrls merged 2 commits into
mainfrom
cost/primitive-cost-bound

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

PrimitiveCostFact.cost moves from the coarse CostShape to the structured CostBound that std.realization_cost_fidelity actually compares, closing the two-carrier fork that module's own frontier note named and scheduled. Follow-up to review 63996 finding 2 on #11026.

What the migration found before it changed anything

The note that scheduled this work said "the six populated rows record a shape and NO PARAMETER". Two corrections, both load-bearing:

  • There are five rows, not six. The sixth was the type PrimitiveCostFact { declaration, counted by a grep. The count is corrected in that note by this PR, which also retires it.
  • No row's parameter is recoverable from its signature. Three of the five (sorted_map_keys, fold_list, index_by) are SignatureUngrounded, and each states in its own reason that authoring a signature would fabricate it. The other two ground on FinitelySupportedFunctionProfile, which supplies type params K,V and a FiniteSupport classification but no cardinality variable. So the axis for the two grounded rows is derived from the carrier's declared support, not read off a signature.

The shape of the change

PrimitiveModeledCost has two arms: StructuredCost { bound } for a row whose axis is derivable, and CoarseCostOnly { shape, cause } for one whose is not.

The coarse shape survives on the refusal arm. Dropping it would have deleted ShapeSortBody and ShapeIterateBody — facts that were true and never in question. Only the structured form is unavailable for those rows, so the two-grain state survives exactly where it is declared with a typed cause, instead of silently across the population.

The axis has exactly one producer. finite_support_cardinality_param reads the carrier's own support axis; linear_support_scan_cost is the single constructor every structured row goes through. No row authors a parameter literal, so two rows about one carrier cannot spell the same axis differently — which nothing in the type system would otherwise prevent, since AtomicCost.param is a bare String.

Evidence

claim_batch over the new suite: 14 witnesses, 14 PASS, rc=0. The existing suites that touch this module are unaffected and re-run green: realization_cost_fidelity_witness 39, primitive_identity_join_witness 16, primitive_signature_grounding_witness 11. All eight modules importing std.primitive_identity compile with 0 blocking errors.

Green witnesses prove nothing on their own, so each discriminating arm was planted against, with the failures predicted in advance:

plant predicted observed
open-support carrier names an axis the 2 refusal arms exactly those 2
bound built quadratic, not linear the 2 shape-agreement arms 4 — see below
map_values authors its own literal axis the axis-identity arm exactly that 1

The second plant failed two arms I had not predicted, and that exposed a defect in my own witness: both_grounded_rows_scale_in_the_same_derived_axis was comparing coarse shapes, so two rows with different parameter names would both have passed it. It now compares the actual parameter names via cost_bound_params, and the third plant confirms it fails on exactly that divergence.

The duplicate carrier goes with it, and the cut is at the root

ModeledCostBoundFact carried the same (PrimitiveIdentity, CostBound) pair as this carrier and had no production rows at all — its only constructions in the tree were fixtures. Leaving two carriers of one fact standing is the §3 fork that gets consolidated later at interest, so it is deleted here rather than after, and §3's replacement-migration rule says the cut goes at the root: the join's signature.

realization_cost_fidelity_rows now takes List<PrimitiveCostFact> directly. That collapses its two input lists into one, and the consequence is worth stating precisely because it is a ladder climb rather than a tidy-up:

  • The join used to take modeled and unstructured separately, so a primitive appearing in both was a contradiction about one key that only a check could catch — PrimitiveIsBothStructuredAndUnstructured.
  • With one list whose rows carry a coproduct, a row is structured or coarse-only and cannot be written as both. The state has no constructor.
  • That is §4b's climb from mechanically preventable to structurally impossible, and §5's "prefer a single authority the realization is derived from over a check that flags the bad state after the fact".

No coverage was lost to that deletion. The old both-lists specimen becomes an ordinary duplicate — the same primitive on two rows, once structured and once coarse — which the surviving arm still refuses, and the witness still exercises it. The refusal type drops from three arms to the one that remains writable, and it is renamed: StructuredPrimitiveDuplicated said "structured" when there were two lists, and the single list holds both kinds, so keeping that spelling would have left one name covering two populations — the same §3 meaning fork this PR closes one layer up.

Declared frontiers

§3c consumption. The five rows had no roster and no consumers — dangling, while their sibling PrimitiveTraversalOrderFact has both. This PR adds primitive_d0_cost_facts, and the row says plainly that the roster is the identity join's subject universe, not the consumer.

  • Consumer: std.realization_cost_fidelity realization_cost_fidelity_row, reached through realization_cost_fidelity_report.
  • Route: the structured rows supply that join's modeled side. That side is typed ModeledCostBoundFact, whose only constructions in the tree are fixtures in test.claim.realization_cost_fidelity_witness — a shape with no production supplier, which is what these rows are shaped for.
  • Trigger: the first construction of a RealizationCostFact outside dag/test/ — stated as that symbol rather than "when measurements exist", because the capability it stands for (the emitted compiler being measurable per primitive, so a realized cost is an observation rather than an assertion) is exactly what the first non-fixture producer demonstrates.
  • Kill condition: if such a producer lands and these rows are still unread by that join, the frontier was wrong and the rows are dangling, not deferred.

§2 next rung. AtomicCost.param is a bare String — an anemic leaf carrying a modeled quantity as free text. The single-producer discipline above is a containment, not a fix: it makes this module's rows agree with each other and can do nothing about another module naming the same axis differently. Trigger: std.induction AtomicCost carrying a parameter identity rather than String, which dissolves the discipline into the type. Not attempted here — it rewrites every cost-bearing row in the tree.

What is still open

The realized side. No RealizationCostFact exists outside dag/test/, so no production caller passes the roster to the join yet — that is the §3c frontier above, and no amount of work on this carrier can supply it. The modeled side is now one carrier, read directly by the join, and executes in the witness suite.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RPVTQBSK9E8j3UDQGPZN1f

Brian Searls and others added 2 commits September 12, 2026 02:16
…ier goes

`PrimitiveCostFact.cost` moves from the coarse `CostShape` to the
structured `CostBound` that `std.realization_cost_fidelity` compares,
closing the fork that module's own frontier note named. Follow-up to review
63996 finding 2 on gunbc#11026.

TWO CORRECTIONS THE MIGRATION FOUND BEFORE CHANGING ANYTHING. There are
FIVE rows, not the six that note claimed -- the sixth was the `type`
declaration, counted by a grep -- and the count is fixed in the note this
change retires. And no row's parameter is recoverable from its signature:
three are `SignatureUngrounded` and each says authoring one would fabricate
it, while the two grounded rows use a profile that supplies type params and
a `FiniteSupport` classification but NO cardinality variable. So the axis is
DERIVED from the carrier's declared support, not read off a signature.

ONE PRODUCER FOR THE AXIS. `finite_support_cardinality_param` is the only
place that string is produced and `linear_support_scan_cost` the only
constructor the rows go through, so two rows about one carrier cannot spell
one axis differently -- which nothing in the type system would otherwise
prevent, `AtomicCost.param` being a bare `String`. The coarse shape SURVIVES
on the refusal arm: dropping it would delete `ShapeSortBody` and
`ShapeIterateBody`, facts that were never in question, when only the
structured form is unavailable.

THE DUPLICATE CARRIER IS DELETED AND THE CUT IS AT THE ROOT.
`ModeledCostBoundFact` held the same (primitive, cost) pair with no
production rows at all -- fixtures only -- so it is removed and
`realization_cost_fidelity_rows` re-signatured onto `PrimitiveCostFact`.
That collapses its two input lists into one, and the consequence is a ladder
climb rather than a tidy-up: a primitive in BOTH lists used to be a
contradiction only a check could catch, and with one list of coproduct-
bearing rows it cannot be written at all. Section 4b's mechanically
preventable to structurally impossible, and section 5's construction over
validation.

NO COVERAGE WAS LOST TO THAT DELETION, which is the part worth checking
rather than asserting. The old both-lists specimen becomes an ordinary
duplicate -- one primitive on two rows, once structured and once coarse --
still refused by the surviving arm and still exercised. The refusal type
drops to the one arm that stays writable and is RENAMED:
`StructuredPrimitiveDuplicated` said "structured" when there were two lists,
and the single list holds both kinds, so the old spelling would have left
one name over two populations.

EVIDENCE. 101 witnesses pass across five suites: the new
`primitive_modeled_cost_witness` 14, `realization_cost_fidelity_witness` 39,
`primitive_identity_join_witness` 16, `primitive_signature_grounding_witness`
11, `primitive_projection_authority_witness` 21. All nine modules that
import `std.primitive_identity` compile with 0 blocking errors. Four planted
defects, each with its failures predicted BEFORE the run: an invented axis
for an open-support carrier failed exactly the 2 refusal arms; a quadratic
bound failed 4 (two more than predicted, which exposed that the axis-identity
witness was comparing coarse SHAPES -- it now compares parameter names via
`cost_bound_params`); a per-row literal axis failed exactly that 1; and
disabling the duplicate check failed exactly the 3 partition specimens.

FRONTIERS, BOTH DECLARED ON THE CARRIER. Section 3c consumption: the join is
the consumer and takes this carrier DIRECTLY, with the realized side and a
production caller still missing; trigger is the first `RealizationCostFact`
constructed outside `dag/test/`, with a kill condition if that lands and
these rows stay unread. Section 2 next rung: `AtomicCost.param` is an anemic
`String` and the single-producer discipline is a CONTAINMENT, not a fix;
trigger is a parameter identity in `std.induction`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RPVTQBSK9E8j3UDQGPZN1f
The frontier note still said the cut was "named here and sequenced
separately rather than performed inside a change that cannot establish its
inputs". This PR performs it, so that sentence described a plan that no
longer exists -- the stale-present-tense class, in the authority a reader
consults first, which is the thing this note is most likely to be read for.

Replaced with what performing it ESTABLISHED, which is more than the
deferral assumed: there are FIVE populated rows, not the six this note
claimed (the sixth was the `type` declaration, counted by a grep), and NO
row's parameter is recoverable from its signature -- three are
`SignatureUngrounded` and say so themselves, and the two grounded rows use a
profile carrying type params and `FiniteSupport` but no cardinality
variable, so their axis is DERIVED from the carrier rather than read off a
signature. The load-bearing half survives in substance because it is still
true and is why the migration could not be a substitution: an invented
parameter produces a spurious axis and a fabricated verdict, which is this
authority's own subject authored into its inputs.

Found by re-reading the change against the REQUEST rather than the review
tally. The tally was already met -- one distinct approval, no
request-changes, CI green, GitHub clean -- and the request had named
retiring this note as part of the work.

Evidence: gunbc compile of std.realization_cost_fidelity, 0 blocking
errors; claim_batch over realization_cost_fidelity_witness, rc=0, 39 PASS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RPVTQBSK9E8j3UDQGPZN1f
@briansrls
briansrls merged commit 92e114e into main Sep 12, 2026
6 of 8 checks passed
@briansrls
briansrls deleted the cost/primitive-cost-bound branch September 12, 2026 06:28
gunbai-bot Bot pushed a commit that referenced this pull request Sep 12, 2026
…h authorities survive

The only conflict was src/v2/compiler/00_compile.dag, and it was two sides adding
imports to the same block rather than a rename or a contested declaration.
Resolved as a per-module union: std.algebra keeps this branch's `trim` beside
main's Cons/Empty/FreeMonoid; v2.std.algebra keeps contains/is_empty/length and
the promoted list_find_first beside main's folds; v2.std.collection takes main's
Map/empty_map/map_insert/map_lookup beside List; and main's new std.decl_ref,
std.content_hash, std.types, v2.std.integer and v2.std.logic imports all land.
One import line per module, no duplicate module imports.

Verified rather than assumed, since a clean auto-merge is weak evidence when both
sides touch the same closure: this branch's native_lane_universe,
native_lane_closure_modules, native_lane_module_for_path and list_find_first are
all present; main's #11004 lens-registration join is present (85 references to
CompileLens / declaration_ref_display_key / content_hash_of_value); the file is
brace-balanced; the seventh row is intact in dag/std/compiler_entry.dag and the
emitter; and the driver template is still the adjudicate one (2 x
'adjudicate <host-facts', 0 x universe.tsv).

ONE DELIBERATE DIVERGENCE FROM THE STANDING MIRROR RULE, flagged rather than
silent. The rule has been to take main's side on the stage0 mirrors, which was
right while main carried the newer ones. Here the opposite holds: 86761d1's
regen put THIS branch's route contract into them, and main has no such commit. So
the merge keeps the regenerated mirrors -- universe.tsv 0, adjudicate 1,
INGEST-CLOSURE 1, row_serialization 1. They are now stale against #11004's
additions and will drift for the next regen either way; keeping them means
anything that runs before that regen still gets the right driver rather than the
pre-route one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013aZDLk2CxsCDznqn49Xhe8
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