diff --git a/dag/gunbc/compiler_frontend_program_status.dag b/dag/gunbc/compiler_frontend_program_status.dag index 03d9d70eb68..1232492418e 100644 --- a/dag/gunbc/compiler_frontend_program_status.dag +++ b/dag/gunbc/compiler_frontend_program_status.dag @@ -315,7 +315,7 @@ fn xl2_prerequisite_standing(p: Xl2Prerequisite) -> Xl2PrerequisiteStanding { BareFieldTypeVisibility => PrerequisiteDelivered { owner: decl_ref(module_path: "v2.test.claim.declaring_identity_spelling.production_ingest", decl_name: "a_bare_parameter_type_reaches_and_a_bare_field_type_does_too"), evidence: Xl2PrerequisiteEvidence { pr: 11574, merge_sha: "b705d17c45" as NonEmptyStr, measured_at: "b705d17c45" as NonEmptyStr }, - qualification: "BARE field types only. A QUALIFIED field-type mention is still absent from the census (v2.test.claim.declaring_identity_spelling.production_ingest a_qualified_field_type_mention_is_absent_from_the_census_today), so this does not establish field position as a whole." as NonEmptyStr, + qualification: "BARE field types only. The QUALIFIED form is its own row, QualifiedFieldTypeVisibility (v2.test.claim.declaring_identity_spelling.production_ingest a_qualified_field_type_mention_reaches_the_census_holds); together the two establish field position at reach grain." as NonEmptyStr, } WholeTreeResolveCensus => PrerequisiteDelivered { owner: decl_ref(module_path: "v2.test.claim.native_census.whole_tree_census_resolve", decl_name: "census_unbound_module_is_resolve_refused_holds"), @@ -381,9 +381,10 @@ fn xl2_prerequisite_standing(p: Xl2Prerequisite) -> Xl2PrerequisiteStanding { evidence: Xl2PrerequisiteEvidence { pr: 12297, merge_sha: "0579afae7f" as NonEmptyStr, measured_at: "0579afae7f" as NonEmptyStr }, qualification: "delivered at RESOLVE grain: v2.compiler.resolve resolve_pattern_node_walk recognizes `_` at every pattern depth, so resolve no longer emits a spurious unbound row at `_`; an undeclared name in a wildcard arm's body is still the sole refusal at its own atom (an_undeclared_name_in_a_wildcard_arm_body_is_the_sole_refusal_at_its_atom). NOT claimed: a distinct lowered wildcard form -- none exists, the wildcard reaches resolve as the atom `_` and resolve declines to look it up -- so gunbc.recurring_failure_mode.a_wildcard_match_arm_resolves_as_an_unbound_name stays OPEN until the pattern has its own lowered form." as NonEmptyStr, } - QualifiedFieldTypeVisibility => PrerequisiteOutstanding { - tracked_by: decl_ref(module_path: "v2.test.claim.declaring_identity_spelling.production_ingest", decl_name: "a_qualified_field_type_mention_is_absent_from_the_census_today"), - why: "a qualified field-type mention is absent from the census: qualification no longer hides a mention (a qualified parameter type reaches, a_qualified_type_position_mention_reaches_the_census_holds), but a record declaration's qualified field type is not grafted into the containment tree, so its import-dependent reference never reaches the residual. BareFieldTypeVisibility bounds itself to bare names for this reason. Tracked by the claim that asserts the absence, which reddens when the mention reaches. No fix has merged." as NonEmptyStr, + QualifiedFieldTypeVisibility => PrerequisiteDelivered { + owner: decl_ref(module_path: "v2.test.claim.declaring_identity_spelling.production_ingest", decl_name: "a_qualified_field_type_mention_reaches_the_census_holds"), + evidence: Xl2PrerequisiteEvidence { pr: 12033, merge_sha: "10e01b1169" as NonEmptyStr, measured_at: "14d58480c9" as NonEmptyStr }, + qualification: "delivered SILENTLY and found by execution, not by a fix aimed at it: the row that tracked it asserted the ABSENCE and was red on main 14d58480c9 with nothing restating it. Attributed by stage on the row's own two-module fixtures (a scratch probe, not committed): the parse holds the token, the normalized tree holds the whole qualified spine, and v2.compiler.reference_site_collector collect_reference_sites emits exactly one site for it, as for the qualified parameter-type control. The landing cited is the one whose change the probe points at -- gunbc#12033 lowers record fields into declared field identities, whose field type is read by the same body_lower_type_expr_lowered_optional a parameter type is -- located by git log -S and NOT re-executed at its parent, so it is the likely flip, not a measured one. Claimed at REACH grain: the mention reaches the census; whether it BINDS is the second fact the neighbouring rows leave to declaration grafting." as NonEmptyStr, } LoweringOccurrenceProjection => PrerequisiteOutstanding { tracked_by: decl_ref(module_path: "gunbc.recurring_failure_mode.lowering_rebuilds_an_authored_atom_without_its_occurrence", decl_name: "lowering_rebuilds_an_authored_atom_without_its_occurrence"), diff --git a/dag/test/claim/compiler_frontend_program_status_witness_test.dag b/dag/test/claim/compiler_frontend_program_status_witness_test.dag index 8e45db3c744..1111fe418db 100644 --- a/dag/test/claim/compiler_frontend_program_status_witness_test.dag +++ b/dag/test/claim/compiler_frontend_program_status_witness_test.dag @@ -697,7 +697,7 @@ fn xl2_label_is(p: Xl2Prerequisite, want: Xl2Prerequisite) -> Bool { xl2_prerequisite_label(p: p) == xl2_prerequisite_label(p: want) } -test fn xl2_prerequisites_partition_into_fifteen_delivered_and_eight_outstanding() -> Bool { +test fn xl2_prerequisites_partition_into_sixteen_delivered_and_seven_outstanding() -> Bool { count(xl2_prerequisites) == 23 && xl2_delivered_by(p: PlainCallArgumentReferenceVisibility, pr: 12108, merge_sha: "b0d307c31b") && xl2_delivered_by(p: BareFieldTypeVisibility, pr: 11574, merge_sha: "b705d17c45") @@ -717,12 +717,12 @@ test fn xl2_prerequisites_partition_into_fifteen_delivered_and_eight_outstanding && xl2_outstanding_tracked_by(p: AsCastOperandLowering, decl_name: "as_cast_has_no_lowered_form") && xl2_outstanding_tracked_by(p: ElseLessIfStatementLowering, decl_name: "else_less_if_statement_has_no_lowered_form") && xl2_delivered_by(p: WildcardMatchArmResolution, pr: 12297, merge_sha: "0579afae7f") - && xl2_outstanding_tracked_by(p: QualifiedFieldTypeVisibility, decl_name: "a_qualified_field_type_mention_is_absent_from_the_census_today") + && xl2_delivered_by(p: QualifiedFieldTypeVisibility, pr: 12033, merge_sha: "10e01b1169") && xl2_delivered_by(p: MatchEveryArmLowered, pr: 12383, merge_sha: "8ebd8b671d") && xl2_delivered_by(p: EnclosingExpressionLoweredWhole, pr: 12436, merge_sha: "d28bf20f0e") && xl2_delivered_by(p: DataInitializerMatchScrutineeLowering, pr: 12510, merge_sha: "b768b0f431") && xl2_delivered_by(p: MatchArmStatementBodyLowering, pr: 12510, merge_sha: "b768b0f431") - && count(xl2_prerequisites_outstanding()) == 8 + && count(xl2_prerequisites_outstanding()) == 7 && all(xl2_prerequisites_outstanding(), p => xl2_label_is(p: p, want: OptionalAccessorLocatedRefusal) || xl2_label_is(p: p, want: LoweringOccurrenceProjection) @@ -730,17 +730,16 @@ test fn xl2_prerequisites_partition_into_fifteen_delivered_and_eight_outstanding || xl2_label_is(p: p, want: LambdaArgumentValueLowering) || xl2_label_is(p: p, want: CaretSymbolOperandLowering) || xl2_label_is(p: p, want: AsCastOperandLowering) - || xl2_label_is(p: p, want: ElseLessIfStatementLowering) - || xl2_label_is(p: p, want: QualifiedFieldTypeVisibility)) + || xl2_label_is(p: p, want: ElseLessIfStatementLowering)) && all(xl2_prerequisites, p => count(xl2_prerequisites |> filter(q => xl2_prerequisite_label(p: q) == xl2_prerequisite_label(p: p))) == 1) } // THE INVARIANT THE PARTITION MUST NOT MOVE: prerequisites are necessary, not sufficient, so -// XL-2 stays NotDerivable and the producer literal stays Unavailable with fifteen of twenty-three delivered +// XL-2 stays NotDerivable and the producer literal stays Unavailable with sixteen of twenty-three delivered // -- and would stay so with twenty-three of twenty-three, because the producer is its own deliverable. test fn xl2_stays_not_derivable_with_its_prerequisites_mostly_delivered() -> Bool { - count(xl2_prerequisites |> filter(p => xl2_prerequisite_is_delivered(p: p))) == 15 + count(xl2_prerequisites |> filter(p => xl2_prerequisite_is_delivered(p: p))) == 16 && standing_is(want: ClassNotDerivable, s: stage_status(s: StageXL2RootDeletionRehearsal)) && match rehearsal_producer_standing() { RehearsalProducerUnavailable { why: _, cause: _ } => true diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index 171ad929185..05d50c09b47 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -407,6 +407,20 @@ fn body_lower_param_binding_symbols_from_param_list(param_list: Node) -> List Optional> { + match body_lower_param_list_comma_spine_optional(param_list_captured: param_list) { + Absent => Absent + Present { value: comma_spine } => + match body_lower_collect_typed_params_all_optional(sequence_capture: comma_spine) { + Absent => Absent + Present { value: params } => Present { value: list_map(xs: params, f: fn(p) { p.binder }) } + } + } +} + fn body_lower_param_binding_symbols_from_fn_captured(captured: Node) -> List { match body_lower_find_captured(root: captured, emitted: ^dag_surface_param_list) { ParseSubtreeFound { captured: param_list } => @@ -812,23 +826,55 @@ fn body_lower_type_variant_children_with_where( // two functions, so the record and positional halves of // gunbc.rung_drop variant_fields_unlowered_on_the_native_route close together, and a field // whose declaration cannot be read is a typed, located refusal -- never a residual shell. +// A field declaration is `name: T`, the shape of a typed parameter, so it is read into the same +// record (BodyLowerTypedParam): the binder atom keeps its token's occurrence, and the field Edge is the +// record's projection. fn body_lower_field_decl_edge_optional(item: Node) -> Optional { - let stripped = body_lower_deep_unwrap_optional(node: item) - match sugar_sequence_pair_optional(node: stripped) { + match body_lower_field_decl_optional(item: item) { + Absent => Absent + Present { value: field } => Present { value: body_lower_typed_param_edge_of(param: field) } + } +} + +fn body_lower_field_decl_optional(item: Node) -> Optional { + body_lower_typed_param_optional(param_capture: body_lower_deep_unwrap_optional(node: item)) +} + +// Every field of a list of field items, or Absent if any one is unreadable: the all-or-nothing read +// both field-bearing blocks lower through. +fn body_lower_field_decls_optional(items: List) -> Optional> { + fold(items, init: Present { value: Empty }, f: fn(acc, item) { + match acc { + Absent => Absent + Present { value: fields } => + match body_lower_field_decl_optional(item: item) { + Absent => Absent + Present { value: f } => Present { value: list_snoc_item(xs: fields, item: f) } + } + } + }) +} + +// `{ f: T, g: U }`: the block's captured is seq(`{`, seq(list, `}`)); its field items. +fn body_lower_field_decl_block_items_optional(captured: Node) -> Optional> { + match sugar_sequence_pair_optional(node: captured) { Absent => Absent Present { value: pair } => - match body_lower_param_binding_optional(node: pair.left) { + match sugar_sequence_pair_optional(node: pair.right) { Absent => Absent - Present { value: field_name } => - match sugar_sequence_pair_optional(node: pair.right) { - Absent => Absent - Present { value: after_colon } => - match body_lower_type_expr_lowered_optional(node: after_colon.right) { - Absent => Absent - Present { value: type_node } => - Present { value: Edge { label: Named { name: field_name }, target: type_node } } - } - } + Present { value: inner } => body_lower_comma_list_items_optional(list_capture: inner.left) + } + } +} + +// The binder atoms of one field_decl_block production, read by the reader its payload lowers through. +fn body_lower_field_decl_block_binder_atoms(captured: Node) -> Optional> { + match body_lower_field_decl_block_items_optional(captured: captured) { + Absent => Absent + Present { value: items } => + match body_lower_field_decls_optional(items: items) { + Absent => Absent + Present { value: fields } => Present { value: list_map(xs: fields, f: fn(f) { f.binder }) } } } } @@ -849,30 +895,13 @@ fn body_lower_field_decl_block_payload(block_shell: Node) -> Outcome { match parse_production_captured_child_optional(node: block_shell) { Absent => refuse Present { value: captured } => - match sugar_sequence_pair_optional(node: captured) { + match body_lower_field_decl_block_items_optional(captured: captured) { Absent => refuse - Present { value: pair } => - match sugar_sequence_pair_optional(node: pair.right) { + Present { value: items } => + match body_lower_field_decls_optional(items: items) { Absent => refuse - Present { value: inner } => - match body_lower_comma_list_items_optional(list_capture: inner.left) { - Absent => refuse - Present { value: items } => - match fold(items, init: Present { value: Empty }, f: fn(acc, item) { - match acc { - Absent => Absent - Present { value: edges } => - match body_lower_field_decl_edge_optional(item: item) { - Absent => Absent - Present { value: e } => Present { value: list_snoc_item(xs: edges, item: e) } - } - } - }) { - Absent => refuse - Present { value: edges } => - outcome_accepted(value: body_lower_payload_node(edges: edges, source: block_shell)) - } - } + Present { value: fields } => + outcome_accepted(value: body_lower_payload_node(edges: list_map(xs: fields, f: fn(f) { body_lower_typed_param_edge_of(param: f) }), source: block_shell)) } } } @@ -1721,100 +1750,112 @@ fn body_lower_after_comma_tail_optional(node: Node) -> Optional { } } -fn body_lower_typed_param_from_comma_seq(sequence_capture: Node) -> Optional { +// ONE PARAMETER AS AUTHORED: the binder atom, lowered from the token it was read from so it keeps +// its minted occurrence (body_lower_param_binding_atom_optional), and the lowered type. The +// parameter Edge is this record's projection (body_lower_typed_param_edge_of), and so is the binder +// list v2.compiler.occurrence_role reads (body_lower_param_list_binder_atoms): one reader, two views. +type BodyLowerTypedParam { + binder: Node, + name: Symbol, + type_node: Node, +} + +fn body_lower_typed_param_edge_of(param: BodyLowerTypedParam) -> Edge { + Edge { label: Named { name: param.name }, target: param.type_node } +} + +fn body_lower_collect_param_edges(sequence_capture: Node) -> List { + list_map(xs: body_lower_collect_typed_params(sequence_capture: sequence_capture), f: fn(param) { body_lower_typed_param_edge_of(param: param) }) +} + +fn body_lower_typed_param_from_comma_seq(sequence_capture: Node) -> Optional { match sugar_sequence_pair_optional(node: sequence_capture) { Present { value: pair } => - match body_lower_typed_param_edge(param_capture: pair.right) { - Present { value: edge } => Present { value: edge } - Absent => body_lower_typed_param_edge(param_capture: pair.left) + match body_lower_typed_param_optional(param_capture: pair.right) { + Present { value: param } => Present { value: param } + Absent => body_lower_typed_param_optional(param_capture: pair.left) } - Absent => body_lower_typed_param_edge(param_capture: sequence_capture) + Absent => body_lower_typed_param_optional(param_capture: sequence_capture) } } -fn body_lower_collect_param_edges_from_repeat_tail(repeat_capture: Node) -> List { +// ONE ENTRY PER PARAMETER SLOT THE LIST WRITES: Present for a slot read as a typed parameter, +// Absent for a slot the reader could not read. The walk never drops a slot, so the two views below +// differ only in what they do with an Absent one: lowering's view keeps the readable parameters +// (body_lower_collect_typed_params, its behaviour unchanged), and the binder view +// v2.compiler.occurrence_role reads (body_lower_param_list_binder_atoms) refuses the whole list, +// because a list with an unread binder is not a complete read of its binders (review 5355095582). +fn body_lower_collect_typed_param_slots_from_repeat_tail(repeat_capture: Node) -> List> { if is_empty_conj_root(n: repeat_capture) { Empty } else { match find_named_child(root: repeat_capture, name: ^grammar_sequence_left_node_projection) { Accepted { value: head_sequence, diagnostics: _ } => - match body_lower_typed_param_from_comma_seq(sequence_capture: head_sequence) { - Present { value: edge } => - match find_named_child( - root: repeat_capture, - name: ^grammar_sequence_right_node_projection - ) { - Accepted { value: tail, diagnostics: _ } => - list_append( - left: [edge], - right: body_lower_collect_param_edges_from_repeat_tail(repeat_capture: tail) - ) - Rejected { diagnostics: _ } => [edge] - } - Absent => - match find_named_child( - root: repeat_capture, - name: ^grammar_sequence_right_node_projection - ) { - Accepted { value: tail, diagnostics: _ } => - body_lower_collect_param_edges_from_repeat_tail(repeat_capture: tail) - Rejected { diagnostics: _ } => Empty - } + let slot = body_lower_typed_param_from_comma_seq(sequence_capture: head_sequence) + match find_named_child(root: repeat_capture, name: ^grammar_sequence_right_node_projection) { + Accepted { value: tail, diagnostics: _ } => + list_append(left: [slot], right: body_lower_collect_typed_param_slots_from_repeat_tail(repeat_capture: tail)) + Rejected { diagnostics: _ } => [slot] } Rejected { diagnostics: _ } => Empty } } } -fn body_lower_collect_param_edges(sequence_capture: Node) -> List { +fn body_lower_collect_typed_param_slots(sequence_capture: Node) -> List> { match find_named_child(root: sequence_capture, name: ^grammar_sequence_left_node_projection) { Accepted { value: first, diagnostics: _ } => - let first_edges = match body_lower_typed_param_edge(param_capture: first) { - Present { value: edge } => [edge] - Absent => Empty - } + let first_slots = [body_lower_typed_param_optional(param_capture: first)] match find_named_child(root: sequence_capture, name: ^grammar_sequence_right_node_projection) { Accepted { value: outer_right, diagnostics: _ } => match find_named_child(root: outer_right, name: ^grammar_sequence_left_node_projection) { Accepted { value: repeat_capture, diagnostics: _ } => - list_append( - left: first_edges, - right: body_lower_collect_param_edges_from_repeat_tail( - repeat_capture: repeat_capture - ) - ) - Rejected { diagnostics: _ } => first_edges + list_append(left: first_slots, right: body_lower_collect_typed_param_slots_from_repeat_tail(repeat_capture: repeat_capture)) + Rejected { diagnostics: _ } => first_slots } - Rejected { diagnostics: _ } => first_edges + Rejected { diagnostics: _ } => first_slots } Rejected { diagnostics: _ } => - match body_lower_typed_param_edge(param_capture: sequence_capture) { - Present { value: edge } => [edge] + match body_lower_typed_param_optional(param_capture: sequence_capture) { + Present { value: param } => [Present { value: param }] Absent => match sugar_sequence_pair_optional(node: sequence_capture) { Absent => Empty Present { value: pair } => - match body_lower_typed_param_edge(param_capture: pair.left) { - Present { value: edge } => - match body_lower_after_comma_tail_optional(node: pair.right) { - Present { value: tail } => - list_append( - left: [edge], - right: body_lower_collect_param_edges(sequence_capture: tail) - ) - Absent => [edge] - } - Absent => - match body_lower_after_comma_tail_optional(node: pair.right) { - Present { value: tail } => body_lower_collect_param_edges(sequence_capture: tail) - Absent => Empty - } + let slot = body_lower_typed_param_optional(param_capture: pair.left) + match body_lower_after_comma_tail_optional(node: pair.right) { + Present { value: tail } => list_append(left: [slot], right: body_lower_collect_typed_param_slots(sequence_capture: tail)) + Absent => [slot] } } } } } +// Lowering's view: the readable parameters, in order. +fn body_lower_collect_typed_params(sequence_capture: Node) -> List { + fold(body_lower_collect_typed_param_slots(sequence_capture: sequence_capture), init: Empty, f: fn(acc, slot) { + match slot { + Present { value: param } => list_snoc_item(xs: acc, item: param) + Absent => acc + } + }) +} + +// The strict view: every slot's parameter, or Absent if any slot is unreadable. +fn body_lower_collect_typed_params_all_optional(sequence_capture: Node) -> Optional> { + fold(body_lower_collect_typed_param_slots(sequence_capture: sequence_capture), init: Present { value: Empty }, f: fn(acc, slot) { + match acc { + Absent => Absent + Present { value: params } => + match slot { + Present { value: param } => Present { value: list_snoc_item(xs: params, item: param) } + Absent => Absent + } + } + }) +} + // A PARAMETER LIST'S EDGES, WITH EVERY ANONYMOUS SLOT MINTED. The raw collector below recurses over // the comma spine and cannot know a slot's position; the mint needs the whole list, so it runs once // here, and both the domain (body_lower_domain_from_param_list) and the ingest-side binding list the @@ -1863,24 +1904,23 @@ fn body_lower_param_binding_atom_optional(node: Node) -> Optional { } } -fn body_lower_typed_param_edge(param_capture: Node) -> Optional { +fn body_lower_typed_param_optional(param_capture: Node) -> Optional { match sugar_sequence_pair_optional(node: param_capture) { Absent => Absent Present { value: pair } => - match body_lower_param_binding_optional(node: pair.left) { + match body_lower_param_binding_atom_optional(node: pair.left) { Absent => Absent - Present { value: binding } => - match sugar_sequence_pair_optional(node: pair.right) { + Present { value: binder } => + match node_atom_identity_optional(node: binder) { Absent => Absent - Present { value: after_colon } => - match body_lower_type_expr_lowered_optional(node: after_colon.right) { + Present { value: binding } => + match sugar_sequence_pair_optional(node: pair.right) { Absent => Absent - Present { value: type_node } => - Present { - value: Edge { - label: Named { name: binding }, - target: type_node - } + Present { value: after_colon } => + match body_lower_type_expr_lowered_optional(node: after_colon.right) { + Absent => Absent + Present { value: type_node } => + Present { value: BodyLowerTypedParam { binder: binder, name: binding, type_node: type_node } } } } } @@ -2297,25 +2337,39 @@ fn body_lower_decl_generic_params_slot_optional(captured: Node) -> Optional Optional { - match body_lower_param_binding_optional(node: body_lower_deep_unwrap_optional(node: item)) { +// ONE TYPE PARAMETER AS AUTHORED: the item it was read from, the binder atom (lowered from its token, +// so it keeps its minted occurrence) and the name. The type-parameter Edge is its projection +// (body_lower_generic_param_edge_of), and so is the binder list v2.compiler.occurrence_role reads +// (body_lower_generic_params_binder_atoms). +type BodyLowerGenericBinder { + item: Node, + binder: Node, + name: Symbol, +} + +fn body_lower_generic_param_binder_optional(item: Node) -> Optional { + match body_lower_param_binding_atom_optional(node: body_lower_deep_unwrap_optional(node: item)) { Absent => optional_absent() - Present { value: name } => - optional_present(value: Edge { - label: Named { name: name }, - target: body_lower_param_ref_atom(source: item, identity: name) - }) + Present { value: binder } => + match node_atom_identity_optional(node: binder) { + Absent => optional_absent() + Present { value: name } => optional_present(value: BodyLowerGenericBinder { item: item, binder: binder, name: name }) + } } } +fn body_lower_generic_param_edge_of(b: BodyLowerGenericBinder) -> Edge { + Edge { label: Named { name: b.name }, target: body_lower_param_ref_atom(source: b.item, identity: b.name) } +} + // The repeat tail of comma_list(item): each element is seq(opt(comma), item). fn body_lower_no_type_param_edges() -> List { Empty } -fn body_lower_generic_param_edges_from_repeat_tail(repeat_capture: Node) -> Optional> { +fn body_lower_generic_param_binders_from_repeat_tail(repeat_capture: Node) -> Optional> { if is_empty_conj_root(n: repeat_capture) { - optional_present(value: body_lower_no_type_param_edges()) + optional_present(value: Empty) } else { match find_named_child(root: repeat_capture, name: ^grammar_sequence_left_node_projection) { Rejected { diagnostics: _ } => optional_absent() @@ -2323,15 +2377,15 @@ fn body_lower_generic_param_edges_from_repeat_tail(repeat_capture: Node) -> Opti match sugar_sequence_pair_optional(node: element) { Absent => optional_absent() Present { value: comma_then_item } => - match body_lower_generic_param_binder_edge(item: comma_then_item.right) { + match body_lower_generic_param_binder_optional(item: comma_then_item.right) { Absent => optional_absent() - Present { value: edge } => + Present { value: binder } => match find_named_child(root: repeat_capture, name: ^grammar_sequence_right_node_projection) { - Rejected { diagnostics: _ } => optional_present(value: [edge]) + Rejected { diagnostics: _ } => optional_present(value: [binder]) Accepted { value: tail, diagnostics: _ } => - match body_lower_generic_param_edges_from_repeat_tail(repeat_capture: tail) { + match body_lower_generic_param_binders_from_repeat_tail(repeat_capture: tail) { Absent => optional_absent() - Present { value: rest } => optional_present(value: list_append(left: [edge], right: rest)) + Present { value: rest } => optional_present(value: list_append(left: [binder], right: rest)) } } } @@ -2342,16 +2396,23 @@ fn body_lower_generic_param_edges_from_repeat_tail(repeat_capture: Node) -> Opti // comma_list(item) = seq(item, seq(repeat(seq(opt(comma), item)), opt(comma))). fn body_lower_generic_param_edges(comma_list: Node) -> Optional> { + match body_lower_generic_param_binders(comma_list: comma_list) { + Absent => optional_absent() + Present { value: binders } => optional_present(value: list_map(xs: binders, f: fn(b) { body_lower_generic_param_edge_of(b: b) })) + } +} + +fn body_lower_generic_param_binders(comma_list: Node) -> Optional> { match sugar_sequence_pair_optional(node: comma_list) { Absent => optional_absent() Present { value: head_rest } => - match body_lower_generic_param_binder_edge(item: head_rest.left) { + match body_lower_generic_param_binder_optional(item: head_rest.left) { Absent => optional_absent() Present { value: first } => match sugar_sequence_pair_optional(node: head_rest.right) { Absent => optional_absent() Present { value: repeat_then_comma } => - match body_lower_generic_param_edges_from_repeat_tail(repeat_capture: repeat_then_comma.left) { + match body_lower_generic_param_binders_from_repeat_tail(repeat_capture: repeat_then_comma.left) { Absent => optional_absent() Present { value: rest } => optional_present(value: list_append(left: [first], right: rest)) } @@ -2371,6 +2432,31 @@ type DeclTypeParams | TypeParams { conj: Node } | TypeParamsUnreadable +// generic_params' captured child is seq(`<`, seq(comma_list, `>`)). +fn body_lower_generic_params_comma_list_optional(generic_params: Node) -> Optional { + match sugar_sequence_pair_optional(node: generic_params) { + Absent => optional_absent() + Present { value: lt_rest } => + match sugar_sequence_pair_optional(node: lt_rest.right) { + Absent => optional_absent() + Present { value: list_gt } => optional_present(value: list_gt.left) + } + } +} + +// The binder atoms of one generic_params production, read by the reader body_lower_decl_type_params +// lowers them through. Absent where that reader refuses the list. +fn body_lower_generic_params_binder_atoms(generic_params: Node) -> Optional> { + match body_lower_generic_params_comma_list_optional(generic_params: generic_params) { + Absent => optional_absent() + Present { value: comma_list } => + match body_lower_generic_param_binders(comma_list: comma_list) { + Absent => optional_absent() + Present { value: binders } => optional_present(value: list_map(xs: binders, f: fn(b) { b.binder })) + } + } +} + fn body_lower_decl_type_params(captured: Node) -> DeclTypeParams { match body_lower_decl_generic_params_slot_optional(captured: captured) { Absent => TypeParamsUnreadable @@ -2378,22 +2464,18 @@ fn body_lower_decl_type_params(captured: Node) -> DeclTypeParams { match body_lower_find_captured(root: slot, emitted: ^dag_surface_generic_params) { ParseSubtreeAbsent => NoTypeParams ParseSubtreeFound { captured: generic_params } => - match sugar_sequence_pair_optional(node: generic_params) { + match body_lower_generic_params_comma_list_optional(generic_params: generic_params) { Absent => TypeParamsUnreadable - Present { value: lt_rest } => - match sugar_sequence_pair_optional(node: lt_rest.right) { + Present { value: comma_list } => + match body_lower_generic_param_edges(comma_list: comma_list) { Absent => TypeParamsUnreadable - Present { value: list_gt } => - match body_lower_generic_param_edges(comma_list: list_gt.left) { - Absent => TypeParamsUnreadable - Present { value: edges } => - TypeParams { - conj: node_lowered_from( - kind: TypeNode { connective: Conj }, - children: edges, - source: generic_params - ) - } + Present { value: edges } => + TypeParams { + conj: node_lowered_from( + kind: TypeNode { connective: Conj }, + children: edges, + source: generic_params + ) } } } @@ -6353,7 +6435,16 @@ fn body_lower_let_name_position(after_let: Node) -> Node { } } +// The let's key binding. The name is the projection of the binder atom below, which is lowered from +// the token it was read from so it keeps its minted occurrence (v2.compiler.occurrence_role reads it). fn body_lower_let_in_key_binding_optional(captured: Node) -> Optional { + match body_lower_let_in_key_binding_atom_optional(captured: captured) { + Present { value: binder } => node_atom_identity_optional(node: binder) + Absent => Absent + } +} + +fn body_lower_let_in_key_binding_atom_optional(captured: Node) -> Optional { let spine = body_lower_unwrap_surface_shell(node: captured) match body_lower_match_spine_left(spine: spine) { Present { value: left } => @@ -6362,28 +6453,29 @@ fn body_lower_let_in_key_binding_optional(captured: Node) -> Optional { match node_atom_identity_optional(node: body_lower_deep_unwrap_optional(node: left)) { Present { value: id } => if id == ^dag_token_kw_let { - match body_lower_param_binding_optional(node: body_lower_let_name_position(after_let: right)) { - Present { value: binding } => Present { value: binding } + match body_lower_param_binding_atom_optional(node: body_lower_let_name_position(after_let: right)) { + Present { value: binder } => Present { value: binder } Absent => match dag_binding_branded_atom_identity(node: right) { - Present { value: binding } => Present { value: binding } + Present { value: binding } => + Present { value: node_lowered_from(kind: TypeNode { connective: Atom { identity: binding } }, children: [], source: right) } Absent => Absent } } } else { - match body_lower_let_in_key_binding_optional(captured: left) { + match body_lower_let_in_key_binding_atom_optional(captured: left) { Present { value: found } => Present { value: found } - Absent => body_lower_let_in_key_binding_optional(captured: right) + Absent => body_lower_let_in_key_binding_atom_optional(captured: right) } } Absent => - match body_lower_let_in_key_binding_optional(captured: left) { + match body_lower_let_in_key_binding_atom_optional(captured: left) { Present { value: found } => Present { value: found } - Absent => body_lower_let_in_key_binding_optional(captured: right) + Absent => body_lower_let_in_key_binding_atom_optional(captured: right) } } Absent => - match body_lower_let_in_key_binding_optional(captured: left) { + match body_lower_let_in_key_binding_atom_optional(captured: left) { Present { value: found } => Present { value: found } Absent => Absent } @@ -6391,15 +6483,15 @@ fn body_lower_let_in_key_binding_optional(captured: Node) -> Optional { Absent => match sugar_sequence_pair_optional(node: spine) { Present { value: pair } => - match body_lower_let_in_key_binding_optional(captured: pair.left) { + match body_lower_let_in_key_binding_atom_optional(captured: pair.left) { Present { value: found } => Present { value: found } - Absent => body_lower_let_in_key_binding_optional(captured: pair.right) + Absent => body_lower_let_in_key_binding_atom_optional(captured: pair.right) } Absent => fold(spine.children, init: Absent, f: fn(acc, e) { match acc { Present { value: _ } => acc - Absent => body_lower_let_in_key_binding_optional(captured: e.target) + Absent => body_lower_let_in_key_binding_atom_optional(captured: e.target) } }) } @@ -8405,55 +8497,91 @@ fn body_lower_io_block(shell: Node) -> Outcome { match parse_production_captured_child_optional(node: shell) { Absent => refuse Present { value: captured } => - match sugar_sequence_pair_optional(node: captured) { + match body_lower_io_block_items_optional(captured: captured) { Absent => refuse - Present { value: kw } => - match sugar_sequence_pair_optional(node: kw.right) { - Absent => refuse - Present { value: brace } => - match sugar_sequence_pair_optional(node: brace.right) { - Absent => refuse - Present { value: inner } => - match body_lower_comma_list_items_optional(list_capture: inner.left) { - Absent => refuse - Present { value: items } => - match fold(items, init: Present { value: IoAcc { edges: Empty, set_aside: Empty } }, f: fn(acc, item) { - match acc { - Absent => Absent - Present { value: a } => - match sugar_sequence_pair_optional(node: body_lower_deep_unwrap_optional(node: item)) { - Absent => Absent - Present { value: field } => - match body_lower_field_decl_edge_optional(item: field.left) { - Absent => Absent - Present { value: edge } => - Present { - value: IoAcc { - edges: list_snoc_item(xs: a.edges, item: edge), - set_aside: if body_lower_io_field_tail_present(tail: field.right) { - list_snoc_item(xs: a.set_aside, item: ServiceSetAside { kind: InterfaceMemberUnmodeled, member: item }) - } else { - a.set_aside - } - } - } - } - } + Present { value: items } => + match fold(items, init: Present { value: IoAcc { edges: Empty, set_aside: Empty } }, f: fn(acc, item) { + match acc { + Absent => Absent + Present { value: a } => + match body_lower_io_item_optional(item: item) { + Absent => Absent + Present { value: io } => + Present { + value: IoAcc { + edges: list_snoc_item(xs: a.edges, item: body_lower_typed_param_edge_of(param: io.field)), + set_aside: if body_lower_io_field_tail_present(tail: io.tail) { + list_snoc_item(xs: a.set_aside, item: ServiceSetAside { kind: InterfaceMemberUnmodeled, member: item }) + } else { + a.set_aside } - }) { - Absent => refuse - Present { value: a } => - outcome_accepted( - value: BodyLowerIoBlock { payload: body_lower_payload_node(edges: a.edges, source: shell), set_aside: a.set_aside } - ) } - } - } + } + } + } + }) { + Absent => refuse + Present { value: a } => + outcome_accepted( + value: BodyLowerIoBlock { payload: body_lower_payload_node(edges: a.edges, source: shell), set_aside: a.set_aside } + ) + } + } + } +} + +// An io block's captured is seq(`input`|`output`, seq(`{`, seq(opt(comma_list), `}`))); its items. +fn body_lower_io_block_items_optional(captured: Node) -> Optional> { + match sugar_sequence_pair_optional(node: captured) { + Absent => Absent + Present { value: kw } => + match sugar_sequence_pair_optional(node: kw.right) { + Absent => Absent + Present { value: brace } => + match sugar_sequence_pair_optional(node: brace.right) { + Absent => Absent + Present { value: inner } => body_lower_comma_list_items_optional(list_capture: inner.left) } } } } +// An io item is seq(field_decl, tail): its field declaration and its `[from "key"] [= default]` tail. +type BodyLowerIoItem { + field: BodyLowerTypedParam, + tail: Node, +} + +fn body_lower_io_item_optional(item: Node) -> Optional { + match sugar_sequence_pair_optional(node: body_lower_deep_unwrap_optional(node: item)) { + Absent => Absent + Present { value: pair } => + match body_lower_field_decl_optional(item: pair.left) { + Absent => Absent + Present { value: field } => Present { value: BodyLowerIoItem { field: field, tail: pair.right } } + } + } +} + +// The binder atoms of one input_block or output_block production, read by the reader +// body_lower_io_block lowers its payload through. +fn body_lower_io_block_binder_atoms(captured: Node) -> Optional> { + match body_lower_io_block_items_optional(captured: captured) { + Absent => Absent + Present { value: items } => + fold(items, init: Present { value: Empty }, f: fn(acc, item) { + match acc { + Absent => Absent + Present { value: atoms } => + match body_lower_io_item_optional(item: item) { + Absent => Absent + Present { value: io } => Present { value: list_snoc_item(xs: atoms, item: io.field.binder) } + } + } + }) + } +} + type IoAcc { edges: List, set_aside: List, diff --git a/src/v2/compiler/occurrence_role.dag b/src/v2/compiler/occurrence_role.dag index 16caa54f1a2..119e9d14082 100644 --- a/src/v2/compiler/occurrence_role.dag +++ b/src/v2/compiler/occurrence_role.dag @@ -16,7 +16,16 @@ import std.occurrence_identity { TypeOccurrence } import v2.compiler.parse { ParseArtifact } -import v2.compiler.body_lowering_fold { body_lower_deep_unwrap_optional, body_lower_param_binding_atom_optional } +import v2.compiler.body_lowering_fold { + body_lower_deep_unwrap_optional, + body_lower_field_decl_block_binder_atoms, + body_lower_generic_params_binder_atoms, + body_lower_io_block_binder_atoms, + body_lower_let_in_key_binding_atom_optional, + body_lower_param_binding_atom_optional, + body_lower_param_list_binder_atoms +} +import v2.compiler.fold_lowering { ArrowLambdaValue, FnLiteralValue, FunctionValueKind, function_value_parts_optional } import v2.extdeps.languages.dag { dag_grammar_root, dag_named_arg_name_node_optional, @@ -24,7 +33,7 @@ import v2.extdeps.languages.dag { parse_production_captured_child_optional, parse_production_emitted_identity_optional } -import v2.std.algebra { fold_list, length, list_snoc_item } +import v2.std.algebra { fold_list, length, list_map, list_snoc_item } import v2.std.integer { Int } import v2.std.collection { List, Map, empty_map, map_insert, map_lookup } import v2.std.compilers.sugar { sugar_sequence_pair_optional } @@ -87,6 +96,12 @@ type NameReader | ReadFieldInitLabel | ReadNamedArgLabel | ReadFieldPatternName + | ReadParamListBinders + | ReadFieldDeclBinders + | ReadGenericParamBinders + | ReadLetBinder + | ReadFunctionValueBinders { kind: FunctionValueKind } + | ReadIoFieldBinders type NameProductionDisposition = NamesAreReferences @@ -159,15 +174,48 @@ fn read_field_pattern_name(captured: Node) -> NameRead { } } +// A binder LIST: every binder of the production, or a refusal where the lowering reader refuses the +// list. An empty list (`()`) is a lawful read of no names. +fn many_names(atoms: Optional>) -> NameRead { + match atoms { + Present { value: xs } => NamesFound { atoms: xs } + Absent => NameReadRefused + } +} + +// `fn(a, b) { .. }` and `p => ..` / `(a, b) => ..`: the binders v2.compiler.fold_lowering +// function_value_parts_optional reads, which body lowering's function-value producer lowers through. +// Each binder carries the token it was read from. +fn read_function_value_binders(captured: Node, kind: FunctionValueKind) -> NameRead { + match function_value_parts_optional(captured: captured, kind: kind) { + Absent => NameReadRefused + Present { value: parts } => NamesFound { atoms: list_map(xs: parts.binders, f: fn(b) { b.at }) } + } +} + fn read_names(reader: NameReader, captured: Node) -> NameRead { match reader { ReadKwThenIdent => read_kw_then_ident(captured: captured) ReadFieldInitLabel => read_field_init_label(captured: captured) ReadNamedArgLabel => read_named_arg_label(captured: captured) ReadFieldPatternName => read_field_pattern_name(captured: captured) + ReadParamListBinders => many_names(atoms: body_lower_param_list_binder_atoms(param_list: captured)) + ReadFieldDeclBinders => many_names(atoms: body_lower_field_decl_block_binder_atoms(captured: captured)) + ReadGenericParamBinders => many_names(atoms: body_lower_generic_params_binder_atoms(generic_params: captured)) + ReadLetBinder => one_name(atom: body_lower_let_in_key_binding_atom_optional(captured: captured)) + ReadFunctionValueBinders { kind: kind } => read_function_value_binders(captured: captured, kind: kind) + ReadIoFieldBinders => many_names(atoms: body_lower_io_block_binder_atoms(captured: captured)) } } +// `operation name { .. }`: body lowering reads the name with the kw-then-ident decoder +// (body_lower_operation), and lowers it to a callable edge of the service. +// `transport shell { .. }`: STILL NOT READ, AND NOT BECAUSE A READER IS MISSING. The name is not a +// binder and not a module-scope reference: it selects one of the closed set of transport kinds +// (v1 00_core TransportKind: rest, shell, file, local), and body lowering sets the member aside +// as a realization member without reading it. No disposition above describes that honestly +// (NamesAreMarkers claims a channel the census counts, and no OccurrenceCategory is a +// closed-vocabulary selector), so the row stays loud until that category is decided. fn dag_name_production_rows() -> List { [ name_row(production: ^dag_production_fn_decl, disposition: NameRoleRead { role: DeclarationRole, category: CallableOccurrence, reader: ReadKwThenIdent }), @@ -184,16 +232,16 @@ fn dag_name_production_rows() -> List { name_row(production: ^dag_production_postfix_expr, disposition: NamesAreReferences), name_row(production: ^dag_production_where_predicate, disposition: NamesAreReferences), name_row(production: ^dag_production_status_pattern, disposition: NamesAreReferences), - name_row(production: ^dag_production_param_list, disposition: NameRoleNotYetRead), - name_row(production: ^dag_production_field_decl_block, disposition: NameRoleNotYetRead), - name_row(production: ^dag_production_generic_params, disposition: NameRoleNotYetRead), - name_row(production: ^dag_production_let_expr, disposition: NameRoleNotYetRead), - name_row(production: ^dag_production_fn_literal, disposition: NameRoleNotYetRead), - name_row(production: ^dag_production_arrow_lambda, disposition: NameRoleNotYetRead), - name_row(production: ^dag_production_operation, disposition: NameRoleNotYetRead), - name_row(production: ^dag_production_transport, disposition: NameRoleNotYetRead), - name_row(production: ^dag_production_input_block, disposition: NameRoleNotYetRead), - name_row(production: ^dag_production_output_block, disposition: NameRoleNotYetRead) + name_row(production: ^dag_production_param_list, disposition: NameRoleRead { role: DeclarationRole, category: LexicalValueOccurrence, reader: ReadParamListBinders }), + name_row(production: ^dag_production_field_decl_block, disposition: NameRoleRead { role: DeclarationRole, category: FieldOccurrence, reader: ReadFieldDeclBinders }), + name_row(production: ^dag_production_generic_params, disposition: NameRoleRead { role: DeclarationRole, category: TypeOccurrence, reader: ReadGenericParamBinders }), + name_row(production: ^dag_production_let_expr, disposition: NameRoleRead { role: DeclarationRole, category: LexicalValueOccurrence, reader: ReadLetBinder }), + name_row(production: ^dag_production_fn_literal, disposition: NameRoleRead { role: DeclarationRole, category: LexicalValueOccurrence, reader: ReadFunctionValueBinders { kind: FnLiteralValue } }), + name_row(production: ^dag_production_arrow_lambda, disposition: NameRoleRead { role: DeclarationRole, category: LexicalValueOccurrence, reader: ReadFunctionValueBinders { kind: ArrowLambdaValue } }), + name_row(production: ^dag_production_operation, disposition: NameRoleRead { role: DeclarationRole, category: CallableOccurrence, reader: ReadKwThenIdent }), + name_row(production: ^dag_production_input_block, disposition: NameRoleRead { role: DeclarationRole, category: FieldOccurrence, reader: ReadIoFieldBinders }), + name_row(production: ^dag_production_output_block, disposition: NameRoleRead { role: DeclarationRole, category: FieldOccurrence, reader: ReadIoFieldBinders }), + name_row(production: ^dag_production_transport, disposition: NameRoleNotYetRead) ] } diff --git a/src/v2/test/claim/declaring_identity_spelling/production_ingest_test.dag b/src/v2/test/claim/declaring_identity_spelling/production_ingest_test.dag index 86774030c74..15586d653bc 100644 --- a/src/v2/test/claim/declaring_identity_spelling/production_ingest_test.dag +++ b/src/v2/test/claim/declaring_identity_spelling/production_ingest_test.dag @@ -494,15 +494,22 @@ test fn a_qualified_type_position_mention_reaches_the_census_holds() -> Bool { } } -// QUALIFIED field type. Still absent, and now for ONE reason rather than two: -// qualification no longer hides a mention (the two rows above reach), so what is -// left is field position -- the record declaration is not grafted into the -// containment tree at all. The discriminator is field_vs_parameter. -test fn a_qualified_field_type_mention_is_absent_from_the_census_today() -> Bool { +// QUALIFIED field type, no call. THIS ROW WAS ENROLLED AS "IS ABSENT TODAY" AND WAS FOUND RED ON +// MAIN (14d58480c9, 2026-09-29) WITH NOTHING HAVING RE-STATED IT: the mention reaches the census as +// one authored spelling, the way the parameter-type row beside it does. Attributed by stage on these +// fixtures: the parse holds the token, the normalized tree holds the whole qualified spine, and +// v2.compiler.reference_site_collector collect_reference_sites emits exactly one site for it. The +// gap the row recorded -- a record declaration not grafted -- closed when +// v2.compiler.namespace_graft began Naming every type_decl unit and body lowering declared record +// fields through body_lower_field_decl_block_payload, whose field type is read by the same +// body_lower_type_expr_lowered_optional a parameter type is. So this is now a conserved control: +// deleting that route makes it red. LocalParam is the positive control, as above; whether the +// mention BINDS is the second fact the rows above leave to their own trigger. +test fn a_qualified_field_type_mention_reaches_the_census_holds() -> Bool { match two_module_census() { Present { value: census } => two_module_usable_class_probe(census: census) - && !authored_is(census: census, spelling: qualified_field_type_spelling) + && authored_is(census: census, spelling: qualified_field_type_spelling) Absent => false } } diff --git a/src/v2/test/claim/namespace_xl0/reference_conservation_test.dag b/src/v2/test/claim/namespace_xl0/reference_conservation_test.dag index ca0862dd0e9..eb009d5f192 100644 --- a/src/v2/test/claim/namespace_xl0/reference_conservation_test.dag +++ b/src/v2/test/claim/namespace_xl0/reference_conservation_test.dag @@ -109,11 +109,13 @@ test fn the_clean_control_conserves_every_authored_atom_holds() -> Bool { // The numbers are separate and together account for the whole population: a check that found // nothing to conserve, or that summed one disposition into another, cannot pass this. The clean -// control has an atom in every one: conserved references, locus-erased binders the role producer -// does not read yet (its parameters), role-excluded declarations and labels, and the header. +// control has conserved references, role-excluded declarations, labels and parameters, and the +// header -- and, now that the parameter lists are read (v2.compiler.occurrence_role +// ReadParamListBinders), no locus-erased atom: before that reader its parameters were the +// locus-erased number, so a zero here is the reader's receipt, not an empty disposition. test fn the_clean_control_accounts_for_its_population_in_three_separate_numbers_holds() -> Bool { let r = clean_control_report() - r.conserved > 0 && r.locus_erased > 0 && r.role_excluded > 0 && r.header_channel > 0 + r.conserved > 0 && r.locus_erased == 0 && r.role_excluded > 0 && r.header_channel > 0 && r.conserved + r.locus_erased + r.role_excluded + r.header_channel == r.authored } @@ -372,13 +374,14 @@ test fn constructor_tags_keep_their_own_occurrence_holds() -> Bool { } // BINDERS AND LABELS ARE COUNTED OUTSIDE THE XL-2 POPULATION, NOT AS LOST REFERENCES. Of the -// fixture's erased atoms, five have a recorded role that is a declaration or a non-module-scope -// reference (the RcRow type, the two fn names, the `v:` record-literal label and the `v:` pattern -// label); three are the header; the three left in locus_erased are the parameter and field- -// declaration binders whose producer rows are not read yet, which role_not_yet_read counts. +// fixture's erased atoms, eight have a recorded role that is a declaration or a non-module-scope +// reference (the RcRow type, the two fn names, the `v:` record-literal label, the `v:` pattern +// label, the two parameters n and r, and the `v` field declaration); three are the header; none is +// left in locus_erased, and no production is left unread. Before the parameter and field-block +// readers landed those three binders were locus_erased and role_not_yet_read counted them. test fn binders_and_labels_are_role_excluded_not_locus_erased_holds() -> Bool { let r = conservation_report_of(subject: construct_tags_subject()) - r.role_excluded == 5 && r.header_channel == 3 && r.locus_erased == 3 && r.role_not_yet_read == 3 && r.role_reader_refused == 0 + r.role_excluded == 8 && r.header_channel == 3 && r.locus_erased == 0 && r.role_not_yet_read == 0 && r.role_reader_refused == 0 } // ---- dotted-spine segments keep their own occurrence (XL-2 LoweringOccurrenceProjection, rank 4) -- @@ -421,11 +424,12 @@ test fn two_anonymous_parameter_slots_are_conserved_by_minted_identity_holds() - // ---- roles are observed whatever normalization does (review 5330814496) ----------------------- -// A module that parses and whose normalization refuses (an else-less `if` statement). Its one fn -// has a parameter list, a binder production the role producer does not read yet, so an observed -// role pass counts exactly one not-yet-read node. Before the repair the refusal arm never ran the -// role producer and reported zero, a gap nobody had observed. -data normalization_refused_source: String = "module v2.test.reference_conservation_refused\n\nfn rc_guard(c: Bool, n: Int) -> Int {\n if c { return n }\n n\n}\n" +// A module that parses and whose normalization refuses (an else-less `if` statement). It also +// holds one transport member, the one production the role producer does not read (its name selects +// a transport kind, and no role describes that yet), so an observed role pass counts exactly one +// not-yet-read node. Before the repair the refusal arm never ran the role producer and reported +// zero, a gap nobody had observed. +data normalization_refused_source: String = "module v2.test.reference_conservation_refused\n\nservice rc.Svc {\n operation RcOp {\n transport shell { argv: [] }\n }\n}\n\nfn rc_guard(c: Bool, n: Int) -> Int {\n if c { return n }\n n\n}\n" fn normalization_refused_subject() -> ReferenceConservationSubject { conservation_subject(id_tail: "normalization_refused", source: normalization_refused_source) diff --git a/src/v2/test/claim/occurrence_role/occurrence_role_test.dag b/src/v2/test/claim/occurrence_role/occurrence_role_test.dag index c5706601017..c3655a3c36a 100644 --- a/src/v2/test/claim/occurrence_role/occurrence_role_test.dag +++ b/src/v2/test/claim/occurrence_role/occurrence_role_test.dag @@ -12,6 +12,7 @@ import v2.compiler.occurrence_role { OccurrenceRoleTableRefused, OccurrenceRolesRead, ReadKwThenIdent, + dag_name_production_emitted_symbols, dag_name_production_rows, name_production_table_admission, name_row, @@ -19,10 +20,11 @@ import v2.compiler.occurrence_role { } import v2.test.claim.namespace_xl0.reference_conservation { conservation_subject } import v2.extdeps.languages.dag { dag_grammar_root } +import std.algebra { Empty } import v2.std.algebra { fold_list, length, list_snoc_item } -import v2.std.collection { List, map_lookup } +import v2.std.collection { List, Map, map_lookup } import v2.std.optional { Absent, Present } -import v2.std.node { Conj, Edge, Named, Node, TypeNode, node_with_occurrence_id } +import v2.std.node { Atom, Conj, Edge, Named, Node, TypeNode, node_with_occurrence_id, symbol_intern_lexeme } import std.occurrence_identity { OccurrenceSynthetic } import v2.std.diagnostic { Accepted, Rejected } import v2.std.grammar { GrammarProduction, GrammarRoot, grammar_atom, grammar_expr_terminal_lexeme } @@ -137,27 +139,28 @@ fn roles_count(outcome: OccurrenceRoleOutcome, role: OccurrenceRole, category: O } // The fixture declares a type (RcRow), three fns (rc_callee, rc_make, rc_positional) and a data -// (rc_row); it labels one record field (`v:` in RcRow { v: .. }) and one call argument (`k:` in -// rc_callee(k: n)); `rc_callee(n)` is a positional argument, which carries no name. Every -// other name in it -- the parameters, the field declaration, the references -- is either a -// reference (not recorded) or a production whose reader has not landed (counted, not recorded). +// (rc_row); three parameters (k, n, n) and one record field (`v: Int` in RcRow); it labels one +// record field (`v:` in RcRow { v: .. }) and one call argument (`k:` in rc_callee(k: n)); +// `rc_callee(n)` is a positional argument, which carries no name. Every other name in it is a +// reference, which is not recorded. test fn the_fixture_declarations_and_labels_are_recorded_holds() -> Bool { let o = roles_fixture_outcome() roles_count(outcome: o, role: DeclarationRole, category: TypeOccurrence) == 1 && roles_count(outcome: o, role: DeclarationRole, category: CallableOccurrence) == 3 - && roles_count(outcome: o, role: DeclarationRole, category: LexicalValueOccurrence) == 1 + && roles_count(outcome: o, role: DeclarationRole, category: LexicalValueOccurrence) == 4 + && roles_count(outcome: o, role: DeclarationRole, category: FieldOccurrence) == 1 && roles_count(outcome: o, role: ReferenceRole, category: FieldOccurrence) == 1 && roles_count(outcome: o, role: ReferenceRole, category: LexicalValueOccurrence) == 1 } -// The references are not recorded: exactly the seven names above and nothing else. And the +// The references are not recorded: exactly the eleven names above and nothing else. And the // positional argument is a lawful absence of a name, not a reader refusal: no production of the // fixture counts one. test fn the_fixture_records_no_reference_to_a_module_scope_name_holds() -> Bool { match roles_fixture_outcome() { OccurrenceRoleTableRefused { unlisted: _, unknown: _, unkeyed: _ } => false OccurrenceRolesRead { roles: r } => - length(xs: r.roles) == 7 && r.unminted == 0 + length(xs: r.roles) == 11 && r.unminted == 0 && refused_at(roles: r, emitted: ^dag_surface_arg) == 0 && refused_at(roles: r, emitted: ^dag_surface_fn_decl) == 0 && refused_at(roles: r, emitted: ^dag_surface_field_init) == 0 @@ -192,3 +195,121 @@ test fn an_unreadable_binder_is_counted_as_a_reader_refusal_holds() -> Bool { OccurrenceRolesRead { roles: r } => refused_at(roles: r, emitted: ^dag_surface_fn_decl) == 1 && length(xs: r.roles) == 0 } } + +// ---- the binder productions, one fixture --------------------------------------------------------- + +// Every binder-bearing production whose reader is body lowering's own: type and fn generic +// parameters, parameters, a record field block, a statement let, an arrow lambda, a fn literal, and +// a service operation with its input and output field blocks. Each binder is written once and named +// by its spelling so a miscount says which reader moved. +data binders_fixture_source: String = "module v2.test.occurrence_role_binder_fixture\n\ntype BfRow {\n bf_v: T\n}\n\nfn bf_id(bf_x: U) -> U {\n let bf_y = bf_x\n bf_y\n}\n\nfn bf_apply(bf_f: Int) -> Int { bf_run(bf_g: bf_p => bf_p, bf_h: fn(bf_a, bf_b) { bf_a }) }\n\nservice bf.Svc {\n operation BfOp {\n input { bf_q: Int }\n output { bf_r: Int }\n }\n}\n" + +fn binders_fixture_outcome() -> OccurrenceRoleOutcome { + let subject = conservation_subject(id_tail: "occurrence_role_binders", source: binders_fixture_source) + match subject.parsed { + Rejected { diagnostics: _ } => OccurrenceRoleTableRefused { unlisted: [^occurrence_role_binder_fixture_unparsed], unknown: [], unkeyed: [] } + Accepted { value: artifact, diagnostics: _ } => occurrence_roles_of_tree(tree: artifact.tree, root: dag_grammar_root(), rows: dag_name_production_rows()) + } +} + +// Type binders: BfRow, T, U. Callables: bf_id, bf_apply, BfOp. Lexical binders: bf_x, bf_f +// (parameters), bf_y (let), bf_p (lambda), bf_a, bf_b (fn literal). Fields: bf_v (record), bf_q +// (input), bf_r (output). Labels: bf_g, bf_h. +test fn every_binder_production_records_its_binders_holds() -> Bool { + let o = binders_fixture_outcome() + roles_count(outcome: o, role: DeclarationRole, category: TypeOccurrence) == 3 + && roles_count(outcome: o, role: DeclarationRole, category: CallableOccurrence) == 3 + && roles_count(outcome: o, role: DeclarationRole, category: LexicalValueOccurrence) == 6 + && roles_count(outcome: o, role: DeclarationRole, category: FieldOccurrence) == 3 + && roles_count(outcome: o, role: ReferenceRole, category: LexicalValueOccurrence) == 2 +} + +// A per-production counter summed over every production the table lists. +fn count_total(m: Map) -> Int { + fold(dag_name_production_emitted_symbols(), init: 0, f: fn(acc, k) { + match map_lookup(m: m, key: k) { + Present { value: n } => acc + n + Absent => acc + } + }) +} + +// Nothing in the fixture is left unread or refused: the gap the NameRoleNotYetRead rows used to +// count is gone for every production the fixture writes, and each new reader read, rather than +// declined, its production. +test fn the_binder_productions_are_neither_unread_nor_refused_holds() -> Bool { + match binders_fixture_outcome() { + OccurrenceRoleTableRefused { unlisted: _, unknown: _, unkeyed: _ } => false + OccurrenceRolesRead { roles: r } => + length(xs: r.roles) == 17 && r.unminted == 0 + && count_total(m: r.not_yet_read) == 0 + && count_total(m: r.reader_refused) == 0 + } +} + +// ---- a partly unreadable parameter list refuses whole (review 5355095582) ------------------------- + +// A PARSED list with one readable binder and one the typed-parameter reader cannot read. No source +// spelling makes a parseable binder unreadable, so the fault is PLANTED AT THE INTERFACE: the binder +// token `zz_plant` of the parsed tree is rewritten to the bare token-class identity dag_token_ident +// (not a branded binding, and a lexer-token symbol), which +// v2.compiler.body_lowering_fold body_lower_param_binding_atom_optional refuses. Everything else is +// the real parse. The role reader must count ONE reader refusal at param_list and record no binder +// from that list -- a readable prefix reported as NamesFound is the fail-open this control reddens. +data mixed_param_list_source: String = "module v2.test.occurrence_role_mixed_params\n\nfn rc_mixed(a: Int, zz_plant: Int) -> Int { a }\n" + +fn plant_unreadable_binder(n: Node) -> Node { + let kind = match n.kind { + TypeNode { connective: Atom { identity: s } } => + if s == symbol_intern_lexeme(lexeme: "zz_plant") { TypeNode { connective: Atom { identity: ^dag_token_ident } } } else { n.kind } + _ => n.kind + } + node_with_occurrence_id( + kind: kind, + children: fold(n.children, init: Empty, f: fn(acc, e) { list_snoc_item(xs: acc, item: Edge { label: e.label, target: plant_unreadable_binder(n: e.target) }) }), + occurrence_id: n.occurrence_id + ) +} + +// The two walks are nullary so the floor computes each ONCE (both are enrolled WARM in +// v2.workflow.floor_pure_producer_share beside roles_fixture_outcome) and each claim below is a +// read of its value, not a re-parse. +fn mixed_param_list_parsed() -> OccurrenceRoleOutcome { + mixed_param_list_walk(plant: false) +} + +fn mixed_param_list_planted() -> OccurrenceRoleOutcome { + mixed_param_list_walk(plant: true) +} + +fn mixed_param_list_walk(plant: Bool) -> OccurrenceRoleOutcome { + let subject = conservation_subject(id_tail: "occurrence_role_mixed_params", source: mixed_param_list_source) + match subject.parsed { + Rejected { diagnostics: _ } => OccurrenceRoleTableRefused { unlisted: [^occurrence_role_mixed_fixture_unparsed], unknown: [], unkeyed: [] } + Accepted { value: artifact, diagnostics: _ } => + if plant { + occurrence_roles_of_tree(tree: plant_unreadable_binder(n: artifact.tree), root: dag_grammar_root(), rows: dag_name_production_rows()) + } else { + occurrence_roles_of_tree(tree: artifact.tree, root: dag_grammar_root(), rows: dag_name_production_rows()) + } + } +} + +// The positive control ON THE SAME PARSE, unplanted: both parameters are read, nothing is refused. +test fn the_unplanted_mixed_fixture_reads_both_parameters_holds() -> Bool { + match mixed_param_list_parsed() { + OccurrenceRoleTableRefused { unlisted: _, unknown: _, unkeyed: _ } => false + OccurrenceRolesRead { roles: r } => + roles_count(outcome: OccurrenceRolesRead { roles: r }, role: DeclarationRole, category: LexicalValueOccurrence) == 2 + && refused_at(roles: r, emitted: ^dag_surface_param_list) == 0 + } +} + +test fn a_param_list_with_one_unreadable_binder_refuses_whole_holds() -> Bool { + match mixed_param_list_planted() { + OccurrenceRoleTableRefused { unlisted: _, unknown: _, unkeyed: _ } => false + OccurrenceRolesRead { roles: r } => + roles_count(outcome: OccurrenceRolesRead { roles: r }, role: DeclarationRole, category: LexicalValueOccurrence) == 0 + && refused_at(roles: r, emitted: ^dag_surface_param_list) == 1 + } +} diff --git a/src/v2/workflow/floor_pure_producer_share.dag b/src/v2/workflow/floor_pure_producer_share.dag index 21fb5ac4787..25110be265a 100644 --- a/src/v2/workflow/floor_pure_producer_share.dag +++ b/src/v2/workflow/floor_pure_producer_share.dag @@ -993,6 +993,9 @@ data floor_cross_claim_pure_producers_warm: List = [ "v2.test.claim.namespace_xl0.reference_conservation_census.under_floor_fixture_files", "v2.test.claim.namespace_xl0.reference_conservation_census.half_share_fixture_files", "v2.test.claim.occurrence_role.occurrence_role.roles_fixture_outcome", + "v2.test.claim.occurrence_role.occurrence_role.binders_fixture_outcome", + "v2.test.claim.occurrence_role.occurrence_role.mixed_param_list_parsed", + "v2.test.claim.occurrence_role.occurrence_role.mixed_param_list_planted", "v2.test.claim.namespace_xl0.reference_conservation.statement_let_binder_subject", "v2.test.claim.namespace_xl0.reference_conservation_accepted_drops.binary_operand_beside_match_subject", "v2.test.claim.namespace_xl0.reference_conservation_accepted_drops.binary_operand_beside_if_subject",