From c0d25ace3e5acf1388c9bce5a30d964ce85dfa48 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 29 Sep 2026 05:35:16 +0000 Subject: [PATCH 1/4] XL-2: a wildcard pattern lowers to its own form, not an atom spelled `_` Lowering mints v2.std.node_query wildcard_pattern_identity (``, unauthorable) for an authored `_` pattern at its occurrence; is_wildcard_pattern is the one recogniser, consulted by resolve, reference_conservation and target_model emission. Retires the open trigger of RFM a_wildcard_match_arm_resolves_as_an_unbound_name. Co-Authored-By: Claude Opus 5.5 (1M context) --- ..._match_arm_resolves_as_an_unbound_name.dag | 11 +- src/v2/compiler/03_resolve.dag | 22 +-- src/v2/compiler/body_lowering_fold.dag | 22 ++- src/v2/compiler/reference_conservation.dag | 8 +- src/v2/std/compilers/target_model.dag | 7 +- src/v2/std/node_query.dag | 24 ++- .../wildcard_pattern_form_test.dag | 154 ++++++++++++++++++ 7 files changed, 227 insertions(+), 21 deletions(-) create mode 100644 src/v2/test/claim/body_lowering/wildcard_pattern_form_test.dag diff --git a/dag/gunbc/recurring_failure_mode/a_wildcard_match_arm_resolves_as_an_unbound_name.dag b/dag/gunbc/recurring_failure_mode/a_wildcard_match_arm_resolves_as_an_unbound_name.dag index 27922d5f3b1..2960668dbd0 100644 --- a/dag/gunbc/recurring_failure_mode/a_wildcard_match_arm_resolves_as_an_unbound_name.dag +++ b/dag/gunbc/recurring_failure_mode/a_wildcard_match_arm_resolves_as_an_unbound_name.dag @@ -1,6 +1,7 @@ module gunbc.recurring_failure_mode.a_wildcard_match_arm_resolves_as_an_unbound_name import std.types { NonEmptyStr } +import std.decl_ref { DeclarationRef, WholeDeclaration } import gunbc.recurring_failure_mode { RecurringFailureMode } data a_wildcard_match_arm_resolves_as_an_unbound_name: RecurringFailureMode = RecurringFailureMode { @@ -12,7 +13,15 @@ data a_wildcard_match_arm_resolves_as_an_unbound_name: RecurringFailureMode = Re "HOW IT SURFACED: the XL-2 if-arm repair (`v2.compiler.body_lowering_fold` `body_lower_if_arm_lowered`) made a match inside an if-arm reach resolve for the first time. On main such a match was dropped whole by the operand reader, so `if c { s } else { match s { BlgA => .. _ => .. } }` resolved; after the repair it refuses at `_`, the verdict the same match already had at the top level of a fn body. The repair did not create the refusal; it stopped hiding it.", "CLASS: a resolve defect, not a lowering one: a wildcard pattern binds nothing and names nothing, and a resolver that looks it up as a symbol has conflated a pattern form with a reference. RUNG FOUND AT: a located refusal (mitigation) -- the wrong answer is loud, never silent, but a correct program is refused. CEILING: structurally guaranteed -- the wildcard is a distinct pattern constructor in the lowered tree, so the resolver can decline to look it up by construction. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: the lowered pattern carries the wildcard as its own form that resolve does not treat as a reference, with a witness whose red is this specimen on main and whose control is a match exhaustive through named arms only.", "REPAIRED AT THE RESOLVE LINK, TRIGGER NOT MET (2026-09-25, on 69e0bb7566e): the earliest wrong link was `v2.compiler.resolve` `resolve_pattern_node_walk`, which recognized `_` only as a constructor FIELD's target and sent a bare arm pattern to `resolve_node_walk` as a reference; it now recognizes `_` at every pattern depth. Witness `v2.test.claim.namespace_xl0.wildcard_arm_resolve`: `a_match_exhaustive_through_a_wildcard_arm_resolves` and `an_undeclared_name_in_a_wildcard_arm_body_is_the_sole_refusal_at_its_atom` false on main, true on head; the field-wildcard and named-binder controls true on both. MEASURED CENSUS BEHAVIOUR: on main the `_` refusal refused the whole module as its own chain ahead of any body refusal (`first=_ +wc_undeclared_body`), with observation COMPLETE -- it never marked the match or module incomplete, it added a spurious chain; on head the body name is the sole chain, observation complete. THE ROW STAYS OPEN because its trigger names a capability not delivered: the lowered pattern still carries the wildcard as an ATOM SPELLED `_`, recognized by `pattern_wildcard_name` at resolve, not as its own pattern form; that construction lives in `v2.compiler.body_lowering_fold`, which this repair did not touch.", + "TRIGGER MET, RUNG NOW STRUCTURALLY GUARANTEED (XL-2, sharp-carp-336): lowering mints the wildcard as its own pattern form. `v2.compiler.body_lowering_fold` `body_lower_pattern_atom_form` lowers an authored `_` pattern -- a bare arm or a constructor field's target -- to `v2.std.node_query` `wildcard_pattern_identity`, an atom whose identity `` no authored name can spell (`<` is not an identifier character; the `v2.std.anonymous_binder` precedent), at the `_`'s own occurrence. `is_wildcard_pattern` is the one recogniser: `v2.compiler.resolve` keeps the form at every pattern depth and never binds it, `v2.compiler.reference_conservation` `occurrence_spelling_conserved` credits the authored `_` to it, and `v2.std.compilers.target_model` `bound_spelling_from_map` spells it `_`. `pattern_wildcard_name` is now the AUTHORED spelling only: a `_` written as a body reference still refuses as a referenced anonymous parameter. WITNESS `v2.test.claim.body_lowering.wildcard_pattern_form`: the bare-arm and field-wildcard claims are false with main's lowering and true on head, and the nullary-constructor and binder controls are true on both. MUTATION: head's lowering with main's resolve reds `wildcard_arm_resolve` `a_match_exhaustive_through_a_wildcard_arm_resolves`, because main's resolve looked the form up as a name. The field-wildcard resolve claim stays green under that mutation, because main's resolve bound an unrecognised field atom as a binder, so the lowering witness is that case's discriminator.", ], - evidence: [], + evidence: [ + DeclarationRef { module_path: "v2.test.claim.body_lowering.wildcard_pattern_form", decl_name: "a_bare_wildcard_arm_lowers_to_the_wildcard_form", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.test.claim.body_lowering.wildcard_pattern_form", decl_name: "a_wildcard_constructor_field_lowers_to_the_wildcard_form", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.test.claim.body_lowering.wildcard_pattern_form", decl_name: "a_nullary_constructor_arm_stays_its_named_atom", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.test.claim.body_lowering.wildcard_pattern_form", decl_name: "a_binder_arm_stays_its_named_atom", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.test.claim.namespace_xl0.wildcard_arm_resolve", decl_name: "a_match_exhaustive_through_a_wildcard_arm_resolves", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.test.claim.namespace_xl0.wildcard_arm_resolve", decl_name: "an_undeclared_name_in_a_wildcard_arm_body_is_the_sole_refusal_at_its_atom", field: WholeDeclaration }, + ], } diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index 291d30c891b..605c74b5a91 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -122,6 +122,7 @@ import v2.std.node_query { construct_field_edges, construct_tag_optional, find_named_child, + is_wildcard_pattern, pattern_wildcard_name } import std.occurrence_identity { NodeOccurrenceIdentity, OccurrenceId } @@ -1378,7 +1379,7 @@ fn resolve_pattern_binders(ctx: ResolveContext, pat: Node, acc: Map - if id == pattern_wildcard_name() { + if is_wildcard_pattern(pattern: e.target) { m } else if resolve_pattern_atom_names_constructor(ctx: ctx, id: id) { m @@ -1392,7 +1393,7 @@ fn resolve_pattern_binders(ctx: ResolveContext, pat: Node, acc: Map Bool { - match pat.kind { - TypeNode { connective: Atom { identity: id } } => id == pattern_wildcard_name() - _ => false - } -} - -// A pattern is the arm's whole `pat` or a constructor field's target, and `_` is the same pattern -// form in both: it binds nothing and names nothing, so it is kept as it is at every depth rather -// than looked up as a reference to a symbol spelled `_`. +// +// A pattern is the arm's whole `pat` or a constructor field's target, and the wildcard is the same +// pattern FORM in both (v2.std.node_query is_wildcard_pattern): it binds nothing and names nothing, +// so it is kept as it is at every depth. Lowering mints it, so it is recognised by form, never by +// comparing an atom's spelling to `_`. fn resolve_pattern_node_walk(ctx: ResolveContext, pat: Node) -> ResolveNodeWalk { - if resolve_pattern_is_wildcard(pat: pat) { + if is_wildcard_pattern(pattern: pat) { ResolveWalkAccepted { value: pat, diagnostics: None } } else { match construct_tag_optional(n: pat) { diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index dd65c2ac2fb..fcaef56e926 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -138,6 +138,8 @@ import v2.std.node_query { field_projection_node, find_named_child, positional_payload_field_name, + pattern_wildcard_name, + wildcard_pattern_identity, repeated_name_edges, variant_declaration_node } @@ -5591,7 +5593,7 @@ fn body_lower_pattern_lowered(pattern_capture: Node) -> Outcome { Present { value: pair } => if is_empty_conj_root(n: body_lower_deep_unwrap_optional(node: pair.right)) { match body_lower_pattern_atom_optional(pattern_capture: pair.left) { - Present { value: atom } => outcome_accepted(value: atom) + Present { value: atom } => outcome_accepted(value: body_lower_pattern_atom_form(atom: atom)) Absent => body_lower_pattern_reject(pattern_capture: pattern_capture) } } else { @@ -5603,12 +5605,28 @@ fn body_lower_pattern_lowered(pattern_capture: Node) -> Outcome { } Absent => match body_lower_pattern_atom_optional(pattern_capture: captured) { - Present { value: atom } => outcome_accepted(value: atom) + Present { value: atom } => outcome_accepted(value: body_lower_pattern_atom_form(atom: atom)) Absent => body_lower_pattern_reject(pattern_capture: pattern_capture) } } } +// A BARE PATTERN ATOM IS A NAME OR THE WILDCARD, and only the wildcard changes form here: an authored +// `_` lowers to the minted wildcard pattern (v2.std.node_query wildcard_pattern_identity) at the `_`'s +// own occurrence, so no reader downstream sees a name it would look up. A name stays the atom it is, +// a constructor or a binder for the resolver to decide. +fn body_lower_pattern_atom_form(atom: Node) -> Node { + match node_atom_identity_optional(node: atom) { + Present { value: id } => + if id == pattern_wildcard_name() { + body_lower_param_ref_atom(source: atom, identity: wildcard_pattern_identity()) + } else { + atom + } + Absent => atom + } +} + // THE ARM BODY IS A VALUE, READ BY THE VALUE READER AND BY NOTHING ELSE. It was read by an operand // reader (body_lower_match_arm_body_optional, deleted) that answered a nested `match` or `if` in arm // position with that form's first atom -- how every arm of std.content_hash compare_content_hash lost diff --git a/src/v2/compiler/reference_conservation.dag b/src/v2/compiler/reference_conservation.dag index 6ff84b2558b..88299de1712 100644 --- a/src/v2/compiler/reference_conservation.dag +++ b/src/v2/compiler/reference_conservation.dag @@ -65,7 +65,7 @@ import v2.compiler.occurrence_role { occurrence_role_index } import v2.std.anonymous_binder { anonymous_param_label, is_anonymous_param_label } -import v2.std.node_query { pattern_wildcard_name } +import v2.std.node_query { pattern_wildcard_name, wildcard_pattern_identity } // REFERENCE CONSERVATION ACROSS LOWERING (XL-2). // @@ -839,8 +839,12 @@ fn absent_cause(declared: ItemDeclarationStanding) -> DroppedReferenceCause { // `dag_binding_type_int`) at that same occurrence. That is the single authority the spelling pool // below already consults, so it is asked here too: the locus is held and the spelling is the one // the rewrite derives. Any other spelling at the occurrence is still RespelledInNormalizedTree. +// An authored `_` pattern is conserved by the wildcard pattern form lowering mints at its occurrence +// (v2.std.node_query wildcard_pattern_identity); the form is the `_`'s lowering, not a different atom. fn occurrence_spelling_conserved(found: Symbol, identity: Symbol) -> Bool { - found == identity || found == body_lower_resolved_type_binding(sym: identity) + found == identity + || found == body_lower_resolved_type_binding(sym: identity) + || ((identity == pattern_wildcard_name()) && (found == wildcard_pattern_identity())) } // WHERE AN ERASED ATOM IS COUNTED. The module header first (its item is -1), then an atom whose diff --git a/src/v2/std/compilers/target_model.dag b/src/v2/std/compilers/target_model.dag index ecdd86418ba..53f04168b93 100644 --- a/src/v2/std/compilers/target_model.dag +++ b/src/v2/std/compilers/target_model.dag @@ -148,7 +148,8 @@ import v2.std.node_query { node_labeled_child_edges, node_positional_child_targets, nullary_inhabitant_by_discriminant, - pattern_wildcard_name + pattern_wildcard_name, + wildcard_pattern_identity } import v2.std.qualified_name { QualifiedName, @@ -1116,6 +1117,8 @@ fn lex_rules_literal(target: TargetModel, token_class: Symbol) -> Outcome, binding: Symbol @@ -1124,7 +1127,7 @@ fn bound_spelling_from_map( Accepted { value: opt, diagnostics: pending } => match opt { Present { value: spelling } => Accepted { value: spelling, diagnostics: pending } Absent => - if is_anonymous_param_label(sym: binding) { + if is_anonymous_param_label(sym: binding) || (binding == wildcard_pattern_identity()) { symbol_interned_spelling(binding: pattern_wildcard_name()) } else { symbol_interned_spelling(binding: binding) diff --git a/src/v2/std/node_query.dag b/src/v2/std/node_query.dag index a8fb4c395c0..db8d0116099 100644 --- a/src/v2/std/node_query.dag +++ b/src/v2/std/node_query.dag @@ -197,11 +197,33 @@ fn positional_payload_field_name() -> Symbol { symbol_intern_lexeme(lexeme: "0") } -// The wildcard pattern `_` binds nothing and names nothing (v1.compiler.parse parse_pattern). +// THE AUTHORED SPELLING of the wildcard: the `_` a source writes in a pattern or as a parameter. +// It binds nothing and names nothing (v1.compiler.parse parse_pattern). It is the SPELLING only: +// lowering never leaves it in a pattern, it mints the wildcard pattern form below instead. fn pattern_wildcard_name() -> Symbol { symbol_intern_lexeme(lexeme: "_") } +// A WILDCARD PATTERN IS ITS OWN FORM, NOT AN ATOM SPELLED `_`. A pattern atom is otherwise a +// constructor or a binder, both names, so a `_` atom was a name to every reader that did not +// special-case its spelling -- resolve looked it up as a reference +// (gunbc.recurring_failure_mode a_wildcard_match_arm_resolves_as_an_unbound_name). Lowering +// (v2.compiler.body_lowering_fold body_lower_pattern_lowered) mints this identity for an authored `_` +// pattern, and `<` is not an identifier character, so no authored name can spell it: the form names +// nothing BY CONSTRUCTION, the anonymous-slot precedent (v2.std.anonymous_binder +// anonymous_param_label). THE ONE MINT; is_wildcard_pattern is the one recogniser. +fn wildcard_pattern_identity() -> Symbol { + symbol_intern_lexeme(lexeme: "") +} + +fn is_wildcard_pattern(pattern: Node) -> Bool { + match pattern.kind { + TypeNode { connective: Atom { identity: id } } => + (id == wildcard_pattern_identity()) && (count(pattern.children) == 0) + _ => false + } +} + fn declared_field_cardinality_of_target(target: Node) -> Cardinality { match target.kind { diff --git a/src/v2/test/claim/body_lowering/wildcard_pattern_form_test.dag b/src/v2/test/claim/body_lowering/wildcard_pattern_form_test.dag new file mode 100644 index 00000000000..87d9b7cadd8 --- /dev/null +++ b/src/v2/test/claim/body_lowering/wildcard_pattern_form_test.dag @@ -0,0 +1,154 @@ +module v2.test.claim.body_lowering.wildcard_pattern_form + +import v2.compiler.body_lowering_fold { body_lower_match_arms_optional, body_lower_unwrap_captured } +import v2.compiler.parse { parse_module } +import v2.compiler.tokenize { tokenize } +import v2.extdeps.languages.dag { dag_language_model, parse_production_emitted_identity_optional } +import v2.std.algebra { length } +import v2.std.diagnostic { Accepted, Rejected } +import v2.std.grammar { node_atom_identity_optional } +import v2.std.integer { Int } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import v2.std.logic { Bool } +import v2.std.node { Named, Node, Symbol } +import v2.std.node_query { construct_tag_optional, is_wildcard_pattern, pattern_wildcard_name } +import v2.std.optional { Absent, Optional, Present, optional_absent, optional_present } +import v2.std.text { String } +import std.algebra { Cons, Empty, FreeMonoid, list_append } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// A WILDCARD PATTERN LOWERS TO ITS OWN FORM, NOT TO AN ATOM SPELLED `_`. +// +// THE SUBJECT IS ONE PRODUCER BOUNDARY: v2.compiler.body_lowering_fold body_lower_match_arms_optional +// over a match's parse capture, its input supplied by tokenize and parse (no normalize, no resolve). +// The resolve route that consumes the form keeps its own witness, +// v2.test.claim.namespace_xl0.wildcard_arm_resolve, which runs the real front end end to end. +// +// FOUR ARMS, IN AN ORDER THAT PAIRS EACH OBSERVATION TO ONE ARM: a nullary constructor, a constructor +// whose field pattern is `_`, a named binder, and a bare `_`. On main the two `_` positions lowered to +// an atom spelled `_` -- a name to every reader that did not special-case the spelling +// (gunbc.recurring_failure_mode a_wildcard_match_arm_resolves_as_an_unbound_name) -- so the two +// wildcard claims are red there. The constructor and binder controls hold on both sides: the mint +// changes the wildcard and nothing else, so a mint that rewrote every bare atom reds them. + +data wpf_source: String = "module m\n\ntype WpfT = WpfA | WpfB { r: Int } | WpfC | WpfD\n\nfn wpf_probe(t: WpfT, wpf_w: Int) -> Int {\n match t {\n WpfA => wpf_w\n WpfB { r: _ } => wpf_w\n wpf_bound => wpf_w\n _ => wpf_w\n }\n}\n" + +fn wpf_parsed() -> Optional { + let lm = dag_language_model() + match tokenize(text: wpf_source, file: ^wpf_file, rules: lm.lex) { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: ts, diagnostics: _ } => + match parse_module(tokens: ts, grammar: lm.grammar) { + Rejected { diagnostics: _ } => optional_absent() + Accepted { value: artifact, diagnostics: _ } => optional_present(value: artifact.tree) + } + } +} + +fn wpf_emitted(root: Node, production: Symbol) -> FreeMonoid { + let own = match parse_production_emitted_identity_optional(node: root) { + Present { value: e } => if e == production { Cons { head: root, tail: Empty } } else { Empty } + Absent => Empty + } + fold(root.children, init: own, f: fn(acc, e) { list_append(left: acc, right: wpf_emitted(root: e.target, production: production)) }) +} + +fn wpf_named_child(n: Node, name: Symbol) -> Optional { + fold(n.children, init: optional_absent(), f: fn(acc, e) { + match acc { + Present { value: _ } => acc + Absent => + match e.label { + Named { name: l } => if l == name { optional_present(value: e.target) } else { acc } + _ => acc + } + } + }) +} + +// The lowered pattern of every arm of the fixture's match, in source order; Empty when the match +// is not found or the producer does not lower it. +fn wpf_patterns() -> FreeMonoid { + match wpf_parsed() { + Absent => Empty + Present { value: tree } => + match wpf_emitted(root: tree, production: ^dag_surface_match_expr) { + Empty => Empty + Cons { head: shell, tail: _ } => + match body_lower_unwrap_captured(shell: shell) { + Absent => Empty + Present { value: captured } => + match body_lower_match_arms_optional(captured: captured) { + Accepted { value: Present { value: arms }, diagnostics: _ } => + fold(arms, init: Empty, f: fn(acc, arm) { + match wpf_named_child(n: arm, name: ^match_arm_pattern) { + Present { value: p } => list_append(left: acc, right: Cons { head: p, tail: Empty }) + Absent => acc + } + }) + _ => Empty + } + } + } + } +} + +fn wpf_pattern_at(index: Int) -> Optional { + let seek = fold(wpf_patterns(), init: WpfSeek { at: 0, found: optional_absent() }, f: fn(acc, p) { + WpfSeek { + at: acc.at + 1, + found: if acc.at == index { optional_present(value: p) } else { acc.found } + } + }) + seek.found +} + +type WpfSeek { at: Int, found: Optional } + +fn wpf_atom_is(n: Node, spelling: Symbol) -> Bool { + match node_atom_identity_optional(node: n) { + Present { value: id } => id == spelling + Absent => false + } +} + +test fn the_arm_list_lowers_all_four_arms() -> Bool { + length(wpf_patterns()) == 4 +} + +test fn a_bare_wildcard_arm_lowers_to_the_wildcard_form() -> Bool { + match wpf_pattern_at(index: 3) { + Present { value: p } => is_wildcard_pattern(pattern: p) && !wpf_atom_is(n: p, spelling: pattern_wildcard_name()) + Absent => false + } +} + +test fn a_wildcard_constructor_field_lowers_to_the_wildcard_form() -> Bool { + match wpf_pattern_at(index: 1) { + Present { value: p } => + match construct_tag_optional(n: p) { + Present { value: _ } => + match wpf_named_child(n: p, name: ^r) { + Present { value: t } => is_wildcard_pattern(pattern: t) + Absent => false + } + Absent => false + } + Absent => false + } +} + +test fn a_nullary_constructor_arm_stays_its_named_atom() -> Bool { + match wpf_pattern_at(index: 0) { + Present { value: p } => wpf_atom_is(n: p, spelling: ^WpfA) && !is_wildcard_pattern(pattern: p) + Absent => false + } +} + +test fn a_binder_arm_stays_its_named_atom() -> Bool { + match wpf_pattern_at(index: 2) { + Present { value: p } => wpf_atom_is(n: p, spelling: ^wpf_bound) && !is_wildcard_pattern(pattern: p) + Absent => false + } +} From 9bcaa67fc9bc15a6252f93ab277a562b8252f204 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 29 Sep 2026 06:11:58 +0000 Subject: [PATCH 2/4] XL-2 wildcard: state the rung the evidence establishes, not structural guarantee Co-Authored-By: Claude Opus 5.5 (1M context) --- .../a_wildcard_match_arm_resolves_as_an_unbound_name.dag | 2 +- src/v2/std/node_query.dag | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/dag/gunbc/recurring_failure_mode/a_wildcard_match_arm_resolves_as_an_unbound_name.dag b/dag/gunbc/recurring_failure_mode/a_wildcard_match_arm_resolves_as_an_unbound_name.dag index 2960668dbd0..b2604651268 100644 --- a/dag/gunbc/recurring_failure_mode/a_wildcard_match_arm_resolves_as_an_unbound_name.dag +++ b/dag/gunbc/recurring_failure_mode/a_wildcard_match_arm_resolves_as_an_unbound_name.dag @@ -13,7 +13,7 @@ data a_wildcard_match_arm_resolves_as_an_unbound_name: RecurringFailureMode = Re "HOW IT SURFACED: the XL-2 if-arm repair (`v2.compiler.body_lowering_fold` `body_lower_if_arm_lowered`) made a match inside an if-arm reach resolve for the first time. On main such a match was dropped whole by the operand reader, so `if c { s } else { match s { BlgA => .. _ => .. } }` resolved; after the repair it refuses at `_`, the verdict the same match already had at the top level of a fn body. The repair did not create the refusal; it stopped hiding it.", "CLASS: a resolve defect, not a lowering one: a wildcard pattern binds nothing and names nothing, and a resolver that looks it up as a symbol has conflated a pattern form with a reference. RUNG FOUND AT: a located refusal (mitigation) -- the wrong answer is loud, never silent, but a correct program is refused. CEILING: structurally guaranteed -- the wildcard is a distinct pattern constructor in the lowered tree, so the resolver can decline to look it up by construction. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: the lowered pattern carries the wildcard as its own form that resolve does not treat as a reference, with a witness whose red is this specimen on main and whose control is a match exhaustive through named arms only.", "REPAIRED AT THE RESOLVE LINK, TRIGGER NOT MET (2026-09-25, on 69e0bb7566e): the earliest wrong link was `v2.compiler.resolve` `resolve_pattern_node_walk`, which recognized `_` only as a constructor FIELD's target and sent a bare arm pattern to `resolve_node_walk` as a reference; it now recognizes `_` at every pattern depth. Witness `v2.test.claim.namespace_xl0.wildcard_arm_resolve`: `a_match_exhaustive_through_a_wildcard_arm_resolves` and `an_undeclared_name_in_a_wildcard_arm_body_is_the_sole_refusal_at_its_atom` false on main, true on head; the field-wildcard and named-binder controls true on both. MEASURED CENSUS BEHAVIOUR: on main the `_` refusal refused the whole module as its own chain ahead of any body refusal (`first=_ +wc_undeclared_body`), with observation COMPLETE -- it never marked the match or module incomplete, it added a spurious chain; on head the body name is the sole chain, observation complete. THE ROW STAYS OPEN because its trigger names a capability not delivered: the lowered pattern still carries the wildcard as an ATOM SPELLED `_`, recognized by `pattern_wildcard_name` at resolve, not as its own pattern form; that construction lives in `v2.compiler.body_lowering_fold`, which this repair did not touch.", - "TRIGGER MET, RUNG NOW STRUCTURALLY GUARANTEED (XL-2, sharp-carp-336): lowering mints the wildcard as its own pattern form. `v2.compiler.body_lowering_fold` `body_lower_pattern_atom_form` lowers an authored `_` pattern -- a bare arm or a constructor field's target -- to `v2.std.node_query` `wildcard_pattern_identity`, an atom whose identity `` no authored name can spell (`<` is not an identifier character; the `v2.std.anonymous_binder` precedent), at the `_`'s own occurrence. `is_wildcard_pattern` is the one recogniser: `v2.compiler.resolve` keeps the form at every pattern depth and never binds it, `v2.compiler.reference_conservation` `occurrence_spelling_conserved` credits the authored `_` to it, and `v2.std.compilers.target_model` `bound_spelling_from_map` spells it `_`. `pattern_wildcard_name` is now the AUTHORED spelling only: a `_` written as a body reference still refuses as a referenced anonymous parameter. WITNESS `v2.test.claim.body_lowering.wildcard_pattern_form`: the bare-arm and field-wildcard claims are false with main's lowering and true on head, and the nullary-constructor and binder controls are true on both. MUTATION: head's lowering with main's resolve reds `wildcard_arm_resolve` `a_match_exhaustive_through_a_wildcard_arm_resolves`, because main's resolve looked the form up as a name. The field-wildcard resolve claim stays green under that mutation, because main's resolve bound an unrecognised field atom as a binder, so the lowering witness is that case's discriminator.", + "THE WILDCARD NO LONGER SHARES A SPELLING WITH ANY NAME; THE TRIGGER IS STILL NOT MET (XL-2, gunbc#12597). `v2.compiler.body_lowering_fold` `body_lower_pattern_atom_form` lowers an authored `_` pattern, whether a bare arm or a constructor field's target, to `v2.std.node_query` `wildcard_pattern_identity`, at the `_`'s own occurrence. That is an Atom whose identity `` no authored name can spell, because `<` is not an identifier character; this follows the `v2.std.anonymous_binder` precedent. `is_wildcard_pattern` is the one recogniser, and three readers consult it: `v2.compiler.resolve` (`resolve_pattern_node_walk`, `resolve_pattern_binders`), `v2.compiler.reference_conservation` `occurrence_spelling_conserved`, and `v2.std.compilers.target_model` `bound_spelling_from_map`. `pattern_wildcard_name` is now the AUTHORED spelling only. WHAT THIS ESTABLISHES: no authored name can collide with the wildcard, and on the resolve path it is kept and never looked up. That is mechanically preventable, with the rung held at the readers that call the recogniser and executed by the witnesses below. WHAT IT DOES NOT ESTABLISH: the form is still an Atom connective distinguished by its identity, so a reader that does not call `is_wildcard_pattern` still receives a named atom. The rung is the minimum across paths, so it is not structurally guaranteed. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: the lowered pattern is read through a typed pattern carrier whose wildcard is its own variant, so that EVERY reader of a match arm's pattern matches that carrier exhaustively and none can receive the wildcard as a name. A new core connective is not the route, because the six-connective vocabulary is closed (DESIGN section 4). WITNESS `v2.test.claim.body_lowering.wildcard_pattern_form`: the bare-arm and field-wildcard claims are false with main's lowering and true on head, and the nullary-constructor and binder controls are true on both. MUTATION: head's lowering with main's resolve reds `wildcard_arm_resolve` `a_match_exhaustive_through_a_wildcard_arm_resolves`. The field-wildcard resolve claim stays green under that mutation, because main's resolve bound an unrecognised field atom as a binder, so the lowering witness is that case's discriminator.", ], evidence: [ diff --git a/src/v2/std/node_query.dag b/src/v2/std/node_query.dag index db8d0116099..d30dbedef29 100644 --- a/src/v2/std/node_query.dag +++ b/src/v2/std/node_query.dag @@ -209,9 +209,11 @@ fn pattern_wildcard_name() -> Symbol { // special-case its spelling -- resolve looked it up as a reference // (gunbc.recurring_failure_mode a_wildcard_match_arm_resolves_as_an_unbound_name). Lowering // (v2.compiler.body_lowering_fold body_lower_pattern_lowered) mints this identity for an authored `_` -// pattern, and `<` is not an identifier character, so no authored name can spell it: the form names -// nothing BY CONSTRUCTION, the anonymous-slot precedent (v2.std.anonymous_binder -// anonymous_param_label). THE ONE MINT; is_wildcard_pattern is the one recogniser. +// pattern, and `<` is not an identifier character, so no authored name can spell it -- the +// anonymous-slot precedent (v2.std.anonymous_binder anonymous_param_label). It is still an Atom told +// apart by identity, so a reader that must not treat it as a name asks is_wildcard_pattern; a typed +// pattern carrier with a wildcard variant is the next rung (the row above). THE ONE MINT; +// is_wildcard_pattern is the one recogniser. fn wildcard_pattern_identity() -> Symbol { symbol_intern_lexeme(lexeme: "") } From b45c58d31ab8b9f9edabf5c493b53c2f4ed2529e Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 29 Sep 2026 06:54:50 +0000 Subject: [PATCH 3/4] XL-2 wildcard witness: enroll wpf_patterns as a warm shared producer The five claims each paid the same ingest (~205k eval steps, ~590ms) against the 72,300-step new-witness budget; same ground as mals_lowered. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/workflow/floor_pure_producer_share.dag | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/v2/workflow/floor_pure_producer_share.dag b/src/v2/workflow/floor_pure_producer_share.dag index 676be0903ce..88778edf00e 100644 --- a/src/v2/workflow/floor_pure_producer_share.dag +++ b/src/v2/workflow/floor_pure_producer_share.dag @@ -620,6 +620,11 @@ import v2.std.collection { List } // mals_third_arm_malformed each tokenize and parse one inline module and call one producer // (body_lower_match_arms_optional) on its match capture; the stored value is the lowered arm list // (Nodes, no resolution context and no closure), portable for the same reason vpw_normalized is. +// THE XL-2 WILDCARD-PATTERN-FORM ROW IS THE MATCH-ARM LIST ROW'S GROUND AT ONE MORE SPECIMEN: +// v2.test.claim.body_lowering.wildcard_pattern_form wpf_patterns tokenizes and parses one inline +// module and calls body_lower_match_arms_optional on its match capture; the stored value is the +// lowered arm patterns (Nodes, no resolution context and no closure). Its first floor run refused all +// five claims over the enrolment margin at ~590ms each, every one of them paying the same ingest. // THE XL-2 MATCH-POSITION ROW: v2.test.claim.body_lowering.match_position_structure mps_normalized // runs the production route (module_roots_from_source_root_ingest) over three inline modules; the // stored value is the normalized trees (Nodes, no resolution context and no closure), portable for @@ -731,6 +736,7 @@ data floor_cross_claim_pure_producers_warm: List = [ "v2.test.claim.namespace_xl0.bound_value_resolve_refusal.bvr_outcomes", "v2.test.claim.body_lowering.match_arm_list_structure.mals_lowered", "v2.test.claim.body_lowering.match_arm_list_structure.mals_third_arm_malformed", + "v2.test.claim.body_lowering.wildcard_pattern_form.wpf_patterns", "v2.test.claim.body_lowering.match_position_structure.mps_normalized", "v2.test.claim.namespace_xl0.wildcard_arm_resolve.wc_outcomes", "v2.test.cli.v2_native_cli.cli_probe_trailing_outcome", From fab1868c22e4fd463cc7f3c03318ea1de5fc0fd3 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 29 Sep 2026 08:01:11 +0000 Subject: [PATCH 4/4] XL-2 wildcard: one recogniser at two grains; conservation and emission ask the symbol predicate Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/compiler/reference_conservation.dag | 6 +++--- src/v2/std/compilers/target_model.dag | 6 +++--- src/v2/std/node_query.dag | 10 +++++++++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/v2/compiler/reference_conservation.dag b/src/v2/compiler/reference_conservation.dag index 88299de1712..baec4ce50e0 100644 --- a/src/v2/compiler/reference_conservation.dag +++ b/src/v2/compiler/reference_conservation.dag @@ -65,7 +65,7 @@ import v2.compiler.occurrence_role { occurrence_role_index } import v2.std.anonymous_binder { anonymous_param_label, is_anonymous_param_label } -import v2.std.node_query { pattern_wildcard_name, wildcard_pattern_identity } +import v2.std.node_query { is_wildcard_pattern_identity, pattern_wildcard_name } // REFERENCE CONSERVATION ACROSS LOWERING (XL-2). // @@ -840,11 +840,11 @@ fn absent_cause(declared: ItemDeclarationStanding) -> DroppedReferenceCause { // below already consults, so it is asked here too: the locus is held and the spelling is the one // the rewrite derives. Any other spelling at the occurrence is still RespelledInNormalizedTree. // An authored `_` pattern is conserved by the wildcard pattern form lowering mints at its occurrence -// (v2.std.node_query wildcard_pattern_identity); the form is the `_`'s lowering, not a different atom. +// (v2.std.node_query is_wildcard_pattern_identity); the form is the `_`'s lowering, not a different atom. fn occurrence_spelling_conserved(found: Symbol, identity: Symbol) -> Bool { found == identity || found == body_lower_resolved_type_binding(sym: identity) - || ((identity == pattern_wildcard_name()) && (found == wildcard_pattern_identity())) + || ((identity == pattern_wildcard_name()) && is_wildcard_pattern_identity(sym: found)) } // WHERE AN ERASED ATOM IS COUNTED. The module header first (its item is -1), then an atom whose diff --git a/src/v2/std/compilers/target_model.dag b/src/v2/std/compilers/target_model.dag index 53f04168b93..f9d19bb4f1a 100644 --- a/src/v2/std/compilers/target_model.dag +++ b/src/v2/std/compilers/target_model.dag @@ -148,8 +148,8 @@ import v2.std.node_query { node_labeled_child_edges, node_positional_child_targets, nullary_inhabitant_by_discriminant, - pattern_wildcard_name, - wildcard_pattern_identity + is_wildcard_pattern_identity, + pattern_wildcard_name } import v2.std.qualified_name { QualifiedName, @@ -1127,7 +1127,7 @@ fn bound_spelling_from_map( Accepted { value: opt, diagnostics: pending } => match opt { Present { value: spelling } => Accepted { value: spelling, diagnostics: pending } Absent => - if is_anonymous_param_label(sym: binding) || (binding == wildcard_pattern_identity()) { + if is_anonymous_param_label(sym: binding) || is_wildcard_pattern_identity(sym: binding) { symbol_interned_spelling(binding: pattern_wildcard_name()) } else { symbol_interned_spelling(binding: binding) diff --git a/src/v2/std/node_query.dag b/src/v2/std/node_query.dag index d30dbedef29..183730996c0 100644 --- a/src/v2/std/node_query.dag +++ b/src/v2/std/node_query.dag @@ -218,10 +218,18 @@ fn wildcard_pattern_identity() -> Symbol { symbol_intern_lexeme(lexeme: "") } +// THE ONE RECOGNISER, AT TWO GRAINS. A reader holding a Symbol (a conserved atom's identity, an +// emitted binding) asks is_wildcard_pattern_identity; a reader holding a pattern Node asks +// is_wildcard_pattern, which is that same question plus the form's structure (an Atom with no +// children). No reader compares against wildcard_pattern_identity() itself. +fn is_wildcard_pattern_identity(sym: Symbol) -> Bool { + sym == wildcard_pattern_identity() +} + fn is_wildcard_pattern(pattern: Node) -> Bool { match pattern.kind { TypeNode { connective: Atom { identity: id } } => - (id == wildcard_pattern_identity()) && (count(pattern.children) == 0) + is_wildcard_pattern_identity(sym: id) && (count(pattern.children) == 0) _ => false } }