Repository navigation
Ground primitive arity and parameter types on the semantic contract, by projection - #9117
Conversation
…by projection PrimitiveSemanticContract carried one field, canonical_name. It now carries a signature grounding: a KEY into std.algebra's AlgebraFieldTemplate rows, never a copy of them, so "what does map take" keeps one set of bytes. The key is (canonical_name, profile), not canonical_name. Measured over the 87 template rows: 21 of the 59 distinct names carry more than one row and four disagree in what they take -- get takes an index on List and a key on Map, join carries three signatures, member disagrees in ARITY OUTRIGHT (2 on BooleanAlgebra, 1 on PointwisePower). Those are receiver-polymorphic operations, not duplicates, so a name-keyed projection would have had to pick one silently. Arity is derived (parameters |> count) and has no field anywhere in the module; an unresolved signature has NO arity rather than a fabricated zero. Population is derived by mapping all_algebra_field_templates(); zero rows are authored, so this surface has no parallel hand roster to drift from. std.primitives PrimitiveContract is untouched: all five of its fields are cost fields, and roadmap_authority's primitive-identity-join rules the five carriers apart, "never one mega-row". Boundary: source -> .dag acceptance only; emission unmeasured. Nothing consumes these facts yet -- the class stays below the floor at mitigatable. Next-rung trigger is func_sig_from_global_bare answering FuncSigResolved for a primitive from this contract, so the 169 registry-union-algebra names reach the EXISTING direct_call_arg_type_mismatch plan rather than a second wall beside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nd carrier note The arity-2 member row is on boolean_algebra_collection_templates, not boolean_algebra_templates (which carries meet/join/complement/top/bottom). Grounding it on BooleanAlgebraProfile would have resolved Absent, so the witness asserting the arity disagreement would have failed for the wrong reason. Verified by reading which fn encloses each row. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # dag/std/primitive_identity.dag
review 55492 blocked on `algebra_grounded_semantic_contract_count() == 87`: a literal copied from the current tree, which DESIGN section 5's oracle ruling rejects -- adding any legitimate template row reds it with no change-related meaning, and automating the update collapses it to measure() == measure(). Correct finding. Replaced by two anchors that carry information without a tree snapshot: a SELF-DENOMINATING identity (derived count == all_algebra_field_templates count, both sides read from the one authority) and a CONTROLLED FIXTURE (three hand-authored rows with hand-authored expected arities plus an absent name -- input and expectation both authored, neither measured). Merging main proved the point: the authority moved 87 -> 90 rows in the same merge, so the literal would already be red for unrelated reasons. That merge also exposed a defect no review caught. Main relocated map_keys/map_values off PartialFunctionProfile, leaving this module's HAND-AUTHORED groundings pointing at nothing -- resolving Absent, silently. The existing totality fold could not catch it: it walks contracts DERIVED from the template rows, so it only fails if the derivation breaks. The declared groundings are the only ones a human can get wrong. w_every_declared_grounding_still_resolves_or_says_ungrounded is that guard; its red arm is the real upstream move replayed. Eleven witnesses pass; three mutations fail; all restore. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Not pushing a fix, because the failure is not in this diff. Re-measured just now rather than assuming it was the same failure I diagnosed earlier. Current state: one failing check, Why it is not mine, proven at tree grain (necessary because main has no completed run carrying the wall — every run since it landed is still queued, so a run comparison cannot answer this):
The fix exists and is owned elsewhere: #9133, "Retire four stale frozen_path_deferrals rows: the fleet-wide RouteGapFreezeIntersection line-stop" — currently open. Until it merges, main still carries the four rows, so re-running this PR would fail identically. A re-run now is wasted CI, and a commit from me would either duplicate #9133's work or be a no-op push. The refusal offers two dispositions per identity (retire the freeze row, or drop it from the route-gap roster). Choosing between them requires knowing whether the required floor genuinely consumes each witness — lane knowledge I do not have — and the shrink log is a receipt-bearing authority. Guessing four dispositions to unblock my own PR is the wrong reason to touch it. This PR otherwise: approved on the current head When #9133 lands I will merge main and re-run rather than assume, confirming both that the intersection count is actually zero at that head and that the eleven witnesses still route and execute. — sent from sunny-ant-106 |
The mechanism this closes half of
src/v1/04_lookup.dagfunc_sig_from_global_barehas two arms that returnFuncSigUnresolvedbefore doing anything: one whenalgebra_method_template_name(name)is true, one wheninfer_builtin_call_type(name)isPresent. Every symbol in registry-∪-algebra — 169 distinct names — therefore can never yield aResolvedFuncSig. NoResolvedFuncSigmeans no formal/actual application plan, anddirect_call_arg_mismatch_diags/direct_call_shape_diagsin04_infer.dagrun only over that plan.So those 169 are not merely unchecked. They are structurally unreachable by the one wall that would check them. #9024 closed the type-application arm of this same hole from the other side; this is the callable-signature arm, and it is shut by construction.
The reason it is shut is that nothing said what a primitive takes. This PR grounds that fact. It does not open the arm — see Rung below.
What landed
std.primitive_identityPrimitiveSemanticContractcarried exactly one field,canonical_name. It now carries a signature grounding:PrimitiveSignatureGrounding = GroundedInAlgebraTemplate { profile } | SignatureUngrounded { reason }PrimitiveSignatureResolution = SignatureResolved { parameters, result } | SignatureAmbiguousOnProfile | SignatureAbsentFromProfile | SignatureNotGroundedprimitive_signature— total over every contractprimitive_arity— derived,parameters |> countalgebra_grounded_semantic_contracts— the population, derived, zero authored rowsstd.primitivesPrimitiveContractis not touched.Why not
PrimitiveContract, which the work item namedEvery one of its five fields is a cost field, and
gunbc.roadmap_authorityprimitive-identity-joinrules verbatim: "PrimitiveDefinition (identity + PrimitiveSemanticContract), PrimitiveInvocationSurface, PrimitiveRealization per RealizationTarget, PrimitiveCostFact, and PrimitiveTraversalOrderFact — five separate carriers joined by PrimitiveIdentity, never one mega-row." Grounding the type half on the cost row is that mega-row.PrimitiveSemanticContractalready existed, already sat underPrimitiveDefinition, already hadPrimitiveCostFactseparate beside it, and carried one anemic field. This fills a carrier declared for this and left hollow; it does not author a fifth. (Operator ruling, after two lanes reached the same conclusion independently.)Projection, not a copy — and the measurement that decided it
The signature half already exists for part of the population, on
std.algebraAlgebraFieldTemplate.param_types. A secondparam_typeshere would answer "what doesmaptake" from two sets of bytes — the §3 fork this change exists to remove. Two admissible shapes; measured:param_typesoccurrences04_infer.dag, 4 in04_types.dag, 2 in04_lookup.dagAll three migration targets are load-bearing pipeline stages, so it is a stage0 regen on top of a root-cut replacement migration. Projection. The contract carries a key; every parameter fact is read back through
primitive_signature.The finding that shaped the key: name → signature is not a function
A name-keyed projection was considered and refused on measurement. Over the 90
AlgebraFieldTemplaterows (post-merge with main; see Merge below):param_types:join,member,contains,get.memberdisagrees in arity outright —[ReceiverSelf, ReceiverElement]onFinitePowerSet(arity 2),[CallableOf{…}]onPointwisePower(arity 1).gettakesNamedTemplate{Int}on the List profile andReceiverKeyon two Map-shaped profiles.joincarries three distinct signatures across three profiles.These are not duplicates to dedup. They are receiver-polymorphic operations:
geton a List takes an index,geton a Map takes a key, and both are correct. A projection keyed oncanonical_namealone would have had to pick one silently — the last-import-wins class this compiler already carries as a confirmed structural hole. So the key is(canonical_name, profile), which is not new vocabulary:roadmap_authorityalready models it asInvocationForm MethodCall { receiver_domain }, andAlgebraProfileis the existing closed enum that names it.Anyone tempted to "simplify" the key back to a bare name should find this paragraph rather than rediscover it through a wrong answer. Two witnesses go red if they try.
Population: the ruled 59 algebra-known names resolve at their true grain to 90
(name, profile)rows. 87 is what 59 is once a spelling stops standing in for an identity. It is still enumeration by the primary structure with no hand roster — derived by mappingall_algebra_field_templates(), zero rows authored.Witnesses, and the one I was asked for and did not write
The brief asked for a witness joining the contract's parameters to the algebra templates and refusing on disagreement. Under projection there is one set of bytes, so disagreement has no spelling — such a check would be permanently green by construction, which §4b names as worse than absent because it gets cited as coverage. It is not here. (Operator concurred; the two asks contradicted each other.)
What is here has authorable REDs:
w_every_algebra_grounded_contract_resolves_a_signature— totality. RED: a grounding naming a(name, profile)pair with no template.w_derived_population_is_exactly_the_algebra_template_population— the denominator as a self-denominating identity (derived count == all_algebra_field_templates() |> count), both sides read from the one authority. RED: a derivation that drops a profile.w_every_row_of_a_controlled_fixture_resolves_to_its_authored_arity— non-vacuity as a controlled fixture: three hand-authored rows with hand-authored expected arities, plus an absent name. Input and expectation both authored here, neither measured from the tree.w_every_declared_grounding_still_resolves_or_says_ungrounded— the hand-authored groundings must resolve or sayNotGrounded;Absent/Ambiguousmeans someone typed a(name, profile)pair upstream no longer honours.w_get_takes_an_index_on_a_list_and_a_key_on_a_map,w_member_has_a_different_arity_on_each_profile— the polymorphism regression controls.w_a_name_absent_from_its_profile_refuses— the absent arm.w_two_templates_of_one_name_on_one_profile_refuse+w_one_template_of_one_name_resolves— the ambiguity arm and its positive control, so a resolver that refused everything would not pass.w_an_ungrounded_contract_has_no_arity_not_zero+w_a_nullary_primitive_reports_arity_zero— the discriminating pair. A resolver fabricating0for an unresolved signature passes every other claim and fails these two.On the ambiguity arm, examined rather than assumed
I originally justified its RED as "plant a name-only grounding". That justification was wrong, and the operator was right to press it:
GroundedInAlgebraTemplatestructurally requires a profile, so a name-only grounding has no constructor and no fixture can author one. The ambiguous collapse by name-keying is therefore structurally impossible, not detected — a higher rung than a wall.The arm survives because it guards a different class: two rows of one name on one profile. Measured, zero duplicate names within any of the eight profiles today — so it is unoccupied. But the mechanism is live (the template lists are hand-authored, a repeated row is writable) and it is join-reachable (the resolver folds over exactly that list). That is a quiet guard, not a dead one.
algebra_signature_from_candidatestakes its candidates as a parameter precisely so a fixture can plant the duplicate; that factoring is a correctness requirement, not style.Arity is derived and has no field anywhere in the module. An unresolved signature has no arity rather than a guessed one:
Absent, not zero, because a nullary primitive and an unresolved signature are different facts.Rung
Nothing here moves the class off mitigatable. The class — a host primitive call binds arguments in no bijection and at no declared type — sits below the floor, because §4b names exact-bijection binding as ordinary floor, and populating a carrier leaves it there. What this PR establishes is that the semantic contract now carries arity and parameter types for a declared population, derived from one authority; nothing consumes them.
Next-rung trigger:
func_sig_from_global_bareansweringFuncSigResolvedfor a primitive from this contract, so the 169 reach the existingdirect_call_arg_type_mismatchplan rather than a second wall built beside it. That is the next PR and it is the one that actually climbs; it is a load-bearing04_lookup/04_inferchange needing a stage0 regen and is deliberately out of scope here.Boundary: source →
.dagacceptance only. Emission is unmeasured by this change.The witnesses prove the two surfaces are one authority and that the projection is total and unambiguous. They do not prove any call is checked.
Executed
Measured on BuildBuddy (linux/amd64), release
gunbcbuilt from this branch's working tree, 2026-08-24.Compile —
gunbc compile --source-root dag --source-root src/v2, both entries:dag/std/primitive_identity.dagdag/test/claim/primitive_signature_grounding_witness_test.dagEvery diagnostic is the pre-existing
where-refinement unenforced … non-literal value at refined positionclass (std.content_hash,std.decl_ref, and six sites already inprimitive_identity). This change adds one more of that same class, at thet.name as NonEmptyStrin the derived population — the established corpus idiom for that cast.Witnesses —
gunbc run … --claim-run, eleven of eleven:Discriminating REDs, executed:
Also re-run after the merge, because I edited a main-side fixture whose whole purpose is a planted RED — a change that quietly greened it would be worse than a compile error:
One fail-closed receipt from the change itself.
signatureis a required field, so adding it made every one of the 12PrimitiveSemanticContractconstruction sites refuse until each declared a grounding. The deletion-is-the-census property worked in the additive direction: nothing could silently keep a signature-less contract.What was NOT executed: the witness floor (
claim_executor) and emission. Boundary is source →.dagacceptance, as stated above.Denominator honesty
The population is not closed, and no number here should be read as completeness.
data *_contract: PrimitiveContractrows;primitive_contract_rosterhas 64 entries. 64/64 is agreement between a roster and its rows, not a universe.PrimitiveContractRow64,BuiltinRegistry130,AlgebraTemplate59 distinct over 90 rows,InterpreterDispatch164 distinct spellings over 189 arms,EmitHandler57 → 218 distinct authored spellings.length/count/sizeare one interpreter arm with three spellings) and spellings fork (foldvsfold_listare separate surfaces that can disagree).dedup,filter_map,hash,len,max_by,repeat,replace_section,sort,sum,to_bytes,to_json,zip. That is a finding in its own right, not rounding.Two observed pre-existing conditions, neither introduced nor fixed here
1.
builtin_registry_surface_namesis 8 rows behind its primary authority — live, today.std.primitivesbuiltin_registry_surface_names(the hand roster feeding the D0 census) carries 122 spellings;v1.compiler.infer_methodbuiltin_function_registry(the authority it mirrors) carries 130. The set difference runs one way only:class_b_import_closure_gate_not_affected_skip,compile_dag_diagnostic_census,dependency_resolution_facts,observe_declared_import_closure_symbol_binding,parse_roadmap_acceptance_event_history_jsonl,parse_stage0_cargo_manifest_bins,project_roadmap_acceptance_event_history_from_authority_text_host,type_ref_hit_ne_bind_measure_activeSo that surface of
primitive_surface_census_derivedunder-reports by 8 right now.primitive_d0_derived_surface_drift_audit_notepredicts this failure in words — mutation control fires when the hand roster is updated, not whenbuiltin_function_registrygrows alone. What was not known is that the prediction has already come true and is eight deep, and that two of the eight (compile_dag_diagnostic_census,observe_declared_import_closure_symbol_binding) are rows whose own carrier notes in04_method.dagpromise they inherit thePrimitiveDefinitionidentity-join dissolution. Rows authored with an explicit promise to be joined are today invisible to the join. A documented gap that has silently become an occupied one is worse than an undocumented one, because the note reads as coverage.Not fixed here: different subject, different owner (
primitive-identity-join), and folding it in would widen this PR's scope. Recorded so the measurement survives.Note that the surface this PR adds is not of that shape: the population is derived by mapping the primary structure, so there is no second list to drift from.
2.
AlgebraFieldTemplatealready carriescost_shapebesideparam_types, so the five-carrier separation is already breached on that surface. Pre-existing, out of scope, named so the next reader does not attribute it to this change.Merge with main, and the two defects it exposed
Merged
origin/main(conflict indag/std/primitive_identity.dag). Neither side could be taken whole:The textual conflict: main's Relocate primitive projection behind the stage0 seed boundary #9060 relocated
type PrimitiveIdentityout of this file intostd.primitive_projection. My side still declared it. Keeping my side whole would have created two declarers; keeping main's whole would have dropped my carrier work. Resolution keeps my new types and lets main's deletion stand;PrimitiveIdentitynow arrives through main's import and still resolves at its use sites.A 13th construction site, in a file that did not conflict. Main's Determinism classifies by primitive identity and traversal order, not by a hand roster of symbols #9092 added
PrimitiveSemanticContractatsrc/v2/lens/determinism/reach_witness_test.dag:230. My "all 12 sites refuse until each declares a grounding" census was correct at my base and stale the moment that landed — the merge-interval class, where two individually-correct PRs produce a broken tree. Git flagged only the textual overlap; this one would have surfaced as a floor red in a file neither side touched. It is a deliberately planted primitive that exists to leave the traversal denominator open, so it takesSignatureUngroundedwith a typed reason rather than a fabricated signature.A stale hand-authored grounding, which nothing would have caught. Main also renamed
BooleanAlgebraCollectionProfile→FinitePowerSetProfile, addedFinitelySupportedFunctionProfile(8 profiles → 9, 87 rows → 90), and relocatedmap_keys/map_valuesoffPartialFunctionProfile. My D0 groundings for those two still pointed at the old profile and would have resolvedAbsent— silently, because the totality fold walks contracts derived from the template rows and so is nearly tautological. The hand-authored groundings are the only ones a human can get wrong, and nothing asserted them.w_every_declared_grounding_still_resolves_or_says_ungroundedis that guard, and RED 1 above is the actual upstream move replayed.This is also the concrete receipt for the
== 87finding (review 55492, correct and acted on): the authority moved to 90 rows in this same merge, so the snapshot literal would have gone red for reasons having nothing to do with this change — exactly the change-detector failure §5's oracle ruling names. It is replaced by a self-denominating identity plus a controlled fixture, neither of which reads a count out of the current tree.Provenance
Inventory jointly measured with
stern-lynx-526, who independently produced the 57-rowEmitHandlerfigure that corrected mine (my grep had matched nestedname:inside rows rather than rows; the union was unaffected because all nine spurious names were already contributed by another surface). Carrier and population ruled byswift-badger-524.