From 07ff12df7b878041f65e62301d350affa4c1e471 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sat, 22 Aug 2026 01:33:56 +0000 Subject: [PATCH 1/2] Finish the accumulator sweep: merge and checkout, the sites I declared and left When the quadratic accumulator shape was fixed in image.dag and object_store.dag, I recorded that checkout.dag carried one more and merge.dag nine, and left them. That was the wrong disposition: DESIGN section 6 says a proven cost-shape defect is always fixed regardless of the realized n, because "n is small here" is not a fact that stays true under reuse. Declaring the debt is not the same as owing less of it. Three different shapes were hiding under one grep: A PROJECTION IS A MAP. authored_role_names folded with concat-at-end to answer a question that is one field read per element; replace_role and record_alternative rebuilt a list to substitute one entry. As maps they are linear AND the intent is visible in the shape -- every element kept, one substituted -- rather than reconstructed by reading an accumulator. A FILTER IS A PREPEND. target_roles and effective_dependencies drop elements, so they stay folds; they now prepend and reverse once at the consumer. A FLATTEN IS STILL A COPY PER STEP. all_proposal_dependencies appends a LIST per proposal, and my first pass at this diff left it alone with a comment claiming that made it linear. That is false -- concat(acc, xs) copies acc whatever xs is -- and the wrong reason was more dangerous than the defect, because a justified exception stops being re-examined. It now prepends each proposal's dependencies reversed and reverses once, and the comment records the error rather than the conclusion. THE REVERSES ARE LOAD-BEARING WHERE ORDER IS THE PROGRAM. checkout_record hands its edges to node_synthetic in order, and under a positional discipline the order IS the meaning, so a dropped or doubled reverse would rebuild f(x, y) as f(y, x) silently. That is not caught by inspection; it is caught by the existing claims. EVIDENCE, executed: all 12 merge claims, all 29 object-store claims and all 16 image claims green. The order-sensitive ones are the point -- arm_a_is_order_independent, arm_b_is_order_independent, every_distinct_alternative_is_accumulated_regardless_of_order, an_independent_sibling_is_preserved_exactly, and round_trip_reconstructs_the_exact_program are what a mis-applied reverse breaks. No behavior is intended to change and none did. --- dag/gunbc/scm/checkout.dag | 13 +++++- dag/gunbc/scm/merge.dag | 86 ++++++++++++++++++++++++-------------- 2 files changed, 65 insertions(+), 34 deletions(-) diff --git a/dag/gunbc/scm/checkout.dag b/dag/gunbc/scm/checkout.dag index 7f86369985e..30b59d33476 100644 --- a/dag/gunbc/scm/checkout.dag +++ b/dag/gunbc/scm/checkout.dag @@ -35,6 +35,14 @@ type CheckoutOutcome // the same name, substitute a plausible one, or fall back to whatever the store currently holds -- // each of those is the fabricated-plausible-output arm DESIGN section 5 forbids, and each would make // the program's meaning depend on the store's current population rather than on the commit. +// EDGES ACCUMULATE BY PREPEND AND ARE REVERSED ONCE AT THE CONSUMER. concat-at-end inside a fold +// copies the whole accumulator per child, so rebuilding one node cost O(children^2) for work that is +// O(children) -- the cost shape DESIGN section 6 says is always fixed regardless of the realized n, +// because "n is small here" is not a fact that stays true under reuse. +// +// The reverse is load-bearing, not cosmetic: these edges are handed to node_synthetic in order, and +// under a positional discipline the order IS the program. A dropped or doubled reverse would rebuild +// `f(x, y)` as `f(y, x)` silently. type CheckoutEdgeAcc { edges: List refusal: CheckoutRefusal? @@ -49,7 +57,7 @@ fn checkout_edge_step(store: ObjectStore, acc: CheckoutEdgeAcc, stored: StoredEd CheckoutEdgeAcc { edges: acc.edges, refusal: Present { value: cause } } CheckedOut { program: child } => CheckoutEdgeAcc { - edges: concat(acc.edges, [Edge { label: stored.label, target: child }]), + edges: concat([Edge { label: stored.label, target: child }], acc.edges), refusal: acc.refusal, } } @@ -65,7 +73,8 @@ fn checkout_record(store: ObjectStore, record: ObjectRecord) -> CheckoutOutcome CheckoutEdgeAcc { edges: edges, refusal: refusal } => match refusal { Present { value: cause } => CheckoutRefused { cause: cause } - Absent => CheckedOut { program: node_synthetic(kind: record.kind, children: edges) } + Absent => + CheckedOut { program: node_synthetic(kind: record.kind, children: reverse(edges)) } } } } diff --git a/dag/gunbc/scm/merge.dag b/dag/gunbc/scm/merge.dag index df77e42abba..7f5d61542e7 100644 --- a/dag/gunbc/scm/merge.dag +++ b/dag/gunbc/scm/merge.dag @@ -300,16 +300,16 @@ fn target_roles(store: ObjectStore, target: Commit) -> TargetReadOutcome { Absent => TargetUnreadable { cause: TargetRootMissing { identity: target.root } } Present { value: record } => TargetRead { - roles: fold(record.children, init: [], f: fn(acc, stored) { + roles: reverse(fold(record.children, init: [], f: fn(acc, stored) { match edge_label_role(label: stored.label) { Absent => acc Present { value: role } => - concat(acc, [ResolvedRole { + concat([ResolvedRole { role: role, value: PreservedFromTarget { object: stored.target }, - }]) + }], acc) } - }), + })), } } } @@ -339,14 +339,16 @@ fn role_is_present(roles: List, role: Symbol) -> Bool { } } +// A REPLACEMENT IS A MAP, NOT A FOLD THAT REBUILDS. Written as a fold with concat-at-end this copied +// the accumulator once per role, making a single replacement O(roles^2); as a map it is O(roles) and +// the intent -- every role kept, one substituted -- is visible in the shape rather than reconstructed +// by reading the accumulator. +fn replaced_role_or_original(r: ResolvedRole, incoming: ResolvedRole) -> ResolvedRole { + if (r.role as String) == (incoming.role as String) { incoming } else { r } +} + fn replace_role(roles: List, incoming: ResolvedRole) -> List { - fold(roles, init: [], f: fn(acc, r) { - if (r.role as String) == (incoming.role as String) { - concat(acc, [incoming]) - } else { - concat(acc, [r]) - } - }) + map(roles, r => replaced_role_or_original(r: r, incoming: incoming)) } fn upsert_role(roles: List, incoming: ResolvedRole) -> List { @@ -395,26 +397,30 @@ fn alternatives_contain(alternatives: List, desired: DesiredRo // for one ordering and (F3,F2) for another. Both are true statements and neither is the fact, and // order deciding which one survives is precisely what this lane forbids. Accumulating every // distinct alternative makes the reported set complete under any permutation. +fn authored_with_alternative( + a: AuthoredRoleState, + role: Symbol, + desired: DesiredRoleValue, +) -> AuthoredRoleState { + if (a.role as String) != (role as String) { + a + } else if alternatives_contain(alternatives: a.alternatives, desired: desired) { + a + } else { + AuthoredRoleState { + role: a.role, + alternatives: concat(a.alternatives, [desired]), + } + } +} + fn record_alternative( authored: List, role: Symbol, desired: DesiredRoleValue, ) -> List { if already_authored(authored: authored, role: role) { - fold(authored, init: [], f: fn(acc, a) { - if (a.role as String) == (role as String) { - if alternatives_contain(alternatives: a.alternatives, desired: desired) { - concat(acc, [a]) - } else { - concat(acc, [AuthoredRoleState { - role: a.role, - alternatives: concat(a.alternatives, [desired]), - }]) - } - } else { - concat(acc, [a]) - } - }) + map(authored, a => authored_with_alternative(a: a, role: role, desired: desired)) } else { concat(authored, [AuthoredRoleState { role: role, alternatives: [desired] }]) } @@ -529,13 +535,13 @@ fn effective_dependencies( authored: List, ) -> List { concat( - fold(target_dependencies, init: [], f: fn(acc, dep) { + reverse(fold(target_dependencies, init: [], f: fn(acc, dep) { if role_name_listed(names: authored, role: dep.dependent) { acc } else { - concat(acc, [dep]) + concat([dep], acc) } - }), + })), proposal_dependencies, ) } @@ -612,7 +618,7 @@ fn root_edge_for(store: ObjectStore, acc: MergeRootAcc, role: Symbol, object: Ob } CheckedOut { program: program } => MergeRootAcc { - edges: concat(acc.edges, [Edge { label: Named { name: role }, target: program }]), + edges: concat([Edge { label: Named { name: role }, target: program }], acc.edges), refusal: none, } } @@ -672,12 +678,28 @@ fn merge( } } +// A PROJECTION IS A MAP. As a fold with concat-at-end this was O(authored^2) to answer a question +// that is one field read per element. fn authored_role_names(authored: List) -> List { - fold(authored, init: [], f: fn(acc, a) { concat(acc, [a.role]) }) + map(authored, a => authored_role_name(a: a)) +} + +fn authored_role_name(a: AuthoredRoleState) -> Symbol { + a.role } +// A FLATTEN IS STILL A COPY PER STEP. An earlier revision of this sweep left this fold alone with a +// comment claiming that appending a LIST rather than an element made it linear. That is false -- +// concat(acc, xs) copies acc whatever xs is, so the total stayed quadratic in the number of +// dependencies -- and it is recorded here because the wrong reason was more dangerous than the +// defect: a justified exception stops being re-examined. +// +// Prepending each proposal's dependencies REVERSED, then reversing once at the end, restores the +// original order at a total cost proportional to the output. fn all_proposal_dependencies(proposals: List) -> List { - fold(proposals, init: [], f: fn(acc, p) { concat(acc, p.dependencies) }) + reverse( + fold(proposals, init: [], f: fn(acc, p) { concat(reverse(p.dependencies), acc) }) + ) } fn finish_merge( @@ -703,7 +725,7 @@ fn finish_merge( Absent => match store_node( store: applied.store, - n: root_node_of(edges: built.edges), + n: root_node_of(edges: reverse(built.edges)), ) { LocatorCollision { identity: identity, existing: _, incoming: _ } => MergeRefused { From 2155001cc877596d4047fff1c8e6d2588b1c8aa9 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sat, 22 Aug 2026 01:59:30 +0000 Subject: [PATCH 2/2] An occurrence PAYLOAD was declared where the occurrence CARRIER was required Main's witness floor has been red since #8833 with 16 failures, all reading `non-exhaustive pattern match on: OccurrenceId { value: N }` across body_lowering.statement_let_bind and fold_lowering. The scrutinee in that message is the defect: a total match over `Node.occurrence_id` received a bare `OccurrenceId` record, which is not a member of the coproduct that field holds. `Node.occurrence_id` is a `NodeOccurrenceId` -- SyntheticOccurrence or MintedOccurrence -- and `std.occurrence_identity.OccurrenceId` is the PAYLOAD inside one of those arms. dag_int_literal_node_from_magnitude declared its parameter as the payload and assigned it straight into the field, so the declaration had been wrong since it was written. #8833 did not introduce that; it changed the CALL SITE to agree with the wrong signature, replacing node_occurrence_minted(id: minted.id) with minted.id, which is what turned a latent type defect into a live runtime failure. So the repair is at the declaration, not the call site: both int-literal constructors now take NodeOccurrenceId, and 02_parse re-wraps through node_occurrence_minted like its four sibling sites already do. The now-unused OccurrenceId import is dropped. WHAT THIS EXPOSES AND DOES NOT FIX: nothing refused a payload stored where its coproduct was declared. That is a floor-class hole -- values inhabit declared types -- and it is why the failure surfaced 2,000 lines away as a match with no arm rather than at the assignment. This change restores green; it does not close that hole, and I am not claiming it does. EVIDENCE, executed on the remote runner. Before, with only main's content: FAIL fold_probe_ingest_is_accepted (non-exhaustive pattern match on: OccurrenceId { value: 79 }) ... 12 more, 4 PASS FAIL statement_let_then_reference_lowers_holds ... 4 total After, both entries fully green: 16/16 fold_lowering and 4/4 statement_let_bind. The failing set and the fixed set are the same set. --- src/v2/compiler/02_parse.dag | 2 +- src/v2/extdeps/languages/dag.dag | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/v2/compiler/02_parse.dag b/src/v2/compiler/02_parse.dag index 0711b0c8154..c21d8101f71 100644 --- a/src/v2/compiler/02_parse.dag +++ b/src/v2/compiler/02_parse.dag @@ -2059,7 +2059,7 @@ fn parse_expr_terminal( ) match dag_int_literal_node_from_lexeme( lexeme: tok.lexeme, - occurrence_id: minted.id + occurrence_id: node_occurrence_minted(id: minted.id) ) { Accepted { value: captured, diagnostics: _ } => ParseExprAccepted { diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index 06370cbbc6d..2c52043a340 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -86,6 +86,7 @@ import v2.std.compilers.lexing { lex_rule_set_token_class_member } import v2.std.node { + NodeOccurrenceId, Arrow, Atom, Bind, @@ -104,7 +105,6 @@ import v2.std.node { SyntheticOccurrence, is_empty_conj_root, } -import std.occurrence_identity { OccurrenceId } import v2.std.node_query { find_named_child } import v2.std.compilers.target_model { ProducedDeclUnwired, @@ -4208,7 +4208,12 @@ fn dag_node_is_eval_literal_atom(node: Node) -> Bool { } } -fn dag_int_literal_node_from_magnitude(magnitude: DecimalMagnitude, occurrence_id: OccurrenceId) -> Node { +// THE PARAMETER IS THE NODE-CARRIER TYPE, NOT THE SHARED IDENTITY. `Node.occurrence_id` is a +// `NodeOccurrenceId` -- the coproduct of SyntheticOccurrence and MintedOccurrence -- and a bare +// `std.occurrence_identity.OccurrenceId` is the PAYLOAD INSIDE one of its arms, not a member of it. +// Declaring the parameter as the payload let a raw id be stored where the coproduct was expected, +// and every later total match over that field then failed at runtime on a value it had no arm for. +fn dag_int_literal_node_from_magnitude(magnitude: DecimalMagnitude, occurrence_id: NodeOccurrenceId) -> Node { Node { kind: TypeNode { connective: Atom { identity: ^dag_token_int_literal } }, children: [ @@ -4221,7 +4226,7 @@ fn dag_int_literal_node_from_magnitude(magnitude: DecimalMagnitude, occurrence_i } } -fn dag_int_literal_node_from_lexeme(lexeme: String, occurrence_id: OccurrenceId) -> Outcome { +fn dag_int_literal_node_from_lexeme(lexeme: String, occurrence_id: NodeOccurrenceId) -> Outcome { match integer_string_to_decimal_digits_optional(s: lexeme) { Present { value: digits } => outcome_accepted(