diff --git a/dag/gunbc/recurring_failure_mode/service_interface_member_has_no_carrier.dag b/dag/gunbc/recurring_failure_mode/service_interface_member_has_no_carrier.dag new file mode 100644 index 00000000000..54971e47f0f --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/service_interface_member_has_no_carrier.dag @@ -0,0 +1,22 @@ +module gunbc.recurring_failure_mode.service_interface_member_has_no_carrier + +import std.types { NonEmptyStr } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data service_interface_member_has_no_carrier: RecurringFailureMode = RecurringFailureMode { + identity: "service_interface_member_has_no_carrier" as NonEmptyStr, + + receipts: [ + "INVALID STATE: an interface fact of a v2 service declaration has no typed carrier, so the declaration-grade lowering (`v2.compiler.body_lowering_fold` `body_lower_service_decl`) cannot lower it with the interface. The members are the operation modifiers `readonly` / `idempotent` / `hermetic` and an io field's `from \"key\"` wire-key and `= default` tails. They are set aside as `body_lowering_reason_interface_member_unmodeled`, typed, located and counted apart from the realization members (`body_lowering_reason_realization_member_set_aside`: transport, config, exit, response, mock_response), which are realization per DESIGN section 3 and are never part of the interface.", + "HARM: these are safety-relevant interface facts, not decoration. `idempotent` decides whether an automatic retry is admissible (DESIGN section 4b names a non-idempotent effect under retry as a class the ladder must reach), and a `from` key decides which wire field fills a declared output. A consumer that read the lowered interface without them would retry what it must not, or read a field it was not given.", + "CONTAINED, NOT SILENT: a set-aside member is unreachable as lowered, by construction. The lowering builds the set-aside population as a typed list (`ServiceSetAside`), and only census grade (`v2.compiler.normalize` `normalize_census`, whose carrier has no route past the symbol index) admits it as located advisories. Every other route refuses the service as `body_lowering_reason_service_realization_unreachable`, with each member's own located diagnostic pending, so no stage that would act on a service can assume a modifier's absence or a default transport.", + "RELATED FRONTIER ON THE SAME LOWERING: a service's name is lowered as ONE label with the whole declared spelling (`shell.Find`) rather than a per-segment spine, because sixteen services in `dag/extdeps/shell.dag` share the `shell` prefix and a spine needs a prefix merge in `v2.compiler.namespace_graft`. The dotted label hides named parts (DESIGN section 2) and a dotted reference to it does not resolve. TRIGGER: v2 resolving service CALLS; then the spine plus a graft prefix merge is the required shape, and the label migrates in one transition.", + "RUNG FOUND AT: mitigated (typed, located, counted set-aside; full normalize refuses). CEILING: structurally impossible -- each modifier and wire-key is a field of a typed operation carrier, so a lowered operation cannot exist without them. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: a typed operation-modifier carrier (and io wire-key / default carrier) in the v2 interface model that `body_lower_service_decl` lowers onto, so that every interface member of every service in the corpus lowers with the interface and `body_lowering_reason_interface_member_unmodeled` has no producer.", + ], + + evidence: [ + DeclarationRef { module_path: "v2.test.claim.normalize.service_declaration_lowering", decl_name: "census_counts_realization_and_unmodeled_interface_apart_holds", field: WholeDeclaration }, + DeclarationRef { module_path: "v2.test.claim.normalize.service_declaration_lowering", decl_name: "full_door_refuses_a_service_with_a_set_aside_realization_holds", field: WholeDeclaration }, + ], +} diff --git a/src/v2/compiler/03_normalize.dag b/src/v2/compiler/03_normalize.dag index b3de8a16fce..5cbbb420a1e 100644 --- a/src/v2/compiler/03_normalize.dag +++ b/src/v2/compiler/03_normalize.dag @@ -50,6 +50,7 @@ import v2.compiler.body_lowering_fold { no_wrapper_retentions, wrapper_retentions_concat, body_lower_fn_decl_to_census_arrow, + body_lower_service_decl, body_lower_is_deferred_lower_at_normalize, body_lower_type_decl_record_name_optional } @@ -463,6 +464,11 @@ fn normalize_census_node(n: Node, under_fn_decl: Bool) -> Outcome if (emitted == ^dag_surface_data_decl) || (emitted == ^dag_surface_test_fn_decl) { Accepted { value: body_lowered_tree_over(node: n, retained: no_wrapper_retentions()), diagnostics: None } + } else if emitted == ^dag_surface_service_decl { + body_lowered_tree_then( + t: body_lowered_tree_over(node: n, retained: no_wrapper_retentions()), + f: fn(shell) { body_lower_service_decl(shell: shell, admit_set_aside: true) } + ) } else if emitted == ^dag_surface_fn_decl { match normalize_census_fold_children(n: n, under_fn_decl: true) { Rejected { diagnostics: r } => Rejected { diagnostics: r } diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index 950d0bf2b00..dcc8ddebb3f 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -52,7 +52,7 @@ import v2.std.optional { optional_present } import v2.std.compilers.body_lowering { lower_binary_infix, lower_branch, lower_bind, lower_loop, lower_match, lower_unary_prefix } -import v2.std.compilers.lexing { symbol_lexeme } +import v2.std.compilers.lexing { symbol_intern_lexeme, symbol_lexeme } import v2.std.compilers.sugar { SugarSequencePair, sugar_sequence_pair_optional } import v2.std.grammar { GrammarExpr, GrammarExprFold, fold_grammar_expr } import v2.std.diagnostic { @@ -74,7 +74,7 @@ import v2.std.diagnostic { rejected_with_pending } import v2.std.integer { Int, integer_string_to_decimal_digits_optional } -import v2.std.qualified_name { QualifiedName, qualified_name_snoc, qualified_name_spine_node, qualified_name_spine_shape_present } +import v2.std.qualified_name { QualifiedName, qualified_name_snoc, qualified_name_to_dotted_string, qualified_name_spine_node, qualified_name_spine_shape_present } import v2.std.symbol_index { SymbolIndex, empty_symbol_index, @@ -88,6 +88,7 @@ import v2.std.node { ComputationNode, Conj, Edge, + EdgeLabel, Instantiation, Named, Node, @@ -585,8 +586,12 @@ fn body_lower_is_deferred_lower_emitted(emitted: Symbol) -> Bool { || (emitted == ^dag_surface_match_arm_stmt_body) } +// A service declaration lowers from its UNFOLDED parse subtree (body_lower_service_decl): its members +// are interface or set-aside facts, never bodies, so folding them first would retain each one. fn body_lower_is_deferred_lower_at_normalize(emitted: Symbol, under_fn_decl: Bool) -> Bool { - if emitted == ^dag_surface_fn_body { + if emitted == ^dag_surface_service_decl { + true + } else if emitted == ^dag_surface_fn_body { under_fn_decl } else { body_lower_is_deferred_lower_emitted(emitted: emitted) @@ -7332,8 +7337,381 @@ fn body_lower_type_decl_by_body(shell: Node, captured: Node, type_name: Symbol, } } +// SERVICE DECLARATIONS, AT DECLARATION GRADE. A service is an interface (DESIGN section 3): a named +// product of function types. `service S { operation Op { input { a: A } output { b: B } } }` lowers +// onto existing connectives only -- `Conj { S: Conj { Op: Arrow(Conj { a: A }, Conj { b: B }) } }`, +// the same Arrow a function TYPE lowers to (body_lower_fn_type_lowered_optional) -- so the graft +// flattens it like a record and the symbol index sees the service, its operations and their +// payload fields. No new node kind (ruling on gunbc adhoc-c537b0c6-a6e: no v2.std.node change, and +// node_minimal's TransportSlot is NOT promoted: transport is a realization handler, not a node field). +// +// WHAT IS NOT LOWERED IS SET ASIDE, TYPED AND LOCATED, UNDER TWO COUNTED REASONS: +// - body_lowering_reason_realization_member_set_aside: transport / config / exit / response / +// mock_response. config binds the transport's endpoint and authentication. +// Realization per DESIGN section 3, never part of the interface, so the interface lowers without it. +// - body_lowering_reason_interface_member_unmodeled: readonly / idempotent / hermetic and an io +// field's `from "key"` / `= default` tail. These ARE interface facts (idempotent governs retry, +// DESIGN section 4b) with no typed carrier yet, so they rank for modeling under their own reason +// instead of hiding in the realization bucket (rfm service_interface_member_has_no_carrier). +// A set-aside member is UNREACHABLE AS LOWERED: only census grade admits them (v2.compiler.normalize +// normalize_census, whose carrier has no route to eval or emission); every other route refuses the +// service (body_lower_service_decl), so no stage that would need a service's realization can be +// handed a service and assume a transport. The set-aside population is a typed list the lowering +// builds, never a scan of diagnostic reasons. A member this lowering cannot read refuses, located. +// +// THE NAME IS ONE LABEL, THE WHOLE DECLARED SPELLING (`shell.Find`), not a per-segment spine. A +// spine collides at the module (dag/extdeps/shell.dag declares sixteen services under `shell`) and +// needs a prefix merge in namespace_graft. FRONTIER, stated: the dotted label hides named parts +// (DESIGN section 2) and a dotted reference to it does not resolve; the trigger is v2 resolving +// service CALLS, at which point the spine plus a graft prefix merge becomes the required shape and +// this label migrates in one transition. +type ServiceSetAsideKind + = RealizationMemberSetAside + | InterfaceMemberUnmodeled + +type ServiceSetAside { + kind: ServiceSetAsideKind, + member: Node, +} + +fn service_set_aside_reason(kind: ServiceSetAsideKind) -> Symbol { + match kind { + RealizationMemberSetAside => ^body_lowering_reason_realization_member_set_aside + InterfaceMemberUnmodeled => ^body_lowering_reason_interface_member_unmodeled + } +} + +fn service_set_aside_diagnostics(xs: List) -> Diagnostics { + fold(xs, init: None, f: fn(acc, x) { + diagnostics_merge( + outer: acc, + inner: Some { diagnostics: diagnostics_singleton(d: body_lower_diagnostic(reason: service_set_aside_reason(kind: x.kind), n: x.member)) } + ) + }) +} + +// The production shells directly beneath a node, looking through the two choice wrappers +// (service_body_entry, operation_body_entry) that carry no fact of their own. +fn body_lower_service_member_shells(root: Node) -> List { + fold(root.children, init: Empty, f: fn(acc, e) { + list_append(left: acc, right: body_lower_service_member_shells_at(node: e.target)) + }) +} + +fn body_lower_service_member_shells_at(node: Node) -> List { + match parse_production_emitted_identity_optional(node: node) { + Present { value: id } => + if (id == ^dag_surface_service_body_entry) || (id == ^dag_surface_operation_body_entry) { + match parse_production_captured_child_optional(node: node) { + Present { value: inner } => body_lower_service_member_shells_at(node: inner) + Absent => [node] + } + } else { + [node] + } + Absent => body_lower_service_member_shells(root: node) + } +} + +// An io field's `[from "key"] [= default]` tail is present when either optional captured anything. +fn body_lower_io_field_tail_present(tail: Node) -> Bool { + match sugar_sequence_pair_optional(node: tail) { + Absent => (is_empty_conj_root(n: tail) == false) + Present { value: pair } => + (is_empty_conj_root(n: pair.left) == false) || (is_empty_conj_root(n: pair.right) == false) + } +} + + +type BodyLowerIoBlock { + payload: Node, + set_aside: List, +} + +fn body_lower_io_block(shell: Node) -> Outcome { + let refuse = outcome_rejected( + d: body_lower_diagnostic(reason: ^body_lowering_reason_field_decl_unlowered, n: shell) + ) + match parse_production_captured_child_optional(node: shell) { + Absent => refuse + Present { value: captured } => + match sugar_sequence_pair_optional(node: 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 + } + } + } + } + } + } + }) { + Absent => refuse + Present { value: a } => + outcome_accepted( + value: BodyLowerIoBlock { payload: body_lower_payload_node(edges: a.edges, source: shell), set_aside: a.set_aside } + ) + } + } + } + } + } + } +} + +type IoAcc { + edges: List, + set_aside: List, +} + +type BodyLowerOperationAcc { + input: Optional, + output: Optional, + set_aside: List, +} + +fn body_lower_operation_member(acc: Outcome, member: Node) -> Outcome { + bind_outcome(o: acc, f: fn(a) { + match parse_production_emitted_identity_optional(node: member) { + Absent => body_lower_service_member_unread(member: member) + Present { value: id } => + if (id == ^dag_surface_input_block) || (id == ^dag_surface_output_block) { + bind_outcome(o: body_lower_io_block(shell: member), f: fn(io) { + if id == ^dag_surface_input_block { + match a.input { + Present { value: _ } => + outcome_rejected(d: body_lower_diagnostic(reason: ^body_lowering_reason_service_io_block_repeated, n: member)) + Absent => + outcome_accepted(value: BodyLowerOperationAcc { input: Present { value: io.payload }, output: a.output, set_aside: list_append(left: a.set_aside, right: io.set_aside) }) + } + } else { + match a.output { + Present { value: _ } => + outcome_rejected(d: body_lower_diagnostic(reason: ^body_lowering_reason_service_io_block_repeated, n: member)) + Absent => + outcome_accepted(value: BodyLowerOperationAcc { input: a.input, output: Present { value: io.payload }, set_aside: list_append(left: a.set_aside, right: io.set_aside) }) + } + } + }) + } else if id == ^dag_surface_op_modifier { + body_lower_operation_acc_set_aside(a: a, kind: InterfaceMemberUnmodeled, n: member) + } else if (id == ^dag_surface_transport) + || (id == ^dag_surface_exit_block) + || (id == ^dag_surface_response_block) + || (id == ^dag_surface_mock_response_block) { + body_lower_operation_acc_set_aside(a: a, kind: RealizationMemberSetAside, n: member) + } else { + body_lower_service_member_unread(member: member) + } + } + }) +} + +fn body_lower_operation_acc_set_aside(a: BodyLowerOperationAcc, kind: ServiceSetAsideKind, n: Node) -> Outcome { + outcome_accepted( + value: BodyLowerOperationAcc { + input: a.input, + output: a.output, + set_aside: list_snoc_item(xs: a.set_aside, item: ServiceSetAside { kind: kind, member: n }) + } + ) +} + +// A member this lowering cannot read REFUSES, located at the member: a partially read service has +// no representation, so nothing is kept and nothing is dropped. +fn body_lower_service_member_unread(member: Node) -> Outcome { + outcome_rejected(d: body_lower_diagnostic(reason: ^body_lowering_reason_service_member_unread, n: member)) +} + +// An operation that declares no input (or no output) block exchanges the empty product. +fn body_lower_io_payload_or_empty(o: Optional, source: Node) -> Node { + match o { + Present { value: payload } => payload + Absent => body_lower_payload_node(edges: Empty, source: source) + } +} + +type BodyLowerOperation { + edge: Edge, + set_aside: List, +} + +fn body_lower_operation(shell: Node) -> Outcome { + match parse_production_captured_child_optional(node: shell) { + Absent => body_lower_service_member_unread(member: shell) + Present { value: captured } => + match dag_surface_kw_then_ident_from_captured(captured: captured) { + Absent => body_lower_service_member_unread(member: shell) + Present { value: op_name } => + bind_outcome( + o: fold( + body_lower_service_member_shells(root: captured), + init: outcome_accepted(value: BodyLowerOperationAcc { input: Absent, output: Absent, set_aside: Empty }), + f: fn(acc, member) { body_lower_operation_member(acc: acc, member: member) } + ), + f: fn(a) { + outcome_accepted( + value: BodyLowerOperation { + edge: Edge { + label: Named { name: op_name }, + target: node_with_occurrence_id( + kind: TypeNode { connective: Arrow }, + children: [ + Edge { label: Positional, target: body_lower_io_payload_or_empty(o: a.input, source: shell) }, + Edge { label: Positional, target: body_lower_io_payload_or_empty(o: a.output, source: shell) } + ], + occurrence_id: shell.occurrence_id + ) + }, + set_aside: a.set_aside + } + ) + } + ) + } + } +} + +type BodyLowerServiceAcc { + operations: List, + set_aside: List, +} + +fn body_lower_service_member(acc: Outcome, member: Node) -> Outcome { + bind_outcome(o: acc, f: fn(a) { + match parse_production_emitted_identity_optional(node: member) { + Absent => body_lower_service_member_unread(member: member) + Present { value: id } => + if id == ^dag_surface_qualified_name { + outcome_accepted(value: a) + } else if id == ^dag_surface_operation { + bind_outcome(o: body_lower_operation(shell: member), f: fn(op) { + if body_lower_service_declares_operation(operations: a.operations, name: op.edge.label) { + outcome_rejected(d: body_lower_diagnostic(reason: ^body_lowering_reason_service_operation_repeated, n: member)) + } else { + outcome_accepted(value: BodyLowerServiceAcc { operations: list_snoc_item(xs: a.operations, item: op.edge), set_aside: list_append(left: a.set_aside, right: op.set_aside) }) + } + }) + } else if (id == ^dag_surface_transport) || (id == ^dag_surface_service_config) { + outcome_accepted( + value: BodyLowerServiceAcc { + operations: a.operations, + set_aside: list_snoc_item(xs: a.set_aside, item: ServiceSetAside { kind: RealizationMemberSetAside, member: member }) + } + ) + } else { + body_lower_service_member_unread(member: member) + } + } + }) +} + +// AN OPERATION NAME IS DECLARED ONCE PER SERVICE. Two operations sharing a name would lower to two +// identically labelled edges of one product, which is not a well-formed node and names no single +// function type; the refusal is located at the second declaration, with its own cause, rather than +// surfacing later as an unlocated post-normalize well-formedness failure at the module root. +fn body_lower_service_declares_operation(operations: List, name: EdgeLabel) -> Bool { + fold(operations, init: false, f: fn(found, e) { found || (e.label == name) }) +} + +fn body_lower_service_name_optional(captured: Node) -> Optional { + match body_lower_find_production_shell_optional(root: captured, emitted: ^dag_surface_qualified_name) { + Absent => Absent + Present { value: qn_shell } => + match parse_production_captured_child_optional(node: qn_shell) { + Absent => Absent + Present { value: qn_capture } => + match parse_qualified_name_segments_from_capture(qn_capture: qn_capture, root: qn_shell) { + Rejected { diagnostics: _ } => Absent + Accepted { value: segments, diagnostics: _ } => + optional_present(value: symbol_intern_lexeme(lexeme: qualified_name_to_dotted_string(qn: segments))) + } + } + } +} + +// THE TWO GRADES DIFFER ONLY IN WHAT A SET-ASIDE MEMBER MEANS. At census grade +// (v2.compiler.normalize normalize_census, whose carrier has no route past the symbol index) the +// interface is admitted and each set-aside member rides as a located advisory. On every other route +// the lowered service would reach resolution, eval or emission, each of which would have to assume +// the set-aside facts, so it REFUSES at the service with every member's own located diagnostic +// pending: a set-aside realization is unreachable as lowered by construction, not by a door that +// scans reasons. +fn body_lower_service_decl(shell: Node, admit_set_aside: Bool) -> Outcome { + match parse_production_captured_child_optional(node: shell) { + Absent => body_lower_wrapper_retained_shell(shell: shell) + Present { value: captured } => + match body_lower_service_name_optional(captured: captured) { + Absent => body_lower_wrapper_retained_shell(shell: shell) + Present { value: service_name } => + match fold( + body_lower_service_member_shells(root: captured), + init: outcome_accepted(value: BodyLowerServiceAcc { operations: Empty, set_aside: Empty }), + f: fn(acc, member) { body_lower_service_member(acc: acc, member: member) } + ) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: a, diagnostics: _ } => + match a.set_aside { + Empty => body_lowering_lowered(o: outcome_accepted(value: body_lower_service_node(shell: shell, service_name: service_name, operations: a.operations))) + Cons { head: _, tail: _ } => + if admit_set_aside { + Accepted { + value: BodyLowered { node: body_lower_service_node(shell: shell, service_name: service_name, operations: a.operations) }, + diagnostics: service_set_aside_diagnostics(xs: a.set_aside) + } + } else { + Rejected { + diagnostics: rejected_with_pending( + pending: service_set_aside_diagnostics(xs: a.set_aside), + rejected: diagnostics_singleton(d: body_lower_diagnostic(reason: ^body_lowering_reason_service_realization_unreachable, n: shell)) + ) + } + } + } + } + } + } +} + +fn body_lower_service_node(shell: Node, service_name: Symbol, operations: List) -> Node { + node_with_occurrence_id( + kind: TypeNode { connective: Conj }, + children: [ + Edge { label: Named { name: service_name }, target: body_lower_payload_node(edges: operations, source: shell) } + ], + occurrence_id: shell.occurrence_id + ) +} + fn body_lower_production_emitted(shell: Node, emitted: Symbol, fn_body_pass_through: Bool) -> Outcome { - if emitted == ^dag_surface_type_decl { + if emitted == ^dag_surface_service_decl { + body_lower_service_decl(shell: shell, admit_set_aside: false) + } else if emitted == ^dag_surface_type_decl { body_lowering_lowered(o: body_lower_type_decl(shell: shell)) } else if body_lower_is_metadata_preserved_emitted(emitted: emitted) { body_lowering_lowered(o: outcome_accepted(value: shell)) diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index e710ccc2762..bb3bfc1201a 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -2043,11 +2043,10 @@ fn dag_grammar_test_fn_decl_expr() -> GrammarExpr { // general type fields do not (see dag_grammar_io_field_decl_expr). // Service-level `config` follows v1 parse_service_config_block: `config` `{` (field `:` expr)* `}` // over v1's closed field set. The v2-inline operation form is v1-admitted and not yet in this -// nest — named remainder, not a silent drop. Parsing is not lowering: dag_surface_service_decl has -// no body-lowering producer, so it lands on the existing lowered | wrapper-retained frontier -// (body_lowering_fold body_lower_wrapper_retained_shell, counted by body_lowering_retention_census) -// and normalized_tree admit_normalized_tree refuses it before resolve. The refusal moved from parse -// to normalize; it did not disappear. +// nest — named remainder, not a silent drop. Parsing is not lowering: dag_surface_service_decl lowers +// at declaration grade in v2.compiler.body_lowering_fold body_lower_service_decl, which reads the +// interface and sets realization (transport, config, exit, response, mock_response) and unmodeled +// interface members aside, typed and located. fn dag_grammar_status_pattern_expr() -> GrammarExpr { dag_grammar_choice( left: dag_grammar_terminal(token_class: ^dag_token_int_literal), @@ -2081,12 +2080,14 @@ fn dag_grammar_status_expr_entry_expr() -> GrammarExpr { ) } -fn dag_grammar_io_block_expr() -> GrammarExpr { +// `input` and `output` are TWO productions, not one io_block over a choice of the two words. A +// literal terminal is captured by its token class alone (v2.std.grammar StampLexeme keeps no +// lexeme), so a single io_block's parse tree could not say which word it matched, and the +// declaration-grade lowering (v2.compiler.body_lowering_fold body_lower_service_decl) needs exactly +// that fact to build an operation's domain and codomain. The production identity carries it. +fn dag_grammar_io_block_expr(keyword: String) -> GrammarExpr { dag_grammar_sequence( - left: dag_grammar_choice( - left: dag_grammar_literal_terminal(token_class: ^dag_token_ident, lexeme: symbol_intern_lexeme(lexeme: "input")), - right: dag_grammar_literal_terminal(token_class: ^dag_token_ident, lexeme: symbol_intern_lexeme(lexeme: "output")) - ), + left: dag_grammar_literal_terminal(token_class: ^dag_token_ident, lexeme: symbol_intern_lexeme(lexeme: keyword)), right: dag_grammar_sequence( left: dag_grammar_terminal(token_class: ^dag_token_lbrace), right: dag_grammar_sequence( @@ -2172,16 +2173,19 @@ fn dag_grammar_mock_response_block_expr() -> GrammarExpr { fn dag_grammar_operation_body_entry_expr() -> GrammarExpr { dag_grammar_choice( - left: dag_grammar_nonterminal(production: ^dag_production_io_block), + left: dag_grammar_nonterminal(production: ^dag_production_input_block), right: dag_grammar_choice( - left: dag_grammar_nonterminal(production: ^dag_production_op_modifier), + left: dag_grammar_nonterminal(production: ^dag_production_output_block), right: dag_grammar_choice( - left: dag_grammar_nonterminal(production: ^dag_production_transport), + left: dag_grammar_nonterminal(production: ^dag_production_op_modifier), right: dag_grammar_choice( - left: dag_grammar_nonterminal(production: ^dag_production_exit_block), + left: dag_grammar_nonterminal(production: ^dag_production_transport), right: dag_grammar_choice( - left: dag_grammar_nonterminal(production: ^dag_production_response_block), - right: dag_grammar_nonterminal(production: ^dag_production_mock_response_block) + left: dag_grammar_nonterminal(production: ^dag_production_exit_block), + right: dag_grammar_choice( + left: dag_grammar_nonterminal(production: ^dag_production_response_block), + right: dag_grammar_nonterminal(production: ^dag_production_mock_response_block) + ) ) ) ) @@ -2583,10 +2587,15 @@ fn dag_grammar_root() -> GrammarRoot { expression: dag_grammar_transport_expr(), emitted: ^dag_surface_transport ) - let io_block = dag_grammar_production( - name: ^dag_production_io_block, - expression: dag_grammar_io_block_expr(), - emitted: ^dag_surface_io_block + let input_block = dag_grammar_production( + name: ^dag_production_input_block, + expression: dag_grammar_io_block_expr(keyword: "input"), + emitted: ^dag_surface_input_block + ) + let output_block = dag_grammar_production( + name: ^dag_production_output_block, + expression: dag_grammar_io_block_expr(keyword: "output"), + emitted: ^dag_surface_output_block ) let op_modifier = dag_grammar_production( name: ^dag_production_op_modifier, @@ -2822,7 +2831,8 @@ fn dag_grammar_root() -> GrammarRoot { operation, operation_body_entry, transport, - io_block, + input_block, + output_block, op_modifier, exit_block, response_block, diff --git a/src/v2/test/claim/normalize/service_declaration_lowering_test.dag b/src/v2/test/claim/normalize/service_declaration_lowering_test.dag new file mode 100644 index 00000000000..97c4f9803da --- /dev/null +++ b/src/v2/test/claim/normalize/service_declaration_lowering_test.dag @@ -0,0 +1,255 @@ +module v2.test.claim.normalize.service_declaration_lowering + +import v2.compiler.normalize { normalize, normalize_census } +import v2.compiler.normalized_tree { CensusTree, NormalizedTree, census_tree_binding_source } +import v2.compiler.symbol_index_fill { symbol_index_fill_module_roots } +import v2.test.parse.supplied_token_stream_support { supplied_stream_matches_tokenize, supplied_stream_parse } +import v2.std.compilers.lexing { Token, TokenStream, symbol_intern_lexeme, token_stream_new } +import v2.std.symbol_index { SymbolIndex, empty_symbol_index, symbol_index_lookup } +import v2.std.diagnostic { Accepted, Diagnostic, Diagnostics, NonEmptyDiagnostics, Outcome, Rejected, Some, bind_outcome, diagnostics_fatal_reason } +import v2.std.algebra { fold_list } +import v2.std.optional { Absent, Present } +import v2.std.logic { Bool } +import v2.std.integer { Int } +import v2.std.node { Node, Symbol } +import v2.std.text { String } +import std.algebra { Cons, Empty, FreeMonoid } + +// DECLARATION-GRADE SERVICE LOWERING (v2.compiler.body_lowering_fold body_lower_service_decl). A +// service lowers to its interface -- a named product of function types -- and sets its realization +// members and its unmodeled interface members aside under two counted reasons. Census grade admits a +// service module; the full normalize route, whose product reaches resolution, eval and emission, +// refuses it, so no consumer can act on a set-aside realization. The fixture is the smallest service +// carrying one member of each kind in dag/extdeps/access/posix_effective_principal_read_op.dag, the +// first gen-two census wall: that file itself is admitted by the census run over the real tree, not +// here, where parsing all of it would cost the claim its enrolment budget (DESIGN section 3). + +data posix_service_source: String = "module m\nservice a.P {\n operation R {\n output { o: Int = 1 }\n readonly\n transport t {}\n }\n}\n" + +// Two services sharing a prefix in one module, the dag/extdeps/shell.dag shape: each is +// ONE label spelled as declared, so they stay distinct at the module. +data shared_prefix_source: String = "module m\nservice s.F {\n operation R {}\n}\nservice s.E {\n operation R {}\n}\n" + +// The discriminating RED: an operation declaring its input twice has no single domain, so the +// lowering refuses located at the second block rather than keeping either. +data repeated_input_source: String = "module m\nservice S {\n operation Op {\n input { x: Int }\n input { y: Int }\n }\n}\n" + +// SUPPLIED TOKEN STREAMS (the v2.test.parse.supplied_token_stream_support pattern): tokenizing each +// fixture inside every claim put the claims over the new-witness eval-step budget. Each stream is +// paired below with a fidelity claim that the real tokenizer produces exactly it, so supplying it +// removes no execution of the real path (DESIGN section 3 pairing obligation). +fn census_of_stream(tokens: TokenStream) -> Outcome { + bind_outcome(o: supplied_stream_parse(tokens: tokens), f: fn(tree) { normalize_census(parse_tree: tree) }) +} + +fn full_of_stream(tokens: TokenStream) -> Outcome { + bind_outcome(o: supplied_stream_parse(tokens: tokens), f: fn(tree) { normalize(parse_tree: tree) }) +} + +// The dag/extdeps/bmc/http.dag shape the first full gen-two census found: one service declaring an +// operation name twice. It refuses located at the second declaration with its own cause, which is +// what distinguishes it from the set-aside members that ride beside it and are admitted. +data repeated_operation_source: String = "module m\nservice S {\n operation Op {}\n operation Op {}\n}\n" + +fn index_of(t: CensusTree) -> SymbolIndex { + symbol_index_fill_module_roots(index: empty_symbol_index(), roots: Cons { head: census_tree_binding_source(t: t), tail: Empty }) +} + +fn declares(index: SymbolIndex, service: String, member: String) -> Bool { + match symbol_index_lookup( + index: index, + qualified_path: Cons { head: ^m, tail: Cons { head: symbol_intern_lexeme(lexeme: service), tail: Cons { head: symbol_intern_lexeme(lexeme: member), tail: Empty } } } + ) { + Absent => false + Present { value: _ } => true + } +} + +fn reason_count(d: Diagnostics, reason: Symbol) -> Int { + match d { + None => 0 + Some { diagnostics: ne } => + fold_list( + xs: ne.tail, + empty: if ne.head.reason == reason { 1 } else { 0 }, + cons: fn(n, x) { if x.reason == reason { n + 1 } else { n } } + ) + } +} + +fn carries(ne: NonEmptyDiagnostics, reason: Symbol) -> Bool { + reason_count(d: Some { diagnostics: ne }, reason: reason) > 0 +} + +test fn census_admits_the_posix_service_and_indexes_its_operation_holds() -> Bool { + match census_of_stream(tokens: posix_service_stream()) { + Rejected { diagnostics: _ } => false + Accepted { value: t, diagnostics: _ } => + declares(index: index_of(t: t), service: "a.P", member: "R") + } +} + +// Both set-aside reasons, counted apart: the transport is realization (1); the readonly modifier and +// the io field's default tail are unmodeled interface facts (2). +test fn census_counts_realization_and_unmodeled_interface_apart_holds() -> Bool { + match census_of_stream(tokens: posix_service_stream()) { + Rejected { diagnostics: _ } => false + Accepted { value: _, diagnostics: d } => + (reason_count(d: d, reason: ^body_lowering_reason_realization_member_set_aside) == 1) + && (reason_count(d: d, reason: ^body_lowering_reason_interface_member_unmodeled) == 2) + } +} + +// THE SET-ASIDE IS UNREACHABLE AS LOWERED. Full normalize is what resolution, eval and emission +// consume; it refuses the same service, and the pending diagnostics carry the set-aside members at +// their own loci, so no consumer is handed a service whose realization it would have to assume. +test fn full_door_refuses_a_service_with_a_set_aside_realization_holds() -> Bool { + match full_of_stream(tokens: posix_service_stream()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: r } => + carries(ne: r, reason: ^body_lowering_reason_service_realization_unreachable) + && carries(ne: r, reason: ^body_lowering_reason_realization_member_set_aside) + } +} + +test fn two_services_sharing_a_prefix_admit_with_distinct_labels_holds() -> Bool { + match census_of_stream(tokens: shared_prefix_stream()) { + Rejected { diagnostics: _ } => false + Accepted { value: t, diagnostics: _ } => + declares(index: index_of(t: t), service: "s.F", member: "R") + && declares(index: index_of(t: t), service: "s.E", member: "R") + } +} + +test fn a_repeated_input_block_refuses_located_RED() -> Bool { + match census_of_stream(tokens: repeated_input_stream()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: r } => diagnostics_fatal_reason(d: r) == ^body_lowering_reason_service_io_block_repeated + } +} + +fn posix_service_stream() -> TokenStream { + token_stream_new(all: [ + Token { class: ^dag_token_kw_module, lexeme: "module", file: ^service_lowering_probe, start: 0, end: 6 }, + Token { class: ^dag_token_ident, lexeme: "m", file: ^service_lowering_probe, start: 7, end: 8 }, + Token { class: ^dag_token_ident, lexeme: "service", file: ^service_lowering_probe, start: 9, end: 16 }, + Token { class: ^dag_token_ident, lexeme: "a", file: ^service_lowering_probe, start: 17, end: 18 }, + Token { class: ^dag_token_dot, lexeme: ".", file: ^service_lowering_probe, start: 18, end: 19 }, + Token { class: ^dag_token_ident, lexeme: "P", file: ^service_lowering_probe, start: 19, end: 20 }, + Token { class: ^dag_token_lbrace, lexeme: "{", file: ^service_lowering_probe, start: 21, end: 22 }, + Token { class: ^dag_token_ident, lexeme: "operation", file: ^service_lowering_probe, start: 25, end: 34 }, + Token { class: ^dag_token_ident, lexeme: "R", file: ^service_lowering_probe, start: 35, end: 36 }, + Token { class: ^dag_token_lbrace, lexeme: "{", file: ^service_lowering_probe, start: 37, end: 38 }, + Token { class: ^dag_token_ident, lexeme: "output", file: ^service_lowering_probe, start: 43, end: 49 }, + Token { class: ^dag_token_lbrace, lexeme: "{", file: ^service_lowering_probe, start: 50, end: 51 }, + Token { class: ^dag_token_ident, lexeme: "o", file: ^service_lowering_probe, start: 52, end: 53 }, + Token { class: ^dag_token_colon, lexeme: ":", file: ^service_lowering_probe, start: 53, end: 54 }, + Token { class: ^dag_token_ident, lexeme: "Int", file: ^service_lowering_probe, start: 55, end: 58 }, + Token { class: ^dag_token_eq, lexeme: "=", file: ^service_lowering_probe, start: 59, end: 60 }, + Token { class: ^dag_token_int_literal, lexeme: "1", file: ^service_lowering_probe, start: 61, end: 62 }, + Token { class: ^dag_token_rbrace, lexeme: "}", file: ^service_lowering_probe, start: 63, end: 64 }, + Token { class: ^dag_token_ident, lexeme: "readonly", file: ^service_lowering_probe, start: 69, end: 77 }, + Token { class: ^dag_token_ident, lexeme: "transport", file: ^service_lowering_probe, start: 82, end: 91 }, + Token { class: ^dag_token_ident, lexeme: "t", file: ^service_lowering_probe, start: 92, end: 93 }, + Token { class: ^dag_token_lbrace, lexeme: "{", file: ^service_lowering_probe, start: 94, end: 95 }, + Token { class: ^dag_token_rbrace, lexeme: "}", file: ^service_lowering_probe, start: 95, end: 96 }, + Token { class: ^dag_token_rbrace, lexeme: "}", file: ^service_lowering_probe, start: 99, end: 100 }, + Token { class: ^dag_token_rbrace, lexeme: "}", file: ^service_lowering_probe, start: 101, end: 102 } + ]) +} + +test fn posix_service_stream_is_what_tokenize_produces_holds() -> Bool { + supplied_stream_matches_tokenize(text: posix_service_source, file: ^service_lowering_probe, supplied: posix_service_stream()) +} + +fn shared_prefix_stream() -> TokenStream { + token_stream_new(all: [ + Token { class: ^dag_token_kw_module, lexeme: "module", file: ^service_lowering_probe, start: 0, end: 6 }, + Token { class: ^dag_token_ident, lexeme: "m", file: ^service_lowering_probe, start: 7, end: 8 }, + Token { class: ^dag_token_ident, lexeme: "service", file: ^service_lowering_probe, start: 9, end: 16 }, + Token { class: ^dag_token_ident, lexeme: "s", file: ^service_lowering_probe, start: 17, end: 18 }, + Token { class: ^dag_token_dot, lexeme: ".", file: ^service_lowering_probe, start: 18, end: 19 }, + Token { class: ^dag_token_ident, lexeme: "F", file: ^service_lowering_probe, start: 19, end: 20 }, + Token { class: ^dag_token_lbrace, lexeme: "{", file: ^service_lowering_probe, start: 21, end: 22 }, + Token { class: ^dag_token_ident, lexeme: "operation", file: ^service_lowering_probe, start: 25, end: 34 }, + Token { class: ^dag_token_ident, lexeme: "R", file: ^service_lowering_probe, start: 35, end: 36 }, + Token { class: ^dag_token_lbrace, lexeme: "{", file: ^service_lowering_probe, start: 37, end: 38 }, + Token { class: ^dag_token_rbrace, lexeme: "}", file: ^service_lowering_probe, start: 38, end: 39 }, + Token { class: ^dag_token_rbrace, lexeme: "}", file: ^service_lowering_probe, start: 40, end: 41 }, + Token { class: ^dag_token_ident, lexeme: "service", file: ^service_lowering_probe, start: 42, end: 49 }, + Token { class: ^dag_token_ident, lexeme: "s", file: ^service_lowering_probe, start: 50, end: 51 }, + Token { class: ^dag_token_dot, lexeme: ".", file: ^service_lowering_probe, start: 51, end: 52 }, + Token { class: ^dag_token_ident, lexeme: "E", file: ^service_lowering_probe, start: 52, end: 53 }, + Token { class: ^dag_token_lbrace, lexeme: "{", file: ^service_lowering_probe, start: 54, end: 55 }, + Token { class: ^dag_token_ident, lexeme: "operation", file: ^service_lowering_probe, start: 58, end: 67 }, + Token { class: ^dag_token_ident, lexeme: "R", file: ^service_lowering_probe, start: 68, end: 69 }, + Token { class: ^dag_token_lbrace, lexeme: "{", file: ^service_lowering_probe, start: 70, end: 71 }, + Token { class: ^dag_token_rbrace, lexeme: "}", file: ^service_lowering_probe, start: 71, end: 72 }, + Token { class: ^dag_token_rbrace, lexeme: "}", file: ^service_lowering_probe, start: 73, end: 74 } + ]) +} + +test fn shared_prefix_stream_is_what_tokenize_produces_holds() -> Bool { + supplied_stream_matches_tokenize(text: shared_prefix_source, file: ^service_lowering_probe, supplied: shared_prefix_stream()) +} + +fn repeated_input_stream() -> TokenStream { + token_stream_new(all: [ + Token { class: ^dag_token_kw_module, lexeme: "module", file: ^service_lowering_probe, start: 0, end: 6 }, + Token { class: ^dag_token_ident, lexeme: "m", file: ^service_lowering_probe, start: 7, end: 8 }, + Token { class: ^dag_token_ident, lexeme: "service", file: ^service_lowering_probe, start: 9, end: 16 }, + Token { class: ^dag_token_ident, lexeme: "S", file: ^service_lowering_probe, start: 17, end: 18 }, + Token { class: ^dag_token_lbrace, lexeme: "{", file: ^service_lowering_probe, start: 19, end: 20 }, + Token { class: ^dag_token_ident, lexeme: "operation", file: ^service_lowering_probe, start: 23, end: 32 }, + Token { class: ^dag_token_ident, lexeme: "Op", file: ^service_lowering_probe, start: 33, end: 35 }, + Token { class: ^dag_token_lbrace, lexeme: "{", file: ^service_lowering_probe, start: 36, end: 37 }, + Token { class: ^dag_token_ident, lexeme: "input", file: ^service_lowering_probe, start: 42, end: 47 }, + Token { class: ^dag_token_lbrace, lexeme: "{", file: ^service_lowering_probe, start: 48, end: 49 }, + Token { class: ^dag_token_ident, lexeme: "x", file: ^service_lowering_probe, start: 50, end: 51 }, + Token { class: ^dag_token_colon, lexeme: ":", file: ^service_lowering_probe, start: 51, end: 52 }, + Token { class: ^dag_token_ident, lexeme: "Int", file: ^service_lowering_probe, start: 53, end: 56 }, + Token { class: ^dag_token_rbrace, lexeme: "}", file: ^service_lowering_probe, start: 57, end: 58 }, + Token { class: ^dag_token_ident, lexeme: "input", file: ^service_lowering_probe, start: 63, end: 68 }, + Token { class: ^dag_token_lbrace, lexeme: "{", file: ^service_lowering_probe, start: 69, end: 70 }, + Token { class: ^dag_token_ident, lexeme: "y", file: ^service_lowering_probe, start: 71, end: 72 }, + Token { class: ^dag_token_colon, lexeme: ":", file: ^service_lowering_probe, start: 72, end: 73 }, + Token { class: ^dag_token_ident, lexeme: "Int", file: ^service_lowering_probe, start: 74, end: 77 }, + Token { class: ^dag_token_rbrace, lexeme: "}", file: ^service_lowering_probe, start: 78, end: 79 }, + Token { class: ^dag_token_rbrace, lexeme: "}", file: ^service_lowering_probe, start: 82, end: 83 }, + Token { class: ^dag_token_rbrace, lexeme: "}", file: ^service_lowering_probe, start: 84, end: 85 } + ]) +} + +test fn repeated_input_stream_is_what_tokenize_produces_holds() -> Bool { + supplied_stream_matches_tokenize(text: repeated_input_source, file: ^service_lowering_probe, supplied: repeated_input_stream()) +} + +test fn a_repeated_operation_name_refuses_located_RED() -> Bool { + match census_of_stream(tokens: repeated_operation_stream()) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: r } => diagnostics_fatal_reason(d: r) == ^body_lowering_reason_service_operation_repeated + } +} + +fn repeated_operation_stream() -> TokenStream { + token_stream_new(all: [ + Token { class: ^dag_token_kw_module, lexeme: "module", file: ^service_lowering_probe, start: 0, end: 6 }, + Token { class: ^dag_token_ident, lexeme: "m", file: ^service_lowering_probe, start: 7, end: 8 }, + Token { class: ^dag_token_ident, lexeme: "service", file: ^service_lowering_probe, start: 9, end: 16 }, + Token { class: ^dag_token_ident, lexeme: "S", file: ^service_lowering_probe, start: 17, end: 18 }, + Token { class: ^dag_token_lbrace, lexeme: "{", file: ^service_lowering_probe, start: 19, end: 20 }, + Token { class: ^dag_token_ident, lexeme: "operation", file: ^service_lowering_probe, start: 23, end: 32 }, + Token { class: ^dag_token_ident, lexeme: "Op", file: ^service_lowering_probe, start: 33, end: 35 }, + Token { class: ^dag_token_lbrace, lexeme: "{", file: ^service_lowering_probe, start: 36, end: 37 }, + Token { class: ^dag_token_rbrace, lexeme: "}", file: ^service_lowering_probe, start: 37, end: 38 }, + Token { class: ^dag_token_ident, lexeme: "operation", file: ^service_lowering_probe, start: 41, end: 50 }, + Token { class: ^dag_token_ident, lexeme: "Op", file: ^service_lowering_probe, start: 51, end: 53 }, + Token { class: ^dag_token_lbrace, lexeme: "{", file: ^service_lowering_probe, start: 54, end: 55 }, + Token { class: ^dag_token_rbrace, lexeme: "}", file: ^service_lowering_probe, start: 55, end: 56 }, + Token { class: ^dag_token_rbrace, lexeme: "}", file: ^service_lowering_probe, start: 57, end: 58 } + ]) +} + +test fn repeated_operation_stream_is_what_tokenize_produces_holds() -> Bool { + supplied_stream_matches_tokenize(text: repeated_operation_source, file: ^service_lowering_probe, supplied: repeated_operation_stream()) +} diff --git a/src/v2/test/claim/parse/g0_service_decl_parse_probe_test.dag b/src/v2/test/claim/parse/g0_service_decl_parse_probe_test.dag index c143b0c2b5f..c339476acd2 100644 --- a/src/v2/test/claim/parse/g0_service_decl_parse_probe_test.dag +++ b/src/v2/test/claim/parse/g0_service_decl_parse_probe_test.dag @@ -18,8 +18,8 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly // transport/config). The marker words are literal terminals over ident, not keyword classes. The first native fatal was leftover token `service` at // dag/extdeps/access/posix_effective_principal_read_op.dag. The family is the // nest below; service-level `config` is admitted over v1's closed field set, and the v2-inline -// operation form remains v1-admitted remainder. Parsing is not lowering: the route rows below pin that a parsed -// service is refused at the normalized-tree door. +// operation form remains v1-admitted remainder. Lowering is a separate stage: the route rows below +// pin what normalize does with a parsed service. data probe_cached_lm: LanguageModel = dag_language_model() @@ -92,9 +92,9 @@ data service_config_unknown_field_red_source: String = "module m\nservice S {\n data service_config_without_colon_red_source: String = "module m\nservice S {\n config {\n endpoint e\n }\n}\n" -// Parsing is not lowering: a service whose only entry is config still lands wrapper-retained and -// is refused at the normalized-tree door, so the config entry is carried to a typed refusal, never -// dropped on the way to resolve. +// A service whose only entry is config: config binds the transport (endpoint, auth), so it is +// realization, set aside, and full normalize refuses the service for it -- carried to a typed +// refusal, never dropped on the way to resolve. data config_only_service_source: String = "module m\nservice S { config {} }\n" data exit_entry_without_arrow_red_source: String = "module m\nservice S {\n operation Op {\n exit {\n 0 Unit\n }\n }\n}\n" @@ -223,28 +223,29 @@ test fn type_field_from_key_still_refuses_holds() -> Bool { refuses_at_parse(src: type_field_from_red_source) } -// ROUTE, not only parse. A service parses but has no body-lowering producer yet, so normalize must -// land it on the declared lowered | wrapper-retained frontier (body_lowering_fold -// body_lower_wrapper_retained_shell) and the normalized-tree door must REFUSE it with a typed reason -// (normalized_tree admit_normalized_tree) -- never admit an unlowered service shell to resolve. -// The type control is the positive half: the same door admits a module with nothing retained. A -// lowered type declaration is the cheapest lowered item; the fn control it replaced paid for -// lowering a fn body this claim never inspects and ran over the new-witness step budget. -fn refused_for_retention(ne: NonEmptyDiagnostics) -> Bool { +// ROUTE, not only parse. A service now LOWERS at declaration grade (v2.compiler.body_lowering_fold +// body_lower_service_decl): its interface onto existing connectives, its realization members +// (transport, config, exit, response, mock_response) set aside, typed and located. Full normalize is +// what resolution, eval and emission consume, so it refuses a service with a set-aside realization, +// for that reason rather than for retention; an empty service sets nothing aside and is admitted. +// This pair was the retention route before the lowering landed; the climb keeps it as the permanent +// control that normalize discriminates on what a service carries (DESIGN section 4b(4)). The type +// control is the positive half: a lowered type declaration is the cheapest lowered item. +fn refused_for_set_aside_realization(ne: NonEmptyDiagnostics) -> Bool { fold_list( xs: ne.tail, - empty: ne.head.reason == ^normalized_tree_reason_wrapper_retention_not_normalized, - cons: fn(found, d) { found || d.reason == ^normalized_tree_reason_wrapper_retention_not_normalized } + empty: ne.head.reason == ^body_lowering_reason_service_realization_unreachable, + cons: fn(found, d) { found || d.reason == ^body_lowering_reason_service_realization_unreachable } ) } -fn normalize_refuses_for_retention(src: String) -> Bool { +fn normalize_refuses_for_set_aside_realization(src: String) -> Bool { match parse_src(src: src) { Rejected { diagnostics: _ } => false Accepted { value: a, diagnostics: _ } => match normalize(parse_tree: a.tree) { Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: r } => refused_for_retention(ne: r) + Rejected { diagnostics: r } => refused_for_set_aside_realization(ne: r) } } } @@ -260,21 +261,26 @@ fn normalize_admits(src: String) -> Bool { } } -// The route needs only a service declaration, not a populated one: an empty service still emits -// dag_surface_service_decl with no lowering producer. The populated smallest_service fixture -// measured 396ms here against the 302ms enrolment margin (floor run 35440687934), paying to parse -// and normalize an operation this claim never inspects (DESIGN 3, a witness discriminates at one -// interface). +// Each route needs only a service declaration, not a populated one. The populated smallest_service +// fixture measured 396ms here against the 302ms enrolment margin (floor run 35440687934), paying to +// parse and normalize an operation this claim never inspects (DESIGN 3, a witness discriminates at +// one interface). data empty_service_source: String = "module m\nservice S {}\n" -test fn parsed_service_is_refused_at_the_normalized_tree_door_holds() -> Bool { - normalize_refuses_for_retention(src: empty_service_source) +data transport_only_service_source: String = "module m\nservice S {\n transport shell { argv: [] }\n}\n" + +test fn a_service_with_a_transport_is_refused_at_the_normalized_tree_door_holds() -> Bool { + normalize_refuses_for_set_aside_realization(src: transport_only_service_source) +} + +test fn an_empty_service_is_admitted_at_the_normalized_tree_door_holds() -> Bool { + normalize_admits(src: empty_service_source) } test fn type_control_is_admitted_at_the_normalized_tree_door_holds() -> Bool { normalize_admits(src: plain_type_source) } -test fn config_only_service_is_refused_at_the_normalized_tree_door_holds() -> Bool { - normalize_refuses_for_retention(src: config_only_service_source) +test fn config_only_service_is_refused_for_its_set_aside_realization_holds() -> Bool { + normalize_refuses_for_set_aside_realization(src: config_only_service_source) } diff --git a/src/v2/workflow/compile_door_cause_ownership.dag b/src/v2/workflow/compile_door_cause_ownership.dag index 2df7d29432f..bcad3f91fad 100644 --- a/src/v2/workflow/compile_door_cause_ownership.dag +++ b/src/v2/workflow/compile_door_cause_ownership.dag @@ -131,6 +131,12 @@ data known_frontier_causes: List = [ lane: SharedSelfHostCriticalPath, flip_trigger: "flips when a v2 stage lowers early exit (an else-less `if` statement) to control flow, a model-first package staffed once the workload shows it on its critical path; until then the statement refuses here rather than being dropped or rewritten (gunbc#11998 batch 2, ruling via neat-boar-16)" }, + CauseOwnership { + cause: ^body_lowering_reason_service_realization_unreachable, + grain: FatalGrain, + lane: SharedSelfHostCriticalPath, + flip_trigger: "a service declaration lowers at declaration grade (v2.compiler.body_lowering_fold body_lower_service_decl) and its realization and unmodeled interface members are set aside; full normalize refuses a closure member carrying any, because resolution, eval and emission would have to assume them. Flips when a realization binding carrier (a transport handler bound to the interface shape) and a typed operation-modifier carrier exist, so that no set-aside reason has a producer (gunbc.recurring_failure_mode service_interface_member_has_no_carrier)" + }, CauseOwnership { cause: ^body_lowering_reason_field_decl_unlowered, grain: FatalGrain,