Skip to content

v2 infer gather: thread one facts Map through fold_node; delete the entries scan and concat - #12640

Closed
gunbai-bot[bot] wants to merge 83 commits into
mainfrom
session/neat-eagle-675
Closed

gunbai-bot[bot] wants to merge 83 commits into
mainfrom
session/neat-eagle-675

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

ON HOLD (draft), by manager ruling. On 11 of 98 specimens this change alters the cause of a refusal, from #12582's list-gather reason to infer_facts_key_conflict. Unblock trigger: occurrence projection (#12604 plus its PR2) removes the synthetic node-key collisions, after which the full 98 are rerun with the differential below. Stacked on #12582: this PR merges #12582's branch and enters facts through its refusing insert.

What changed

v2.compiler.infer's gather no longer carries an entries list. It is still one v2.std.node fold_node, but the carrier is a function over a single threaded InferGatherState, which holds the facts Map<Node, InferredFacts>, the pending diagnostics and a failed flag. Each node's run runs its children in edge order and then enters its own facts. What a node awaits is still decided at fold time.

  • Deleted: lookup_inferred_facts_in_entries (the per-lookup list scan) and concat_inferred_facts_entries (the per-parent join). Every row reads the map through infer_known_facts, which is declared at InferredFacts. The seven "derive, admit, or fail" row bodies collapse into infer_gather_state_admit.
  • Entering a fact can refuse: it goes through v2 infer: a key carrying two different facts refuses; equal facts merge #12582's inferred_facts_map_enter. A key already carrying a different fact fails the run with infer_facts_key_conflict; it no longer lets the first fact answer.
  • Metadata and payload edges: metadata edges (an Arrow's declared order, a declaration's binders) no longer run their subtree. Payload edges (empty domain, nullary alternative, and v2 infer: a key carrying two different facts refuses; equal facts merge #12582's literal payload families) run their child against an empty state only to keep its refusal, then enter the payload's own facts.
  • New claim: v2.test.execution.infer_gather_cost infer_gather_deep_product_spine_derives_its_root_holds runs a supplied 8-level product spine through the threaded gather to its root. It is an inhabitance claim, not a cost gate. No depth that fits the new-witness budget (v2.workflow.required_floor required_floor_new_witness_eval_step_budget) separates old from new, so cost evidence is the floor reading below. Its pairing claim, which runs the real front end, is v2.test.execution.infer_product_introduction product_introduction_derives_fully_evidenced_products_holds.

Cost (what this PR claims, and what it does not)

Instrument: the required floor's per-claim eval-step reading, from the [over-cost] / COMPLETED-OVER-COST-REQUIREMENT lines and the required_floor_claim_cost.tsv eval_steps column. It measures the spine claim at depths 24, 48 and 96 on two throwaway branches:

Readings from those runs (eval steps):

depth old (run 36621935843) new (run 36621886463) old − new
24 111,530 80,280 31,250
48 328,106 200,460 127,646
96 not measured (wall deadline, ≥ 8,104 ms CPU) 604,981 —
  • Claimed: the removed term, old − new, grows 4.1× per doubling of depth. That is the list join and scan, and it is gone.
  • Not claimed: that the gather is now linear. The new gather still grows 2.5× (24 → 48) and 3.0× (48 → 96) per doubling. That residue is measured, and it is out of scope here. Hypothesis, not verified: Node-keyed map insert and lookup hash and compare whole subtrees, and each node pays per-node descent-witness work over its subtree. Next trigger: the occurrence-keyed FactSubject change, which keys facts by occurrence identity rather than by structural Node (blocked on the lowering occurrence fix, MQ PR1 (model): a lowering closes its image; derived nodes mint OccurrenceProjected #12604).

Differential (identity grain, old list gather vs threaded gather)

Recipe: DNM #12648, branch dnm/neat-eagle-675-differential. It carries the old gather as the probe module v2.test.claim.dnm.infer_old_gather: #12582's own v2.compiler.infer, verbatim except infer renamed to infer_old, so the two sides differ only in the gather. v2.test.claim.dnm.infer_gather_differential assembles each specimen with tpb_assemble, runs both infers, and compares four fields:

  • verdict: accepted or refused.
  • diagnostics: the whole chain. For refusals there is one stated exception: two refusals also agree when both fatals are infer_facts_key_conflict with the same key and locus. The threaded gather stops at the first conflicting insert and the list gather at map build, so the chain before the fatal may be shorter.
  • facts: facts.lookup(n) for every node_subtree_nodes node.
  • root facts: the same lookup at the root.

The universe is all 98 distinct "module p\n..." sources in src/v2/test.

round head / run result
1 old = main 476baf1 (no #12582), run 36621886463 91/98 agree; 7 differ (043, 046, 051, 070, 079, 082, 084). Structural Node keys collide at different positions, and a run-wide lookup answers from the other position.
2 stacked on #12582, run 36642897401 the 7 agree on verdict and facts. 043/079/082/084 refuse loudly with a key conflict. 046/051/070 differ only in diagnostics.
3 run 36647127349 046/051/070 differ only in the infer_grounding_not_derived count. Cause: the product-row guard read the run-wide map. Fixed in 3cd4135 by deciding the row by kind.
4 6d7d577 / 36650929413, then split 36654098309 046/051/070/084 now agree. 11 differ (041, 042, 043, 057, 060, 062, 074, 077, 078, 079, 082). Both sides refuse, facts and root agree, and the new fatal is infer_facts_key_conflict. 0 of 98 acceptances became refusals.
5 b8d7518 / 36657350187 for all 11, the old fatal is not a key conflict, so the refusal cause changed. Hold.

The #12640 floor on 3cd4135 is green: no enrolled claim regresses and none newly refuses with a key conflict.

Why the gather cannot close this by itself. Every variant that threads one run-wide map over a structural Node key meets the synthetic-occurrence collisions somewhere. The pre-3cd4135 guard matched these 11 but silently dropped advisories on 046/051/070; the kind-decided row keeps the advisories but reaches a key conflict first. Subtree-scoped lookups were rejected because they hide the node-key defect. Occurrence keys are the fix.

🤖 Generated with Claude Code

gunbc-ci-auto-heal and others added 30 commits September 27, 2026 15:28
…_atom_identities) instead of re-parsing each
…uments (the witness is about carets, not the lambda frontier)
…l spelling instead of being captured

Body lowering rewrote every type atom spelled Int or Bool to the kernel binding before any scope
existed. A module that declared its own `type Int = | Mine` and wrote `let y: Int = 1` therefore
had its annotation replaced by the kernel Int, and infer judged the let matched.

The spelling table moves to its language authority (v2.extdeps.languages.dag
dag_kernel_type_binding_optional), and resolve_atom consults it only after the scope walk and the
symbol index. A hit declared in the referencing module binds that declaration. Imported, foreign,
ambiguous and unbound kernel spellings keep the kernel binding, unchanged.

Claims (v2.test.claim.body_let_annotation, 5c): the module-declared Int and Bool lets refuse at the
annotation, and the annotation is asserted not to be the kernel binding. An undeclared Int/Bool
still binds the kernel type. Both shadow rows are red on main 9ce0394 and green here.
The rfm row records the residual and its trigger.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…l Int

Resolve now reads a kernel spelling by the declaration a door selected.
v2.extdeps.languages.dag dag_kernel_type_declaration_binding_optional lists the declarations a
kernel spelling denotes. A hit on one of them takes the canonical binding, and the module's own
declaration shadows it. Any other declaration, reached through an import or another module,
refuses with resolve_reason_kernel_type_spelling_names_a_foreign_declaration. Unbound and
ambiguous names keep the spelling fallback.

Third RED: bla_imported_user_int_refuses_rather_than_binding_the_kernel_int (a two-module
fixture, p imports q's `type Int = | Mine`). All three REDs are F on main and T here, and the
controls are T on both. The new specimens are enrolled in floor_pure_producer_share. The rfm row now
states rung = refused, with the trigger at capability grain: declaration-keyed binding across every
door.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…te is a kernel declaration

An ambiguous kernel spelling now binds the kernel type only when every candidate is a kernel
declaration of it: v2.std.integer Int beside std.integer Int is one kernel type. Otherwise it
refuses with the ordinary resolve_reason_ambiguous_symbol instead of defaulting to the kernel. An
unbound name keeps the kernel binding, which is the correct answer when no declaration is in scope.

New claims:
- RED bla_ambiguous_imported_int_refuses_rather_than_defaulting_to_the_kernel (p imports Int from
  q and r). F on main, T here.
- Control bla_ambiguous_kernel_declarations_bind_the_kernel_type. T on both.

The multi-module specimens now share one helper, bla_assemble_with_peers. The rfm residual is now
only the kernel-declaration path list. Its trigger is a mark on the kernel declarations themselves.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-ibex-696/symbol-arm

# Conflicts:
#	src/v2/test/claim/body_let_annotation_test.dag
#	src/v2/workflow/floor_pure_producer_share.dag
…std.node Symbol)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hored return-type spelling

Three floor blockers on fa8c596. These claims read the positional return clause from body lowering
and expected the kernel binding (dag_binding_type_int, bool_node_symbol). Lowering used to produce
that binding by rewriting the spelling. That rewrite now happens in v2.compiler.resolve, after the
scope walk, so lowering carries `Int` and `Bool` as written, as the generic row already reads `T`.
Arm 1 still discriminates: the return type is Bool, not the parameter's Int.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
review 72280: the symbol arm tested dag_node_is_symbol_literal_atom and then recomputed
dag_symbol_literal_name_optional. That was the same optional twice, and the recomputation needed an
arm the predicate had already ruled out. The decision now matches the optional once. The remaining
Absent arm is a name payload with no atom identity, which is reachable.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…teral_name_optional directly (review 72280 on #12549)
…-ibex-696/symbol-arm

# Conflicts:
#	src/v2/compiler/03_resolve.dag
…xeme-stamped terminal; delete the span/source-text route and its prose note row (review 72294 on #12549)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ens span route: nothing else consumed them

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…pe as this PR derives it

review 72301: the receipt still said the symbol literal's type stays on the GroundingNotDerived
frontier. The arm in this PR makes that false. It now names the derivation route
(DagCanonicalSymbolLiteral, infer_literal_type_binding, v2.std.node symbol_type_node) and the two
body_let_annotation claims that execute it. Census trigger (a) stays open and the receipt says why:
it names the source checker v1.compiler.types, which still types LitSymbol as string_type.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…anned per lookup

facts_map_from_entries admitted entries into a List and answered every
lookup with lookup_inferred_facts_in_entries, a linear scan: each consumer
walking the tree paid quadratic. It now enters admitted entries into a
Map<Node, InferredFacts> once and answers lookup with map_lookup. A
repeated node keeps its FIRST admitted entry, as the scan did.

The gather fold's own scans (lookup_inferred_facts_in_entries inside
infer_gather_fold_algebra) remain: the catamorphism computes children
independently, so removing them needs a state-threading node fold, filed
as node adhoc-914690c3-870.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ions; ownership reads declared_in

GitHub review 5342739525 on #12540. The same-module test compared a declaration's path to
ctx.namespace.module_qn, but build_program_namespace (the plain normalize -> resolve route)
leaves module_qn Empty and records its owner in declared_in. namespace_owns_declaration now reads
declared_in, which is the owner on both namespace routes and the field root_binding_origin reads.

Measuring that route found the earlier boundary. build_program_namespace harvested only the root's
named edges, and a normalized module keeps its declarations under captured -> <module path>, so
none of them were bound. `type Myint` refused as unbound, and a module's own `type Int` fell
through to the kernel spelling and was silently bound to the kernel Int. The namespace now also
harvests the module body, which it finds by declared_in.

Controls on that route (v2.test.claim.body_let_annotation, enrolled share points):
- bla_single_tree_module_declared_int_and_bool_bind_the_local_declaration: F before, T now.
- bla_single_tree_undeclared_int_binds_the_kernel_type: T on both.
The foreign-import refusal and both ambiguity dispositions are unchanged and still hold.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	src/v2/workflow/floor_pure_producer_share.dag
…y walking names

Review 5343957887 (P2 on b55b8a7). namespace_tree_module_body walked the module path's names and
restarted at the root on a missing step. Two leaks followed:
- `module m.t` with a root-level record `t` missed `m`, restarted, found `t`, and bound the record's
  field as a module binding.
- `module t` with a record `t` selected it at once.

The body is now selected by the producer's own mark. v2.compiler.namespace_graft
namespace_graft_module_body_optional descends the containment spine
(namespace_graft_spine_segment_edge_optional) until a step is not a segment, never restarts, and
answers only when that stop is the marked body (namespace_graft_node_is_module_body).
Header and flat representations have no grafted body, so they keep root-only harvesting.

The admission reader in v2.compiler.name_resolve already descended the same spine with its own
copy (admit_named_exports_body_root and _descend_spine). It now calls the one function in
namespace_graft (namespace_graft_module_body_root), so the spine has one reader.

Controls (v2.test.claim.resolve.single_tree_module_body): supplied emit-shaped roots, because
normalize always emits a well-formed graft and source text cannot author these shapes.
- a_record_matching_the_path_suffix_is_not_a_module_body_holds: F on b55b8a7, T here.
- a_record_named_like_a_flat_module_is_not_its_body_holds: F on b55b8a7, T here.
Each asserts `leaked` is not bound and `t` still is.

The local Int/Bool and undeclared-kernel controls still hold.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ation

The parse phase refused a // annotation inside the fn body (DESIGN section 4c: only module-item
grain is modeled). This is the same text, placed above the declaration.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
gunbc-ci-auto-heal and others added 7 commits September 29, 2026 20:01
…-ibex-696/symbol-arm

# Conflicts:
#	src/v2/test/claim/body_let_annotation_test.dag
#	src/v2/workflow/floor_pure_producer_share.dag
…ection ahead of #12382's renamed named-argument header

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…g insert; literal payloads by family

Merge session/eager-newt-412 (#12582). A key already carrying a different
fact now fails the gather with infer_facts_key_conflict instead of the
first entry answering; the literal payload edge dispatches on #12582's
InferLiteralPayload family.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
gunbai-bot Bot pushed a commit that referenced this pull request Sep 29, 2026
…d on #12582 (b52ccb8), with key-conflict classification

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
gunbc-ci-auto-heal and others added 12 commits September 29, 2026 22:14
…mbol_value_lowering (the refusal suite stays deleted)
…aret_symbol_value_refusal (carried via #12549), add main's bcn_let_in_value_cast_verdict share row

Main's #12615 let-in caret case lived in the retired refusal test; moving it into the lowering test is
#12420's (flagged to its owner).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ipt; caret cause row stays retired; let-in comment matches the lowering)
… HANDED receipt and #12549's LOWERED rung row (derived as v2.std.node Symbol)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…y a run-wide lookup

Asked of the threaded map, 'does this node already have an entry' saw a
structurally equal node entered elsewhere and skipped the row, dropping that
occurrence's frontier diagnostic (DNM #12648 run 36647127349: 046/051/070
differ only in their infer_grounding_not_derived count). The list gather's
subtree-scoped check never found the node itself, so the row always ran.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
gunbai-bot Bot pushed a commit that referenced this pull request Sep 30, 2026
…r-node facts, root) on #12640 with the kind-decided product row

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@gunbai-bot
gunbai-bot Bot marked this pull request as draft September 30, 2026 01:12
gunbc-ci-auto-heal and others added 2 commits September 30, 2026 01:14
…first-entry-wins (review 72985)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@briansrls
briansrls marked this pull request as ready for review September 30, 2026 02:40
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-30T02:45:36.078526Z ca5c4d8 Draft marked ready
ℹ️ About Codex in GitHub

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

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

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ca5c4d8a4b

ℹ️ About Codex in GitHub

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

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

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

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

Comment on lines +2368 to +2369
match inferred_facts_map_enter(facts: state.facts, entry: entry) {
Rejected { diagnostics: r } => infer_gather_state_failed(state: state, rejected: r)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid cross-occurrence fact-key collisions

When distinct source occurrences lower to structurally equal synthetic Node keys but derive different facts, the run-wide map treats them as the same subject and this insertion fails immediately with infer_facts_key_conflict. This preempts the actual semantic diagnostic for existing refinement and coproduct inputs; the differential probe records the changed fatal reason for 11 of 98 specimens. Keep facts occurrence-scoped, or defer the threaded global map until keys include occurrence identity.

Useful? React with 👍 / 👎.

@@ -0,0 +1,3591 @@
module v2.compiler.infer

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove the checked-in differential baseline

This adds a 3,591-line frozen copy of the inference implementation solely for the new probe_tmp shell scripts; repo-wide search finds no production or test-discovery consumer. Every subsequent inference change can leave this second implementation stale, while the scripts also copy the repository into /tmp and replace the live module manually, so these throwaway comparison artifacts should remain outside the committed source tree.

Useful? React with 👍 / 👎.

@gunbai-bot

gunbai-bot Bot commented Oct 1, 2026

Copy link
Copy Markdown
Contributor Author

Closing as superseded by eager-newt-412's site-keyed facts trie, per neat-boar-16's ruling: this PR's hold existed so the facts accumulator wasn't rewritten twice while its key was undecided, and the trie settles that key (ruling B: a site key, injective by construction because the structure mirrors the tree). A Map keyed by structural Node, as here, would still collide on equal synthetic nodes. The trie PR carries this PR's goal as a condition: ONE accumulator threaded through fold_node, with no per-node copies or rebuilds, the entries scan and concat deleted, and the cost shape stated and measured at the native seven's subject. Reusable parts may be taken from this branch, credited.

— sent from quiet-gull-780

@gunbai-bot gunbai-bot Bot closed this Oct 1, 2026
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.

0 participants