diff --git a/dag/gunbc/recurring_failure_mode/data_initializer_match_reads_its_scrutinee_from_an_arm_at_v2_body_lowering.dag b/dag/gunbc/recurring_failure_mode/data_initializer_match_reads_its_scrutinee_from_an_arm_at_v2_body_lowering.dag index 33024f565d8..30723fe7820 100644 --- a/dag/gunbc/recurring_failure_mode/data_initializer_match_reads_its_scrutinee_from_an_arm_at_v2_body_lowering.dag +++ b/dag/gunbc/recurring_failure_mode/data_initializer_match_reads_its_scrutinee_from_an_arm_at_v2_body_lowering.dag @@ -12,9 +12,12 @@ data data_initializer_match_reads_its_scrutinee_from_an_arm_at_v2_body_lowering: "WHERE, cited by symbol, traced (a normalized-tree dump of the fixture shows the scrutinee edge holding `dag_token_kw_true` at the first arm body's occurrence). `v2.compiler.body_lowering_fold` `body_lower_match_scrutinee_optional` finds the scrutinee by searching the WHOLE match subtree for the first `dag_surface_binary_expr`, then `dag_surface_postfix_expr` (`body_lower_find_captured`). A data initializer is folded bottom-up before the declaration is dispatched (`body_lower_data_initializer_body`'s own note), so by then the scrutinee's shell has been reduced and is no longer that production. The search therefore finds the next one, inside an arm body. That is a positional fact obtained by searching for its shape (`a_positional_fact_is_obtained_by_searching_for_its_shape`'s class), at a new site.", "RECEIPTS. Census rerun on the #12313 lexer (exact loci): 13 of the 3,472 atoms absent in accepted modules have a production chain `data_decl/expr/match_expr/binary_expr...` (the scrutinee position), which is exact at chain grain, all in `test.claim.manual.outcome_diagnostics_eq` `witness_diagnostics_merge_some`. Fixtures: a call scrutinee with record arguments and one with plain arguments both drop; the fn-body form drops only the argument label, which is a separate row.", "RUNG FOUND AT: below the floor (silent wrongness). CEILING: structurally guaranteed. The scrutinee is read from its grammar position, the child after `match`, or the match refuses. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: v2 body lowering reads a match's scrutinee positionally on every route, data initializers included, never by searching the match subtree. Owner: bright-boar-848's fold queue. The pinned drop claim holds while the drop happens and reds when the repair lands.", + "CLIMBED, THE TRIGGER CAPABILITY MET (lively-bear-30, XL-2). `v2.compiler.body_lowering_fold` `body_lower_match_scrutinee_optional` reads the scrutinee from its grammar position -- match_expr is seq(`match`, seq(binary_expr, seq(`{`, ..))), so it is the left of the spine's right after the `match` keyword -- on every route, and a spine not headed by `match` answers Absent, which the caller refuses as match_arm_navigation_refused. The subtree search and its spine-walk fallback (`body_lower_match_scrutinee_from_spine`, `body_lower_match_scrutinee_on_token`) are deleted. The position holds on the data route, whose scrutinee shell the fold has already reduced, because `body_lower_match_scrutinee_value` reads an already-lowered node as it stands. The pinned drop flips to a conservation control (accepted AND neither the scrutinee argument nor its callee dropped), red against the base fold and green after. Its pair is the same match as a fn body, which conserves on both sides (`the_same_match_as_a_fn_body_conserves_its_scrutinee_holds`). The shape is read by `v2.test.claim.body_lowering.match_position_structure`: the data route's Match has the authored call as its first positional child, red against the base fold, with the fn-route twin green on both sides. RUNG NOW: structurally guaranteed.", ], evidence: [ - DeclarationRef { module_path: "v2.test.claim.namespace_xl0.reference_conservation_accepted_drops", decl_name: "a_data_initializer_match_scrutinee_is_reported_dropped_holds", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.test.claim.namespace_xl0.reference_conservation_accepted_drops", decl_name: "a_data_initializer_match_scrutinee_is_conserved_holds", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.test.claim.namespace_xl0.reference_conservation_accepted_drops", decl_name: "the_same_match_as_a_fn_body_conserves_its_scrutinee_holds", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.test.claim.body_lowering.match_position_structure", decl_name: "a_data_initializer_match_lowers_its_scrutinee_from_the_match_position_holds", field: WholeDeclaration }, ], } diff --git a/dag/gunbc/recurring_failure_mode/match_arm_statement_body_lowers_to_its_first_expression_at_v2_body_lowering.dag b/dag/gunbc/recurring_failure_mode/match_arm_statement_body_lowers_to_its_first_expression_at_v2_body_lowering.dag index 3aa24a90bcd..55fdc1410b2 100644 --- a/dag/gunbc/recurring_failure_mode/match_arm_statement_body_lowers_to_its_first_expression_at_v2_body_lowering.dag +++ b/dag/gunbc/recurring_failure_mode/match_arm_statement_body_lowers_to_its_first_expression_at_v2_body_lowering.dag @@ -12,9 +12,11 @@ data match_arm_statement_body_lowers_to_its_first_expression_at_v2_body_lowering "WHERE, cited by symbol. `v2.compiler.body_lowering_fold` `body_lower_match_arm_body_capture_optional` answers the arm body with the first `dag_surface_expr` it finds in the subtree (`body_lower_find_captured`, a pre-order first match). For a `dag_surface_match_arm_stmt_body`, the first expr is the let's value. The `let` and the continuation are never read. This is one of the first-match readers in bright-boar-848's fold-cleanup lane.", "RECEIPTS. Fixture traced by the census at the #12313 head: `X => let t = zq / g(a: zr)` drops `t`, `g` and `zr`; with a nested match as the continuation, its scrutinee and arms are dropped too. Census: `v2.compiler.reference_conservation_census` `reference_conservation_census_for_paths` over `reference_conservation_stratified_sample_paths`, rerun on the #12313 lexer (exact loci). 29 of the 3,472 atoms absent in accepted modules have a production chain through `match_arm/stmt`, which is exact at chain grain; one of them is `test.claim.sql_create_table_fold_test` `sql_fold_fold_serialize`. DISTINGUISHING FACTS: not `lowering_rebuilds_an_authored_atom_without_its_occurrence`'s let binder, where the binder is kept at the let shell's occurrence; here the continuation is lost too. Not `match_arms_after_the_second_are_dropped_at_v2_body_lowering`: the arm is present, but its body is truncated.", "RUNG FOUND AT: below the floor (silent wrongness). CEILING: structurally guaranteed. An arm body lowers to the bind-and-continue form its statements denote, or refuses at the statement it could not read. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: v2 body lowering reads a match arm's statement body through the statement authority that fn bodies use, not by searching for an expression. Owner: bright-boar-848's fold queue. The pinned drop claim holds while the drop happens and reds when the repair lands.", + "CLIMBED, THE TRIGGER CAPABILITY MET (lively-bear-30, XL-2). Two readings in `v2.compiler.body_lowering_fold`. (1) `body_lower_match_arm_body_capture_optional` reads the arm body from its grammar position, the right of the `=>`, instead of the first expr, primary or postfix found in the arm. (2) `body_lower_match_arm_body_lowered`, which both arm routes now call, lowers a statement body through `body_lower_stmt_spine`, the authority a fn body and an if arm use: a let binds the rest, the last statement is the value, any other statement preceding another refuses located. The grammar's match_arm_stmt_body production is realized UNWRAPPED by `v2.compiler.parse` `parse_match_arm_stmt_body` (a parsed arm is `=>` followed directly by its `stmt` productions, with no production shell), so the statement body is recognized by what it is, a spine headed by a `stmt` production with a successor (`body_lower_match_arm_statement_spine_optional`). The pinned drop flips to a conservation control (accepted AND neither the continuation's argument nor the let's value dropped), red against the base fold and green after. The shape is read by `v2.test.claim.body_lowering.match_position_structure` `a_match_arm_statement_body_lowers_to_the_let_binding_its_continuation_holds`: the arm body is a Bind of key, value and continuation in that order, so a swapped value and continuation reds it. RUNG NOW: structurally guaranteed.", ], evidence: [ - DeclarationRef { module_path: "v2.test.claim.namespace_xl0.reference_conservation_accepted_drops", decl_name: "a_statement_after_a_let_in_a_match_arm_is_reported_dropped_holds", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.test.claim.namespace_xl0.reference_conservation_accepted_drops", decl_name: "a_statement_after_a_let_in_a_match_arm_is_conserved_holds", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.test.claim.body_lowering.match_position_structure", decl_name: "a_match_arm_statement_body_lowers_to_the_let_binding_its_continuation_holds", field: WholeDeclaration }, ], } diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index 78d99a94d55..270e48c6401 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -5396,22 +5396,29 @@ fn body_lower_match_arm_navigation_reject(arm_capture: Node) -> Outcome { ) } +// THE ARM BODY IS READ FROM ITS GRAMMAR POSITION: match_arm is seq(pattern, seq(`=>`, body)), so the +// body is the right of the `=>`. It was the first dag_surface_expr, then primary_expr, then +// postfix_expr found anywhere in the arm (body_lower_find_captured, a pre-order first match). For +// an arm whose body is a statement sequence -- `X => let t = v` then `e` -- the first expr is the +// let's value, so the arm lowered to `v`, and the binder and every later statement were gone with no +// diagnostic (gunbc.recurring_failure_mode +// match_arm_statement_body_lowers_to_its_first_expression_at_v2_body_lowering). An arm whose `=>` +// is not at its position answers Absent, and its caller refuses the arm's navigation. fn body_lower_match_arm_body_capture_optional(stripped: Node) -> Optional { - match body_lower_find_captured(root: stripped, emitted: ^dag_surface_expr) { - ParseSubtreeFound { captured: expr } => optional_present(value: expr) - ParseSubtreeAbsent => - match body_lower_find_captured( - root: stripped, - emitted: ^dag_surface_primary_expr - ) { - ParseSubtreeFound { captured: primary } => optional_present(value: primary) - ParseSubtreeAbsent => - match body_lower_find_captured( - root: stripped, - emitted: ^dag_surface_postfix_expr - ) { - ParseSubtreeFound { captured: postfix } => optional_present(value: postfix) - ParseSubtreeAbsent => optional_absent() + match body_lower_match_spine_right(spine: stripped) { + Absent => optional_absent() + Present { value: after_pattern } => + match body_lower_match_spine_left(spine: after_pattern) { + Absent => optional_absent() + Present { value: arrow_node } => + match node_atom_identity_optional(node: body_lower_deep_unwrap_optional(node: arrow_node)) { + Absent => optional_absent() + Present { value: arrow } => + if arrow == ^dag_token_fat_arrow { + body_lower_match_spine_right(spine: after_pattern) + } else { + optional_absent() + } } } } @@ -5582,11 +5589,47 @@ fn body_lower_pattern_lowered(pattern_capture: Node) -> Outcome { // its inner match -- and then kept as the FALLBACK for what the value reader declined, where its own // Absent was reported as match_arm_navigation_refused. That relabel is how an unlowered fn literal in // an arm body (a fold call's `f: fn(acc, x) { .. }`) surfaced as a navigation failure of the arm. -// An arm body now lowers through body_lower_value_read on both routes -- the pattern-capture route -// and the spine route below -- so its refusal is the value's, located at the value; the navigation -// refusal is left to exactly what it names: an arm whose pattern or `=>` cannot be found. +// An arm body now lowers through body_lower_match_arm_body_lowered on both routes -- the +// pattern-capture route and the spine route below -- which is the value reader for an expression +// body and the statement authority for a statement body, so its refusal is the value's or the +// statement's, located there; the navigation refusal is left to exactly what it names: an arm whose +// pattern or `=>` cannot be found. +// AN ARM BODY THAT IS A STATEMENT SEQUENCE LOWERS THROUGH THE STATEMENT AUTHORITY. The grammar's +// match_arm_stmt_body is seq(stmt, repeat(stmt)) (v2.extdeps.languages.dag +// dag_grammar_match_arm_stmt_body_expr), and the parser realizes it UNWRAPPED: +// v2.compiler.parse parse_match_arm_stmt_body answers the statements' repeat spine itself, with no +// production shell (measured on a parsed arm: `A =>` is followed directly by two `stmt` productions). +// So a statement body is recognized by what it is -- a spine whose head is a `stmt` production and +// which has a successor -- and lowers through body_lower_stmt_spine, the reader a fn body and an if +// arm use: a let binds the rest, the last statement is the value, and any other statement that +// precedes another refuses located. An expression body sits under its own expr production and is +// read by the value reader, as before. +fn body_lower_match_arm_statement_spine_optional(body_capture: Node) -> Optional { + let spine = body_lower_deep_unwrap_optional(node: body_capture) + match body_lower_stmt_spine_head_optional(spine: spine) { + Absent => optional_absent() + Present { value: head } => + match parse_production_emitted_identity_optional(node: body_lower_deep_unwrap_optional(node: head)) { + Absent => optional_absent() + Present { value: emitted } => + if (emitted == ^dag_surface_stmt) && body_lower_stmt_spine_has_successor(spine: spine) { + optional_present(value: spine) + } else { + optional_absent() + } + } + } +} + +fn body_lower_match_arm_body_lowered(body_capture: Node) -> Outcome { + match body_lower_match_arm_statement_spine_optional(body_capture: body_capture) { + Present { value: spine } => body_lower_stmt_spine(spine: spine) + Absent => body_lower_value_read(value: body_capture) + } +} + fn body_lower_match_arm_wire_read(pat_node: Node, body_capture: Node) -> Outcome { - match body_lower_value_read(value: body_capture) { + match body_lower_match_arm_body_lowered(body_capture: body_capture) { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: body, diagnostics: d } => outcome_with_diagnostics(value: body_lower_match_arm_wire(pat_node: pat_node, body: body), diagnostics: d) @@ -7081,45 +7124,6 @@ fn body_lower_match_spine_right(spine: Node) -> Optional { } } -fn body_lower_match_scrutinee_on_token(id: Symbol, left: Node, right: Node) -> Optional { - match id == ^dag_token_lbrace { - true => Absent - false => - match id == ^dag_token_kw_match { - true => body_lower_match_scrutinee_from_spine(spine: right) - false => - match body_lower_operand_ref_optional(node: left) { - Present { value: op } => Present { value: op } - Absent => body_lower_match_scrutinee_from_spine(spine: right) - } - } - } -} - -fn body_lower_match_scrutinee_from_spine(spine: Node) -> Optional { - if is_empty_conj_root(n: spine) { - Absent - } else { - match body_lower_match_spine_left(spine: spine) { - Present { value: left } => - match body_lower_match_spine_right(spine: spine) { - Present { value: right } => - match node_atom_identity_optional(node: body_lower_deep_unwrap_optional(node: left)) { - Present { value: id } => - body_lower_match_scrutinee_on_token(id: id, left: left, right: right) - Absent => - match body_lower_operand_ref_optional(node: left) { - Present { value: op } => Present { value: op } - Absent => body_lower_match_scrutinee_from_spine(spine: right) - } - } - Absent => Absent - } - Absent => Absent - } - } -} - // THE SCRUTINEE IS A VALUE, READ AS ONE. It was read by the operand reader alone, which before // gunbc#12145 narrowed a call scrutinee `match nat_compare(a: x, b: y) {..}` to `nat_compare`, // dropping its arguments, and after it answered Absent, so the whole match refused as @@ -7136,18 +7140,38 @@ fn body_lower_match_scrutinee_value(node: Node) -> Outcome> { } } +// THE SCRUTINEE IS READ FROM ITS GRAMMAR POSITION: match_expr is seq(`match`, seq(binary_expr, +// seq(`{`, ..))), so it is the left of the spine's right, after the `match` keyword. It was found by +// searching the whole match subtree for the first binary_expr, then postfix_expr. On a fn body that +// is the scrutinee, because the parse is unfolded. A data initializer is folded bottom-up before its +// declaration is dispatched (body_lower_data_initializer_body), so its scrutinee's shell is already +// reduced, and the search found the next binary_expr, inside the first arm's body: `data d: Bool = +// match h(a) { X => true .. }` lowered to a match on `true` with `h` and `a` gone and the module +// accepted (gunbc.recurring_failure_mode +// data_initializer_match_reads_its_scrutinee_from_an_arm_at_v2_body_lowering). The position holds on +// both routes, and body_lower_match_scrutinee_value reads an unfolded shell and an already-lowered +// node alike (body_lower_value_lowered peels a lowered node first). A match whose spine is not headed +// by `match` answers Absent, which its caller refuses as match_arm_navigation_refused. fn body_lower_match_scrutinee_optional(captured: Node) -> Outcome> { - match body_lower_find_captured(root: captured, emitted: ^dag_surface_binary_expr) { - ParseSubtreeFound { captured: binary } => body_lower_match_scrutinee_value(node: binary) - ParseSubtreeAbsent => - match body_lower_find_captured(root: captured, emitted: ^dag_surface_postfix_expr) { - ParseSubtreeFound { captured: postfix } => body_lower_match_scrutinee_value(node: postfix) - ParseSubtreeAbsent => - outcome_accepted( - value: body_lower_match_scrutinee_from_spine( - spine: body_lower_unwrap_surface_shell(node: captured) - ) - ) + let spine = body_lower_unwrap_surface_shell(node: captured) + match body_lower_match_spine_left(spine: spine) { + Absent => outcome_accepted(value: optional_absent()) + Present { value: head } => + match node_atom_identity_optional(node: body_lower_deep_unwrap_optional(node: head)) { + Absent => outcome_accepted(value: optional_absent()) + Present { value: id } => + if id == ^dag_token_kw_match { + match body_lower_match_spine_right(spine: spine) { + Absent => outcome_accepted(value: optional_absent()) + Present { value: after_keyword } => + match body_lower_match_spine_left(spine: after_keyword) { + Absent => outcome_accepted(value: optional_absent()) + Present { value: scrutinee } => body_lower_match_scrutinee_value(node: scrutinee) + } + } + } else { + outcome_accepted(value: optional_absent()) + } } } } diff --git a/src/v2/test/claim/body_lowering/match_position_structure_test.dag b/src/v2/test/claim/body_lowering/match_position_structure_test.dag new file mode 100644 index 00000000000..aa3da0eb0df --- /dev/null +++ b/src/v2/test/claim/body_lowering/match_position_structure_test.dag @@ -0,0 +1,177 @@ +module v2.test.claim.body_lowering.match_position_structure + +import v2.compiler.normalized_tree { NormalizedTree, normalized_tree_roots_to_nodes } +import v2.compiler.program_assembly { module_roots_from_source_root_ingest } +import v2.compiler.source_authority { DagSourceReadWitness, SourceRootIngest } +import v2.std.algebra { fold_list, length } +import v2.std.artifact { Artifact, SourceFile } +import v2.std.collection { List, list_at_optional } +import v2.std.compilers.body_lowering { Application, ApplicationRefused, NotApplication, application_read } +import v2.std.cross_tree.import_model { DagTree } +import v2.std.diagnostic { Accepted, Outcome, Rejected } +import v2.std.grammar { node_atom_identity_optional } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import v2.std.logic { Bool } +import v2.std.node { Bind, ComputationNode, Found, Match, Node, Symbol, named_edge_target_lookup, node_subtree_nodes } +import v2.std.node_query { node_positional_child_targets } +import v2.std.optional { Absent, Optional, Present, optional_absent, optional_present } +import v2.std.text { String } +import v2.extdeps.languages.dag { dag_language_model } +import std.algebra { Cons, Empty, FreeMonoid } +import extdeps.communication.medium { Lossless, Medium } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// A MATCH'S PARTS ARE READ FROM THEIR GRAMMAR POSITIONS, AND THESE CLAIMS READ THE SHAPE. +// +// The conservation controls (v2.test.claim.namespace_xl0.reference_conservation_accepted_drops +// *_is_conserved_holds) establish that every authored atom reaches the lowered tree. They stay +// green under a lowering that keeps every atom in the wrong place: a scrutinee lowered into an arm, +// or a let whose bound value and continuation are swapped. These claims read the LOWERED TREE the +// production route hands the resolver (program_assembly module_roots_from_source_root_ingest, +// projected through normalized_tree_roots_to_nodes) and assert where each part sits. +// +// THE PAIRING. The same match, with the same scrutinee call, is the initializer of a data +// declaration in one module and the body of a fn in the other. The two routes reach the match +// differently: a data initializer is folded bottom-up before its declaration is dispatched, so the +// scrutinee's own shell is already reduced when the match reads it; a fn body is read from its +// unfolded parse. The data route read its scrutinee from the first arm +// (gunbc.recurring_failure_mode data_initializer_match_reads_its_scrutinee_from_an_arm_at_v2_body_lowering), +// the fn route kept it. Each route's claim names its own callee, so neither can be satisfied by the +// other module. +// +// THE ARM STATEMENT BODY. `MpsA => let mps_local = mps_bound` followed by `mps_id(mps_after)` is a +// statement sequence, and it lowers to the Bind the statement authority builds for a fn body: key +// mps_local, value mps_bound, continuation the call. It lowered to `mps_bound` alone +// (gunbc.recurring_failure_mode match_arm_statement_body_lowers_to_its_first_expression_at_v2_body_lowering). +// The claim asserts the Bind's three positions, so a swapped value and continuation reds it. +// +// module_roots_from_source_root_ingest refuses the whole ingest when one module refuses, so a module +// that stops lowering reds every claim here, not only its own. + +data mps_data_source: String = "module v2.test.mps_data\n\ntype MpsT = MpsA | MpsB\n\ndata mps_arg_d: Int = 1\n\nfn mps_pick_d(a: Int) -> MpsT { MpsA }\n\ndata mps_matched: Bool = match mps_pick_d(mps_arg_d) {\n MpsA => true\n MpsB => false\n}\n" + +data mps_fn_source: String = "module v2.test.mps_fn\n\ntype MpsT = MpsA | MpsB\n\ndata mps_arg_f: Int = 1\n\nfn mps_pick_f(a: Int) -> MpsT { MpsA }\n\nfn mps_matched() -> Bool {\n match mps_pick_f(mps_arg_f) {\n MpsA => true\n MpsB => false\n }\n}\n" + +data mps_arm_source: String = "module v2.test.mps_arm\n\ntype MpsT = MpsA | MpsB\n\ndata mps_bound: Int = 1\n\ndata mps_after: Int = 2\n\nfn mps_id(a: Int) -> Int { a }\n\nfn mps_arm(t: MpsT) -> Int {\n match t {\n MpsA =>\n let mps_local = mps_bound\n mps_id(mps_after)\n MpsB => 0\n }\n}\n" + +fn mps_read(source: String, id: Symbol, unit: Symbol, path: String) -> DagSourceReadWitness { + DagSourceReadWitness { + source: Medium { carried: source, fidelity: Lossless }, + artifact: Artifact { kind: SourceFile, id: id, file_path: path }, + compilation_unit: unit, + source_root: DagTree + } +} + +data mps_ingest: SourceRootIngest = Cons { + head: mps_read(source: mps_data_source, id: ^mps_data_artifact, unit: ^mps_data_cu, path: "src/v2/test/fixture/body_lowering/mps_data.dag"), + tail: Cons { + head: mps_read(source: mps_fn_source, id: ^mps_fn_artifact, unit: ^mps_fn_cu, path: "src/v2/test/fixture/body_lowering/mps_fn.dag"), + tail: Cons { + head: mps_read(source: mps_arm_source, id: ^mps_arm_artifact, unit: ^mps_arm_cu, path: "src/v2/test/fixture/body_lowering/mps_arm.dag"), + tail: Empty + } + } + } + +fn mps_normalized() -> Outcome> { + module_roots_from_source_root_ingest(ingest: mps_ingest, lm: dag_language_model()) +} + +fn mps_roots() -> FreeMonoid { + match mps_normalized() { + Accepted { value: roots, diagnostics: _ } => normalized_tree_roots_to_nodes(roots: roots) + Rejected { diagnostics: _ } => Empty + } +} + +fn mps_is_atom(n: Node, id: Symbol) -> Bool { + match node_atom_identity_optional(node: n) { + Present { value: got } => got == id + Absent => false + } +} + +// A one-argument call to `callee` whose argument is the atom `arg`, read through +// v2.std.compilers.body_lowering application_read, the authority for what a lowered call is. +fn mps_is_unary_call(n: Node, callee: Symbol, arg: Symbol) -> Bool { + match application_read(site: n) { + Application { view: v } => + mps_is_atom(n: v.head, id: callee) && (length(xs: v.operands) == 1) && (match list_at_optional(xs: v.operands, index: 0) { + Present { value: a } => mps_is_atom(n: a, id: arg) + Absent => false + }) + NotApplication => false + ApplicationRefused { diagnostics: _ } => false + } +} + +fn mps_positional(n: Node, i: Int) -> Optional { + list_at_optional(xs: node_positional_child_targets(node: n), index: i) +} + +// A Match whose scrutinee (its first positional child, v2.std.compilers.body_lowering lower_match) +// is the call callee(arg). +fn mps_is_match_on_call(n: Node, callee: Symbol, arg: Symbol) -> Bool { + match n.kind { + ComputationNode { behavior: Match } => + match mps_positional(n: n, i: 0) { + Present { value: s } => mps_is_unary_call(n: s, callee: callee, arg: arg) + Absent => false + } + _ => false + } +} + +// A Bind (v2.std.compilers.body_lowering lower_bind: key, value, continuation, in that order) whose +// key is `key`, whose value is the atom `value`, and whose continuation is callee(arg). +fn mps_is_bind(n: Node, key: Symbol, value: Symbol, callee: Symbol, arg: Symbol) -> Bool { + match n.kind { + ComputationNode { behavior: Bind } => + (match mps_positional(n: n, i: 0) { Present { value: k } => mps_is_atom(n: k, id: key) Absent => false }) + && (match mps_positional(n: n, i: 1) { Present { value: v } => mps_is_atom(n: v, id: value) Absent => false }) + && (match mps_positional(n: n, i: 2) { Present { value: b } => mps_is_unary_call(n: b, callee: callee, arg: arg) Absent => false }) + _ => false + } +} + +// An arm (body_lowering_fold body_lower_match_arm_wire: a Conj with a Named match_arm_body edge) +// whose body is that Bind. +fn mps_is_arm_binding(n: Node, key: Symbol, value: Symbol, callee: Symbol, arg: Symbol) -> Bool { + match named_edge_target_lookup(children: n.children, name: ^match_arm_body) { + Found { target: body } => mps_is_bind(n: body, key: key, value: value, callee: callee, arg: arg) + _ => false + } +} + +type MpsShape + = MpsMatchOnCall { callee: Symbol, arg: Symbol } + | MpsArmBinding { key: Symbol, value: Symbol, callee: Symbol, arg: Symbol } + +fn mps_node_has_shape(n: Node, shape: MpsShape) -> Bool { + match shape { + MpsMatchOnCall { callee: c, arg: a } => mps_is_match_on_call(n: n, callee: c, arg: a) + MpsArmBinding { key: k, value: v, callee: c, arg: a } => mps_is_arm_binding(n: n, key: k, value: v, callee: c, arg: a) + } +} + +fn mps_any_node(shape: MpsShape) -> Bool { + fold_list(xs: mps_roots(), empty: false, cons: fn(acc, r) { + acc || fold_list(xs: node_subtree_nodes(root: r), empty: false, cons: fn(inner, n) { + inner || mps_node_has_shape(n: n, shape: shape) + }) + }) +} + +test fn a_data_initializer_match_lowers_its_scrutinee_from_the_match_position_holds() -> Bool { + mps_any_node(shape: MpsMatchOnCall { callee: ^mps_pick_d, arg: ^mps_arg_d }) +} + +test fn the_same_match_as_a_fn_body_lowers_its_scrutinee_from_the_match_position_holds() -> Bool { + mps_any_node(shape: MpsMatchOnCall { callee: ^mps_pick_f, arg: ^mps_arg_f }) +} + +test fn a_match_arm_statement_body_lowers_to_the_let_binding_its_continuation_holds() -> Bool { + mps_any_node(shape: MpsArmBinding { key: ^mps_local, value: ^mps_bound, callee: ^mps_id, arg: ^mps_after }) +} diff --git a/src/v2/test/claim/namespace_xl0/reference_conservation_accepted_drops_test.dag b/src/v2/test/claim/namespace_xl0/reference_conservation_accepted_drops_test.dag index 0cfe1dd1358..7561ff7393f 100644 --- a/src/v2/test/claim/namespace_xl0/reference_conservation_accepted_drops_test.dag +++ b/src/v2/test/claim/namespace_xl0/reference_conservation_accepted_drops_test.dag @@ -1,7 +1,7 @@ module v2.test.claim.namespace_xl0.reference_conservation_accepted_drops import v2.test.claim.namespace_xl0.reference_conservation { conservation_report_of, conservation_subject, report_drop_count } -import v2.compiler.reference_conservation { ReferenceConservationSubject } +import v2.compiler.reference_conservation { ReferenceConservationSubject, reference_conservation_accepted } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.std.logic { Bool } import v2.std.text { String } @@ -97,8 +97,11 @@ fn match_arm_statement_body_subject() -> ReferenceConservationSubject { conservation_subject(id_tail: "match_arm_statement_body", source: match_arm_statement_body_source) } -test fn a_statement_after_a_let_in_a_match_arm_is_reported_dropped_holds() -> Bool { - report_drop_count(report: conservation_report_of(subject: match_arm_statement_body_subject()), wanted: ^rc_after) == 1 +test fn a_statement_after_a_let_in_a_match_arm_is_conserved_holds() -> Bool { + let r = conservation_report_of(subject: match_arm_statement_body_subject()) + reference_conservation_accepted(report: r) + && (report_drop_count(report: r, wanted: ^rc_after) == 0) + && (report_drop_count(report: r, wanted: ^rc_bound) == 0) } // A data initializer that is a match reads its scrutinee from an arm: the call scrutinee -- callee @@ -111,6 +114,26 @@ fn data_initializer_match_scrutinee_subject() -> ReferenceConservationSubject { conservation_subject(id_tail: "data_initializer_match_scrutinee", source: data_initializer_match_scrutinee_source) } -test fn a_data_initializer_match_scrutinee_is_reported_dropped_holds() -> Bool { - report_drop_count(report: conservation_report_of(subject: data_initializer_match_scrutinee_subject()), wanted: ^rc_arg) == 1 +test fn a_data_initializer_match_scrutinee_is_conserved_holds() -> Bool { + let r = conservation_report_of(subject: data_initializer_match_scrutinee_subject()) + reference_conservation_accepted(report: r) + && (report_drop_count(report: r, wanted: ^rc_arg) == 0) + && (report_drop_count(report: r, wanted: ^rc_pick) == 0) +} + +// THE PAIRED CONTROL: the same match, the same scrutinee call, as a fn body. It conserved before the +// data-initializer repair and must still conserve after it -- the two routes reach the match +// differently (a data initializer is folded bottom-up before its declaration is dispatched; a fn body +// is read from its unfolded parse), and a repair to one must not cost the other. +data fn_body_match_scrutinee_source: String = "module v2.test.reference_conservation_fn_match\n\ntype RcT = RcA | RcB\n\ndata rc_arg: Int = 1\n\nfn rc_pick(a: Int) -> RcT { RcA }\n\nfn rc_matched() -> Bool {\n match rc_pick(rc_arg) {\n RcA => true\n RcB => false\n }\n}\n" + +fn fn_body_match_scrutinee_subject() -> ReferenceConservationSubject { + conservation_subject(id_tail: "fn_body_match_scrutinee", source: fn_body_match_scrutinee_source) +} + +test fn the_same_match_as_a_fn_body_conserves_its_scrutinee_holds() -> Bool { + let r = conservation_report_of(subject: fn_body_match_scrutinee_subject()) + reference_conservation_accepted(report: r) + && (report_drop_count(report: r, wanted: ^rc_arg) == 0) + && (report_drop_count(report: r, wanted: ^rc_pick) == 0) } diff --git a/src/v2/workflow/floor_pure_producer_share.dag b/src/v2/workflow/floor_pure_producer_share.dag index d4c5c290e8c..8c35bc82945 100644 --- a/src/v2/workflow/floor_pure_producer_share.dag +++ b/src/v2/workflow/floor_pure_producer_share.dag @@ -612,6 +612,10 @@ 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 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 +// the same reason vpw_normalized is. // THE XL-2 BOUND-VALUE RESOLVE ROW IS THE SAME GROUND AT ELEVEN SPECIMENS: // v2.test.claim.namespace_xl0.bound_value_resolve_refusal bvr_outcomes drives one front end over // eleven inline modules and stores one verdict arm per module, portable for the same reason. @@ -697,6 +701,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.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", "v2.test.emit.closure_emit_arrow_body_refusal.arrow_body_probe_verdict", @@ -925,6 +930,7 @@ data floor_cross_claim_pure_producers_warm: List = [ "v2.test.claim.namespace_xl0.reference_conservation_accepted_drops.qualified_constructor_value_subject", "v2.test.claim.namespace_xl0.reference_conservation_accepted_drops.match_arm_statement_body_subject", "v2.test.claim.namespace_xl0.reference_conservation_accepted_drops.data_initializer_match_scrutinee_subject", + "v2.test.claim.namespace_xl0.reference_conservation_accepted_drops.fn_body_match_scrutinee_subject", "v2.test.claim.namespace_xl0.reference_conservation.anonymous_slots_subject", "v2.test.claim.namespace_xl0.reference_conservation_admission.two_same_spelled_labels_subject", "v2.test.claim.namespace_xl0.reference_conservation_admission.operator_call_argument_subject",