From 5269897c5abdd32838843dcbe747b4bc8d7ce47a Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 25 Sep 2026 04:59:40 +0000 Subject: [PATCH 1/4] v2: declaration-grade lowering of service declarations (interface onto existing connectives; realization and unmodeled interface members set aside, counted; full door refuses) Co-Authored-By: Claude Opus 5.5 (1M context) --- ...ervice_interface_member_has_no_carrier.dag | 22 ++ src/v2/compiler/body_lowering_fold.dag | 341 +++++++++++++++++- src/v2/compiler/normalized_tree.dag | 32 +- src/v2/extdeps/languages/dag.dag | 51 +-- .../service_declaration_lowering_test.dag | 129 +++++++ .../g0_service_decl_parse_probe_test.dag | 45 ++- .../workflow/compile_door_cause_ownership.dag | 6 + 7 files changed, 581 insertions(+), 45 deletions(-) create mode 100644 dag/gunbc/recurring_failure_mode/service_interface_member_has_no_carrier.dag create mode 100644 src/v2/test/claim/normalize/service_declaration_lowering_test.dag 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..a121ee85eec --- /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, 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. Only the census door (`v2.compiler.normalized_tree` `admit_census_tree`, whose carrier has no route to eval or emission) admits the diagnostics; the full door (`admit_normalized_tree`) refuses a service carrying any of them as `normalized_tree_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 with a refusing full door). 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/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index ab5d68d86c1..6c6a3617645 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, @@ -479,8 +479,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) @@ -7034,8 +7038,337 @@ fn body_lower_type_decl(shell: Node) -> Outcome { } } +// 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 / exit / response / mock_response. +// 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 the census door admits these diagnostics +// (v2.compiler.normalized_tree admit_census_tree, whose carrier has no route to eval or emission); +// the full door refuses them (admit_normalized_tree), so no stage that would need a service's +// realization can be handed a service and assume a transport. Any member this lowering cannot read +// retains the shell, which both doors refuse. +// +// 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. +fn body_lower_service_set_aside(reason: Symbol, n: Node) -> Diagnostics { + Some { diagnostics: diagnostics_singleton(d: body_lower_diagnostic(reason: reason, n: n)) } +} + +// 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) + } +} + + +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, diagnostics: None } }, 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), + diagnostics: if body_lower_io_field_tail_present(tail: field.right) { + diagnostics_merge( + outer: a.diagnostics, + inner: body_lower_service_set_aside(reason: ^body_lowering_reason_interface_member_unmodeled, n: item) + ) + } else { + a.diagnostics + } + } + } + } + } + } + }) { + Absent => refuse + Present { value: a } => + outcome_with_diagnostics( + value: body_lower_payload_node(edges: a.edges, source: shell), + diagnostics: a.diagnostics + ) + } + } + } + } + } + } +} + +type IoAcc { + edges: List, + diagnostics: Diagnostics, +} + +type BodyLowerOperationAcc { + input: Optional, + output: Optional, + diagnostics: Diagnostics, +} + +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_wrapper_retained_shell_as(shell: 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 }, output: a.output, diagnostics: a.diagnostics }) + } + } 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 }, diagnostics: a.diagnostics }) + } + } + }) + } else if id == ^dag_surface_op_modifier { + body_lower_operation_acc_set_aside(a: a, reason: ^body_lowering_reason_interface_member_unmodeled, 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, reason: ^body_lowering_reason_realization_member_set_aside, n: member) + } else { + body_lower_wrapper_retained_shell_as(shell: member) + } + } + }) +} + +fn body_lower_operation_acc_set_aside(a: BodyLowerOperationAcc, reason: Symbol, n: Node) -> Outcome { + outcome_accepted( + value: BodyLowerOperationAcc { + input: a.input, + output: a.output, + diagnostics: diagnostics_merge(outer: a.diagnostics, inner: body_lower_service_set_aside(reason: reason, n: n)) + } + ) +} + +// A member this lowering cannot read retains, typed by the one retention producer, whatever the +// carrier type of the fold it interrupts: the retained shell's diagnostic rides a Rejected here +// because a partially read service has no representation. +fn body_lower_wrapper_retained_shell_as(shell: Node) -> Outcome { + outcome_rejected(d: body_lower_wrapper_retained_diagnostic(n: shell)) +} + +// 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) + } +} + +fn body_lower_operation(shell: Node) -> Outcome { + match parse_production_captured_child_optional(node: shell) { + Absent => body_lower_wrapper_retained_shell_as(shell: shell) + Present { value: captured } => + match dag_surface_kw_then_ident_from_captured(captured: captured) { + Absent => body_lower_wrapper_retained_shell_as(shell: 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, diagnostics: None }), + f: fn(acc, member) { body_lower_operation_member(acc: acc, member: member) } + ), + f: fn(a) { + outcome_with_diagnostics( + value: 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 + ) + }, + diagnostics: a.diagnostics + ) + } + ) + } + } +} + +type BodyLowerServiceAcc { + operations: List, + diagnostics: Diagnostics, +} + +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_wrapper_retained_shell_as(shell: 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) { + outcome_accepted(value: BodyLowerServiceAcc { operations: list_snoc_item(xs: a.operations, item: op), diagnostics: a.diagnostics }) + }) + } else if id == ^dag_surface_transport { + outcome_accepted( + value: BodyLowerServiceAcc { + operations: a.operations, + diagnostics: diagnostics_merge( + outer: a.diagnostics, + inner: body_lower_service_set_aside(reason: ^body_lowering_reason_realization_member_set_aside, n: member) + ) + } + ) + } else { + body_lower_wrapper_retained_shell_as(shell: member) + } + } + }) +} + +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))) + } + } + } +} + +fn body_lower_service_decl(shell: Node) -> 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 } => + bind_outcome( + o: fold( + body_lower_service_member_shells(root: captured), + init: outcome_accepted(value: BodyLowerServiceAcc { operations: Empty, diagnostics: None }), + f: fn(acc, member) { body_lower_service_member(acc: acc, member: member) } + ), + f: fn(a) { + outcome_with_diagnostics( + value: node_with_occurrence_id( + kind: TypeNode { connective: Conj }, + children: [ + Edge { label: Named { name: service_name }, target: body_lower_payload_node(edges: a.operations, source: shell) } + ], + occurrence_id: shell.occurrence_id + ), + diagnostics: a.diagnostics + ) + } + ) + } + } +} + +fn body_lowering_diagnostic_is_service_set_aside(d: Diagnostic) -> Bool { + (d.reason == ^body_lowering_reason_realization_member_set_aside) + || (d.reason == ^body_lowering_reason_interface_member_unmodeled) +} + +fn body_lowering_diagnostics_carry_service_set_aside(d: Diagnostics) -> Bool { + match d { + None => false + Some { diagnostics: ne } => + fold_list( + xs: ne.tail, + empty: body_lowering_diagnostic_is_service_set_aside(d: ne.head), + cons: fn(found, x) { found || body_lowering_diagnostic_is_service_set_aside(d: x) } + ) + } +} + 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) + } else if emitted == ^dag_surface_type_decl { body_lower_type_decl(shell: shell) } else if body_lower_is_metadata_preserved_emitted(emitted: emitted) { outcome_accepted(value: shell) diff --git a/src/v2/compiler/normalized_tree.dag b/src/v2/compiler/normalized_tree.dag index 22e53bcaa26..d4a24d697af 100644 --- a/src/v2/compiler/normalized_tree.dag +++ b/src/v2/compiler/normalized_tree.dag @@ -1,6 +1,9 @@ module v2.compiler.normalized_tree -import v2.compiler.body_lowering_fold { body_lowering_diagnostics_carry_wrapper_retention } +import v2.compiler.body_lowering_fold { + body_lowering_diagnostics_carry_service_set_aside, + body_lowering_diagnostics_carry_wrapper_retention +} import v2.std.diagnostic { Accepted, Diagnostic, @@ -80,6 +83,23 @@ fn normalized_tree_retention_diagnostic(root: Node) -> Diagnostic { } } +// A SERVICE WITH A SET-ASIDE MEMBER IS UNREACHABLE AS LOWERED. Body lowering lowers a service's +// interface and sets its realization members (transport, exit, response, mock_response) and its +// unmodeled interface members (modifiers, io `from` / default tails) aside, typed and located +// (v2.compiler.body_lowering_fold body_lower_service_decl). A NormalizedTree is what resolution, +// eval and emission consume, and every one of those would need the set-aside facts to act on a +// service -- so this door refuses, carrying each set-aside member's own located diagnostic as +// pending, and nothing downstream can assume a default transport. Only the census door, whose +// carrier has no route past the symbol index, admits them. Next-rung trigger: a realization binding +// carrier (a transport handler bound to the interface shape) and a typed operation-modifier carrier. +fn normalized_tree_service_set_aside_diagnostic(root: Node) -> Diagnostic { + Diagnostic { + reason: ^normalized_tree_reason_service_realization_unreachable, + at: node_locus(node: root), + correction: Unavailable { reason: ExternalContractUnknown } + } +} + fn admit_normalized_tree( root: Node, test_markers: TestMarkerChannel, @@ -94,6 +114,13 @@ fn admit_normalized_tree( rejected: diagnostics_singleton(d: normalized_tree_retention_diagnostic(root: root)) ) } + } else if body_lowering_diagnostics_carry_service_set_aside(d: diagnostics) { + Rejected { + diagnostics: rejected_with_pending( + pending: diagnostics, + rejected: diagnostics_singleton(d: normalized_tree_service_set_aside_diagnostic(root: root)) + ) + } } else { Accepted { value: NormalizedTree { @@ -134,6 +161,9 @@ type CensusTree sole_constructor { // could not read -- and a declaration census that silently dropped one would answer resolution with // a confident "not declared" (DESIGN section 5). A declaration that failed to PARSE never reaches // this door: the census entry takes a ParseTree. +// A service's set-aside members (realization, unmodeled interface) are admitted HERE and only here: +// the carrier's one reader is the symbol index, which needs the interface and never a realization, +// while admit_normalized_tree refuses them for every consumer that would. fn admit_census_tree( root: Node, import_bindings: FreeMonoid, diff --git a/src/v2/extdeps/languages/dag.dag b/src/v2/extdeps/languages/dag.dag index 9056c69b0ec..4b33c961d3f 100644 --- a/src/v2/extdeps/languages/dag.dag +++ b/src/v2/extdeps/languages/dag.dag @@ -2042,11 +2042,9 @@ fn dag_grammar_test_fn_decl_expr() -> GrammarExpr { // v1's `Type [from "key"] [= default]` tail (extdeps.shell `Find` declares `max_depth: Int = 1`); // general type fields do not (see dag_grammar_io_field_decl_expr). // The v2-inline operation form and service-level `config` are 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 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), @@ -2080,12 +2078,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( @@ -2171,16 +2171,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) + ) ) ) ) @@ -2524,10 +2527,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, @@ -2761,7 +2769,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..350157033e7 --- /dev/null +++ b/src/v2/test/claim/normalize/service_declaration_lowering_test.dag @@ -0,0 +1,129 @@ +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.compiler.tokenize { tokenize } +import v2.test.parse.supplied_token_stream_support { supplied_stream_parse } +import v2.extdeps.languages.dag { dag_lex } +import v2.std.compilers.lexing { symbol_intern_lexeme } +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. The census door +// admits a service module; the full door, whose carrier 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 access.P {\n operation Read {\n input { read: R }\n output { stdout: String from \"stdout\" }\n readonly\n transport shell { argv: [] }\n }\n}\n" + +// Two services sharing the `shell` 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 shell.Find {\n operation Run { input { path: String } }\n}\nservice shell.Env {\n operation Read { output { value: String } }\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" + +fn parse_of(source: String) -> Outcome { + bind_outcome( + o: tokenize(text: source, file: ^service_lowering_probe, rules: dag_lex()), + f: fn(tokens) { supplied_stream_parse(tokens: tokens) } + ) +} + +fn census_of(source: String) -> Outcome { + bind_outcome(o: parse_of(source: source), f: fn(tree) { normalize_census(parse_tree: tree) }) +} + +fn full_of(source: String) -> Outcome { + bind_outcome(o: parse_of(source: source), f: fn(tree) { normalize(parse_tree: tree) }) +} + +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(source: posix_service_source) { + Rejected { diagnostics: _ } => false + Accepted { value: t, diagnostics: _ } => + declares(index: index_of(t: t), service: "access.P", member: "Read") + } +} + +// Both set-aside reasons, counted apart: the transport is realization (1); the readonly modifier and +// the `from` key are unmodeled interface facts (2). +test fn census_counts_realization_and_unmodeled_interface_apart_holds() -> Bool { + match census_of(source: posix_service_source) { + 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. The full door 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(source: posix_service_source) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: r } => + carries(ne: r, reason: ^normalized_tree_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(source: shared_prefix_source) { + Rejected { diagnostics: _ } => false + Accepted { value: t, diagnostics: _ } => + declares(index: index_of(t: t), service: "shell.Find", member: "Run") + && declares(index: index_of(t: t), service: "shell.Env", member: "Read") + } +} + +test fn a_repeated_input_block_refuses_located_RED() -> Bool { + match census_of(source: repeated_input_source) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: r } => diagnostics_fatal_reason(d: r) == ^body_lowering_reason_service_io_block_repeated + } +} 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 cbabc2d7b2f..a4c11d39e80 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` and the v2-inline operation form remain -// v1-admitted remainder. Parsing is not lowering: the route rows below pin that a parsed -// service is refused at the normalized-tree door. +// v1-admitted remainder. Lowering is a separate stage: the route rows below pin what the +// normalized-tree door does with a parsed service. data probe_cached_lm: LanguageModel = dag_language_model() @@ -195,26 +195,28 @@ 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 fn control is the positive half: the same door admits a module with nothing retained. -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 set +// aside, typed and located. The normalized-tree door (normalized_tree admit_normalized_tree) is what +// resolution, eval and emission consume, so it still refuses a service with a set-aside realization, +// now 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 the door discriminates on what a service carries (DESIGN section 4b(4)). +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 == ^normalized_tree_reason_service_realization_unreachable, + cons: fn(found, d) { found || d.reason == ^normalized_tree_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) } } } @@ -230,15 +232,20 @@ 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 fn_control_is_admitted_at_the_normalized_tree_door_holds() -> Bool { diff --git a/src/v2/workflow/compile_door_cause_ownership.dag b/src/v2/workflow/compile_door_cause_ownership.dag index 41ea557437c..e727f01ade5 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: ^normalized_tree_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; the full normalized-tree door 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, From d18aa2d74670f4586cf13f90dbc036625b8b58da Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 25 Sep 2026 06:46:24 +0000 Subject: [PATCH 2/4] service lowering claims: supplied token streams + smaller fixtures (new-witness eval-step budget 72300) Co-Authored-By: Claude Opus 5.5 (1M context) --- .../service_declaration_lowering_test.dag | 145 ++++++++++++++---- 1 file changed, 118 insertions(+), 27 deletions(-) diff --git a/src/v2/test/claim/normalize/service_declaration_lowering_test.dag b/src/v2/test/claim/normalize/service_declaration_lowering_test.dag index 350157033e7..6afbf94384e 100644 --- a/src/v2/test/claim/normalize/service_declaration_lowering_test.dag +++ b/src/v2/test/claim/normalize/service_declaration_lowering_test.dag @@ -3,10 +3,8 @@ 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.compiler.tokenize { tokenize } -import v2.test.parse.supplied_token_stream_support { supplied_stream_parse } -import v2.extdeps.languages.dag { dag_lex } -import v2.std.compilers.lexing { symbol_intern_lexeme } +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 } @@ -26,29 +24,26 @@ import std.algebra { Cons, Empty, FreeMonoid } // 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 access.P {\n operation Read {\n input { read: R }\n output { stdout: String from \"stdout\" }\n readonly\n transport shell { argv: [] }\n }\n}\n" +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 the `shell` prefix in one module, the dag/extdeps/shell.dag shape: each is +// 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 shell.Find {\n operation Run { input { path: String } }\n}\nservice shell.Env {\n operation Read { output { value: String } }\n}\n" +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" -fn parse_of(source: String) -> Outcome { - bind_outcome( - o: tokenize(text: source, file: ^service_lowering_probe, rules: dag_lex()), - f: fn(tokens) { supplied_stream_parse(tokens: tokens) } - ) +// 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 census_of(source: String) -> Outcome { - bind_outcome(o: parse_of(source: source), f: fn(tree) { normalize_census(parse_tree: tree) }) -} - -fn full_of(source: String) -> Outcome { - bind_outcome(o: parse_of(source: source), f: fn(tree) { normalize(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) }) } fn index_of(t: CensusTree) -> SymbolIndex { @@ -82,17 +77,17 @@ fn carries(ne: NonEmptyDiagnostics, reason: Symbol) -> Bool { } test fn census_admits_the_posix_service_and_indexes_its_operation_holds() -> Bool { - match census_of(source: posix_service_source) { + match census_of_stream(tokens: posix_service_stream()) { Rejected { diagnostics: _ } => false Accepted { value: t, diagnostics: _ } => - declares(index: index_of(t: t), service: "access.P", member: "Read") + 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 `from` key are unmodeled interface facts (2). +// 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(source: posix_service_source) { + 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) @@ -104,7 +99,7 @@ test fn census_counts_realization_and_unmodeled_interface_apart_holds() -> Bool // 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(source: posix_service_source) { + match full_of_stream(tokens: posix_service_stream()) { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: r } => carries(ne: r, reason: ^normalized_tree_reason_service_realization_unreachable) @@ -113,17 +108,113 @@ test fn full_door_refuses_a_service_with_a_set_aside_realization_holds() -> Bool } test fn two_services_sharing_a_prefix_admit_with_distinct_labels_holds() -> Bool { - match census_of(source: shared_prefix_source) { + match census_of_stream(tokens: shared_prefix_stream()) { Rejected { diagnostics: _ } => false Accepted { value: t, diagnostics: _ } => - declares(index: index_of(t: t), service: "shell.Find", member: "Run") - && declares(index: index_of(t: t), service: "shell.Env", member: "Read") + 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(source: repeated_input_source) { + 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()) +} From e649114ce56ae7f5be9dc849bf5acdf39686d397 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 25 Sep 2026 07:30:33 +0000 Subject: [PATCH 3/4] service lowering: a repeated operation name refuses located (body_lowering_reason_service_operation_repeated), not as an unlocated post-normalize well-formedness failure (the bmc/http.dag shape) Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/compiler/body_lowering_fold.dag | 15 +++++++- .../service_declaration_lowering_test.dag | 35 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index 5618ccbf74a..99c9611fd86 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -88,6 +88,7 @@ import v2.std.node { ComputationNode, Conj, Edge, + EdgeLabel, Instantiation, Named, Node, @@ -7609,7 +7610,11 @@ fn body_lower_service_member(acc: Outcome, member: Node) -> outcome_accepted(value: a) } else if id == ^dag_surface_operation { bind_outcome(o: body_lower_operation(shell: member), f: fn(op) { - 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) }) + 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 { outcome_accepted( @@ -7625,6 +7630,14 @@ fn body_lower_service_member(acc: Outcome, member: Node) -> }) } +// 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 diff --git a/src/v2/test/claim/normalize/service_declaration_lowering_test.dag b/src/v2/test/claim/normalize/service_declaration_lowering_test.dag index 7bbdab7e1cc..97c4f9803da 100644 --- a/src/v2/test/claim/normalize/service_declaration_lowering_test.dag +++ b/src/v2/test/claim/normalize/service_declaration_lowering_test.dag @@ -46,6 +46,11 @@ 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 }) } @@ -218,3 +223,33 @@ fn repeated_input_stream() -> TokenStream { 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()) +} From d94ca257b3b96b11c1d4ac72fc8790bf13ec8138 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 25 Sep 2026 07:31:04 +0000 Subject: [PATCH 4/4] 03_normalize: drop the in-body annotation on the census service arm (the emitter admits module-item grain only; DESIGN 4c) Co-Authored-By: Claude Opus 5.5 (1M context) --- src/v2/compiler/03_normalize.dag | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/v2/compiler/03_normalize.dag b/src/v2/compiler/03_normalize.dag index 23ea7bb517a..5cbbb420a1e 100644 --- a/src/v2/compiler/03_normalize.dag +++ b/src/v2/compiler/03_normalize.dag @@ -465,8 +465,6 @@ fn normalize_census_node(n: Node, under_fn_decl: Bool) -> Outcome