diff --git a/DESIGN.md b/DESIGN.md index e0acd8598b1..eb1c253f9db 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -213,6 +213,7 @@ One row per class, each carrying its recognition rule and its receipts, in [docs - `executed_conjunct_discriminates_nothing` - `unbacked_execution_claim` - `mitigation_injected_where_judgment_declined` +- `one_refusal_two_destinations` ## Building & checks diff --git a/dag/gunbc/recurring_failure_mode.dag b/dag/gunbc/recurring_failure_mode.dag index 67b860be827..35689daf23d 100644 --- a/dag/gunbc/recurring_failure_mode.dag +++ b/dag/gunbc/recurring_failure_mode.dag @@ -210,6 +210,12 @@ data mitigation_injected_where_judgment_declined: RecurringFailureMode = Recurri evidence: [], } +data one_refusal_two_destinations: RecurringFailureMode = RecurringFailureMode { + identity: "one_refusal_two_destinations" as NonEmptyStr, + authored: "**one refusal, two destinations, remedy misdirected** (a single refusal at a single source column emits two rows naming DIFFERENT expected types, because two producers resolve the same declared spelling in different environments. The reader repairs from the row that names the parameter -- and that is the row the checker did NOT refuse on. **THE SYMPTOM IS THE RECOGNITION RULE, NOT THE CAUSE**: what a debugging author encounters is one column, two rows, two expected types; the instruction that saves them is DO NOT REPAIR FROM THE INHABITANCE ROW -- the compat row is what refused. Specimen, measured 2026-09-01 on gunbc c0a378c07be by calm-boar-314 and bold-carp-449 independently, on two binaries and two phases (`gunbc compile` emit-stage refusal and `gunbc run` entry-resolve), unrepaired at authoring. A module declaring `type String = FreeMonoid` and `fn takes_bare(s: String) -> Int`, called from a foreign module that IMPORTS the function, with a deliberately wrong `Int` actual so the diagnostic must name its destination, refuses with: `type mismatch: expected 'Primitive(String)', got 'Primitive(Int)'` AND `value does not inhabit its declared type at the direct call argument for parameter 's': declared 'Node(String)', produced 'Primitive(Int)'`. THE TWO PRODUCERS, read from source rather than inferred from message shape: `v1.compiler.infer direct_call_arg_mismatch_diags` renders the compat row from the formal as the CALLER environment resolves it, and `v1.compiler.infer declared_type_obligation_diags` renders the inhabitance row from `obligation.declared` through `declared_type_inhabitance`. Both reach `type_mismatch_error` and `DeclaredTypeNotInhabited` in `v1.compiler.core`, so the rendering is shared and the DIVERGENCE IS UPSTREAM OF IT -- which is why the two rows look like one mechanism disagreeing with itself. **THE TRIGGER IS TWO COMPETING BINDINGS LIVE IN ONE RESOLVING SCOPE, NOT THE IMPORT AS SUCH**, and this row states the narrower claim deliberately: the with/without-import pair is measured, but the two AGREEING cells agree for OPPOSITE reasons -- an un-imported foreign caller agrees at `Primitive(String)` because only the kernel binding is available, and an in-module caller agrees at `Node(String)` because only the local declaration is. Phrasing the trigger as the import would make a reader conclude the class does not reproduce when some other construct puts a second binding in scope. An explicit `import \{ \}` is the ONLY construct measured to create the competition; whether any other does is UNMEASURED. Crossed arm, pre-registered before its log was read: removing the import moves ONLY the inhabitance row, from `Node(String)` to `Primitive(String)`, while the compat row is `Primitive(String)` under both -- so one producer follows the import binding and the other does not, rather than the whole reading shifting. RUNG FOUND AT: 1 (mitigatable) -- the line DOES stop, and stops located, so this is not silent wrongness; what fails is that the refusal MISDIRECTS the remedy, which is DESIGN section 5's requirement that a refusal be typed AND located read one layer deeper: located at the right column while naming the wrong destination. CEILING: 3 (structurally guaranteed) -- one resolution of a callee's declared type per call site, consumed by both producers, makes two destinations unconstructible. NEXT-RUNG TRIGGER, naming the capability rather than an artifact: a single resolved-formal authority at the call site that both the compat check and the inhabitance obligation READ, sufficient that no call-site refusal can render two different expected types for one parameter. Distinct from `meaning_fork`, which is one NAME carrying two meanings across scopes -- that is the underlying condition here and is separately rostered; this row is about two PRODUCERS disagreeing inside a single refusal, which is what makes the remedy wrong rather than merely the meaning ambiguous. Distinct from `diagnostic_name_mechanism_silent`: there the diagnostic names a mechanism that never ran; here both mechanisms ran and both reported faithfully about different environments. WHAT IS NOT CLAIMED, by either measurer: which producer is correct, and why they read the binding differently. No arm on either rig touches it, and the source reading above identifies the producers without settling which environment a call-argument formal SHOULD be resolved in.)" as NonEmptyStr, + evidence: [], +} + data instrument_output_read_as_subject_content: RecurringFailureMode = RecurringFailureMode { identity: "instrument_output_read_as_subject_content" as NonEmptyStr, authored: "**instrument output read as subject content** (a consumer derives a SET from a report whose completeness is a property of the REPORTING TOOL rather than of the subject. **The boundary against `execution_provenance_loss` is the whole justification for a separate row, because the two look identical at the call site and have opposite repairs**: there a value is consumed as though an observation produced it while nothing proves the observation path RAN, and the fix is to bind an execution receipt; here the observation ran and reported FAITHFULLY — the instrument answered its own question correctly — and what is wrong is that its output was complete for the REPORTER s purpose and was read as complete for the CONSUMER s. Provenance is intact; the SUBJECT is substituted, so no receipt binding can detect it and the repair is instead to derive the set from the subject itself. Distinct from `total_at_the_level_examined_blind`, which is a judgment shape (a match total over the type it examines, blind inside an arm), and from `reachability_read_as_occupancy`, which asks whether a quiet guard should exist; this is neither a match nor a guard. Specimen with a receipt, repaired on main by gunbc#9829: the required floor asked `extdeps.git` `git.Core.DiffUnified0` (`git diff -U0 `) which lines a change touched, and `floor_diff_edits_from_line_ranges` attributed those lines to enclosing declarations to decide which witness identities the change established. Git prints only the hunks that DIFFER, so on a rename it prints the module line and whatever else moved — while every identity at the destination is a newly qualified `module.function` that has never executed under that spelling, because the authored module name moved with the file. **THE REASON IT SURVIVED IS THAT THE TWO ANSWERS COINCIDE BY ACCIDENT IN THE COMMON CASE**: for a wholly-added file every line is a `+` line, so line attribution reaches every declaration and the defect is invisible; it appears only when the instrument gets TERSE, which rename detection is. Measured over the window reachable in a shallow session clone — a floor on the population, not the population, since older renames are unmeasured — 8 of 8 rename-destination `.dag` files carrying test decls were under-enrolled and at least 87 of their 103 identities were never selected; a PURE MOVE with no content edit enrolled NOTHING. It was not merely a reporting gap: changed-witness membership is what OVERRIDES the cost-debt withhold and the outside-gate suppression, so the same miss on a rostered identity is a silent decline of a witness whose author is present, which is the state `v2.workflow.floor_changed_witness` `changed_witness_execution_standing` exists to red. The repair derives the set from the subject: a path `parse_unified_diff_added_paths` already rules new-at-path contributes the declarations THE FILE ITSELF PARSES TO, bounded to that file s own decl list and never the corpus, so it is the precise answer rather than an absorbing widen. Its discriminating red is `rename_destination_enrolls_every_test_decl_not_only_the_diff_touched_one`, which feeds the specimen s verbatim diff and FAILS when the rule is disabled, paired with `in_place_modify_enrolls_only_the_touched_test_decl`, which passes under both states so the first cannot be satisfied by widening. **Recognition rule, and it predicts well beyond git**: whenever a set is read out of a tool s output, ask whether that tool promised completeness over YOUR question or over its own — `--stat`, any de-duplicating or truncating reporter, and any porcelain that elides for human brevity all report faithfully and none of them promise the consumer s set. **Its near twin is `executed_conjunct_discriminates_nothing`, approached from the other side**: there the satisfied signal is EXECUTION -- the conjunct genuinely runs and is genuinely gated on -- while here it is COMPLETENESS RELATIVE TO THE REPORTER, which is why this class is not a reachability problem and looking for an unexecuted path finds everything healthy. In both, the axis the diligent reader checks already reads green, and checking it harder returns the same green.)", @@ -259,4 +265,5 @@ data recurring_failure_mode_roster: List = [ executed_conjunct_discriminates_nothing, unbacked_execution_claim, mitigation_injected_where_judgment_declined, + one_refusal_two_destinations, ] diff --git a/docs/design-ledgers.md b/docs/design-ledgers.md index 2fe205fb376..3a7449634b3 100644 --- a/docs/design-ledgers.md +++ b/docs/design-ledgers.md @@ -42,6 +42,7 @@ The landing measurement partitions the 31 parser-visible identities into **2 cit - **a conjunct that executes, is reached, is gated on, and still discriminates nothing** (a predicate wired into a live check runs on every call, sits inside a gate that really blocks, and yet deletes cleanly: remove it and every witness stays green, because no fixture ever constructs the population that would make it false. **It is invisible precisely BECAUSE reachability is satisfied.** The usual signature of an unenforced mechanism -- nothing calls it -- is absent, so every reachability-based tell reports health, and review reads the conjunct as a wall because it genuinely is evaluated. Executed and enforcing are different properties, and no amount of coverage over the calling path distinguishes them. **It is bounded against three neighbours and none of their recognition rules will find it.** `reachability_read_as_occupancy` asks whether to DELETE a guard that is quiet, and warns that mechanism-yes / join-yes / occupancy-zero is a healthy guard; this class is not about deletion and not about occupancy -- the conjunct here should stay, and the defect is that it was believed to be doing work it was not. Section 6's inert-lens tell is machinery that nothing GATES on; this is gated on. Section 4b's decoration case is a check whose RED cannot be authored ANYWHERE; this RED can be authored -- it simply never was, which is the whole difference, and it is the difference between a permanent ceiling and one missing fixture. **THE REMEDY HAS TWO HALVES AND THE SECOND IS WHAT MAKES THE FIRST SOUND.** First: for each wall, run the DELETE arm and confirm EXACTLY ONE witness reddens -- not that a witness reddens. Second: the subject that isolates the conjunct must be chosen with care, because a red for the wrong reason is how this class survives its own test. **SPECIMEN, with both halves executed** (gunbc#9838, `gunbc.self_host_compile_phase_frontier`): `census_population_is_duplicate_free` was added as a conjunct of `receipt_population_coherent`, evaluated on every coherence check and reachable from the live path; deleting it left the whole witness module green. The fixture that isolates it is a receipt whose `refused_files` REPEATS A PATH -- enrolled as `a_receipt_whose_refused_population_repeats_a_path_is_refused_by_coherence` in `test.claim.self_host_compile_phase_frontier_witness`, with `the_same_receipt_without_the_repeat_is_coherent` as its positive control. The subject that looks equally good and is not: a repeated ERROR DIAGNOSTIC IDENTITY also reddens the module, but it breaks a count invariant one line over rather than this conjunct, so it would have scored the DEAD conjunct as LIVE and closed the investigation with the wall still inert. **RUNG: mechanically preventable, and the honest ceiling is the same rung rather than higher.** The unenforcing conjunct stays writable; what exists is an executed falsifier per wall, not a construction that forbids authoring one. The next-rung trigger is a CAPABILITY and not an artifact: a mechanism that derives, from the check itself, the population its removal would change -- so that a conjunct with an empty such population fails to compile rather than waiting for an author to think of the delete arm. Enumerating delete arms by hand does not retire it; it only executes this row's recognition rule once per wall. **Its near twin is `instrument_output_read_as_subject_content`, and they meet from opposite sides**: there a report is complete for the REPORTER and read as complete for the SUBJECT; here a conjunct is genuinely executed and read as genuinely discriminating. Both are the shape where the health signal the reader actually checks -- completeness there, execution here -- is fully satisfied while the property they believe it establishes does not hold. **Neither is found by checking harder along the axis that already reads green.**) - **unbacked execution claim** (prose asserts a live executing relation — this runs on X, X reads these fields, Y already resolves Z — that no authority backs, and a reader forms the premise and plans against it. §4b's CI rung drop names the harm PREMISE CONTAMINATION in its own words; this row is that harm as a general class, since the drop is about one paragraph in the canonical authority and the class is about every carrier. **THE ORIGINS ARE A FIELD OF THIS ONE ROW RATHER THAN SEPARATE ROWS, because the recognition rule is identical across them** — resolve the claim against the authority that owns the relation — **but the REMEDY BRANCHES ON THE ORIGIN, and a remedy applied without reading the origin manufactures new false statements.** Three origins are ATTESTED here and a fourth is conceivable but was NOT found. ORIGIN 1, EXECUTOR DELETED: the claim was true and its executor was removed. Specimen: `gunbc.floor_component_receipt_document` recited that an alert downloads an artifact named `floor-component-receipt` from a run id it is given and reads four named fields; that alert was `falsifier-alert.yml`, deleted 2026-08-15, and a lane read the module, believed the transport live, planned against it, and was corrected by its reviewing authority — the cost is measured, not hypothetical. Second specimen, same origin: `gunbc.ci_failure_class` stated its dissolution trigger as calling `classify_failure_reason` *the way claim_executor already resolves* `gunbc.floor_component_receipt`; `write_floor_component_receipt_at` did exactly that from 2026-07-30 (gunbc#7467) until gunbc#9228 deleted it on 2026-08-25. ORIGIN 2, THE CITED SYMBOL DIED WHILE ITS CONTENT SURVIVED AS AN ANNOTATION, and this one has a corpus-wide producer: `v1_compiler.cli_run` twice cited `floor_component_resource_checkpoint_note` and `floor_component_phase_journal_scaffold_note`, which were real `data …: String` declarations in `gunbc.floor_component_receipt` until the 2026-08-30 prose-bankruptcy sweep (gunbc#9752) converted module-scope commentary rows into §4c annotations corpus-wide. §4c makes an annotation uncitable by construction — `v1_compiler.cli_run` `annotation_erased_scan_text` feeds the semantic passes, so annotation text is not present in the tree the citation harvester walks. **THE CONSEQUENCE IS THE OPPOSITE OF THE ONE THIS ROW FIRST DREW, AND THE CORRECTION IS THE INSTRUCTIVE PART.** The first revision reasoned that the sweep therefore broke every citation of a converted row, making the population the sweep's own diff. Measured by a separate lane (gunbc XL-0-CITE, 2026-09-01) against `v1_compiler.declaration_index`, whose only citation producers are `citation_from_record_literal` and `citation_from_constructor_call`: the typed-citation breakage is **ZERO**, computed both today and as of the sweep commit, on a control that discriminates in both directions. A plain name in annotation prose was never a citation, so the sweep could not break one. What that leaves is a REAL class with an honest ceiling rather than a defect population: these citations bind READERS and no mechanism, so they sit in §4b's *outside the modeled guarantee* column — observed and repaired by reading, never gated — and calling them a dangling-citation population would have been rung inflation about a check that by construction cannot see them. The two `cli_run` sites are genuine instances of the harm and there is no mechanical census that would have found them. ORIGIN 3, WRONG AUTHORITY FOR SOMETHING THAT STILL EXECUTES: `dag/test/claim/instrument_sandbox_witness_test` says its live half `runs on the falsifier lane`; that half sits on `FalsifierSubstrateLongLane`, which `std.witness_admission` `witness_cadence_has_scheduled_route` reports routeless — but it also carries a local recipe, and `OfflineLocalRecipe` HAS a route, so the relation is live and only the named authority is wrong. THE FOURTH, NEVER BACKED AT ALL, IS NOT ATTESTED IN THIS CORPUS and is recorded as unmeasured rather than as a population. **THIS ROW IS NOT THE AUTHORITY ON ANY POPULATION, and saying so is the point:** `gunbc.deleted_cadence_reference_census` already owns the falsifier-cadence instance of origin 1 — twelve sites, a `DeletedCadenceReferenceStanding` vocabulary whose `PremiseInvalidated` arm is exactly this class, and one `GuaranteeRungDrop` filed once for the shared executor rather than per site. A pattern row that restated its population would be a second authority for a fact that already has one, which is the §3 violation this row's own remedy is meant to prevent; so the census is cited here and nothing about its contents is copied. What belongs HERE is only the recognition rule and the remedy arms, which generalize past the falsifier. **THE MECHANICAL TEST for finding one is sharper than asking whether the verb is runs or is enrolled: resolve the claim against the authority that owns the relation, and ask whether the prose implies a live route for something that authority says has none.** The naive runs/enrolled discriminator loses one arm and it is the common one — `Enrolled on falsifier_rehomed_bin_wet_entries (nightly batch 5)` is nominally an enrollment claim, which is TRUE, but `nightly` asserts a cadence that executes and `FalsifierRehomedBinWet` is routeless, so the reader forms the false premise anyway. Three outcomes per line: asserts execution (contaminating), asserts enrollment only (true, leave it), asserts enrollment while implying a live cadence (contaminating). **THE REMEDY HAS THREE ARMS, KEYED TO WHAT THE AUTHORITY SAYS NOW, and picking the wrong one is itself an instance of this class.** Origin 1 → PAST TENSE WITH THE DATE, keeping the reasoning, because the reasoning usually outlives its executor and a reader who finds a dangling claim silently deleted learns nothing. Origin 2 → name the module and the FACT rather than the dead symbol, since §4c left no symbol to cite; past-tensing here would wrongly report the content as gone when only its citability is. Origin 3 → CORRECT IT to the authority that does back it, NOT past tense: past-tensing a live relation records it as dead and is a new contamination in the opposite direction, produced by the remedy itself. Origin 4 → say it was never backed, or delete the assertion while keeping any surviving reasoning; NEVER past tense, because there is no past to record, and `X USED TO resolve Y` asserts an execution that also never happened. **The default against deletion is a default and not an absolute: a sentence whose entire content is a false execution claim has no reasoning to preserve.** **THE SELF-RECEIPT, and it is the most useful thing in this row (2026-09-01):** the first revision of this entry asserted the `ci_failure_class` clause and the two `cli_run` notes as never-true, on the strength of a grep showing they resolve nowhere TODAY. **Both were true when written**, and the row about unbacked claims therefore contained two of them. It was caught by a reviewing authority asking for verification rather than inheritance, not by any check. **RECOGNITION RULE EARNED: `resolves nowhere now` is not `never resolved`, the distinction is decided by history and not by the working tree, and the instrument is `git log --all -S` over the DECLARATION FORM** — which also separates origin 1 from origin 2 by showing WHAT deleted the referent. The neighbouring rule is `positional_citation`'s: a citation left naming a deleted symbol fakes a grep hit, so every arm above records the change rather than quietly removing the name.) - **mitigation injected where the judgment declined** (a downstream stage injects a RUNTIME mitigation at exactly the seam where an upstream judgment returned Undecidable, so a 4b rung-1 fallback comes to occupy the place the wall belongs and the declined seam stops looking empty. **THE LOAD-BEARING HALF IS WHAT THE COINCIDENCE PROVES ABOUT THE DECLINE**: an Undecidable verdict is normally read as benign conservatism — the facts did not settle it, nothing is claimed, no harm asserted — and a mitigation landing on the SAME LINE is the first evidence that a real defect was sitting under that silence. The decline is therefore load-bearing AT THIS SITE, which refutes reading an Undecidable verdict as evidence of ABSENCE. **IT DOES NOT MAKE THE DECLINE CENSUS PREDICTIVE, and this row carries that limit because the author first wrote the stronger claim and then measured it FALSE** on the same board that produced the specimen: 259 of 272 declines sit in modules with ZERO blocking errors, only 6 of 28 decline-bearing modules carry any error at all, and the module holding 42 of the 63 errors carries ZERO declines and 105 ACCEPTS. Declines and defects are ANTI-CORRELATED at module grain, so this specimen is ONE co-location out of 272 and a walk of the decline census would be walking sites that are ~95% empty. The class is about a mitigation landing on a declined seam; it is NOT a claim that declines predict defects. Distinct from `absorbing_fallback`, whose arm WIDENS to a superset and destroys the precise mechanism s signal; here the arm NARROWS to a panic and destroys nothing except the evidence that a judgment was owed. Distinct from `reflection_evidence_structural_proof`, which mistakes an observation for a proof; here nothing is proven or observed — a decision was declined and then papered. Specimen with a receipt, measured 2026-09-01 on gunbc 0e8c49d and unrepaired at authoring: `v2.std.diagnostic outcome_accepted` is `fn outcome_accepted(value: T) -> Outcome` with T FREE; `v2.compiler.07_target_carriers target_fidelity_quotient_optional` declares `-> Outcome>` and calls `outcome_accepted(Present { value: child })`, so T instantiates to `Optional` and THE SOURCE IS CORRECT. The Rust emitter nonetheless rendered `Some(child).expect("fail-closed: an optional value flowed into non-optional parameter 0 of outcome_accepted (empty Optional at runtime)")`. FOUR DEFECTS IN ONE LINE: it fires on a GENERIC formal where `Optional` is a legitimate instantiation rather than a cardinality escape, so the check does not know T is free; it substitutes a runtime panic for a compile-time judgment; the emitted code does NOT typecheck anyway (rustc E0308 expected `Option>` found `Rc`), so the mitigation buys nothing while standing where the wall belongs; and its own panic string calls itself `fail-closed`, which is 4b rung inflation inside a string literal — a diagnostic naming the discipline it violates is worse than a silent one because it will be cited as evidence the discipline holds. The coincidence that makes this a class and not a bug: `v1.compiler.infer declared_type_inhabitance` returned InhabitanceUndecidable{UndecidableOptionalCarrier} at that module s ONLY optional-carrier decline, 07_target_carriers.dag:128:24 parameter `value` — the same line the emitter mitigated. RUNG FOUND AT: 1 (mitigatable) on a seam whose CEILING is 3 (structurally guaranteed), since generic-formal instantiation is decidable from modeled structure. CONSEQUENCE WITH ITS OWN TRIGGER, deliberately NOT folded into this row: wiring the optional-carrier judgment is argued by this specimen but is a separate change, and its trigger is a producer at that seam that can distinguish a FREE type variable from a declared non-optional formal — until that exists the judgment would decline for the same reason it declines today.) +- **one refusal, two destinations, remedy misdirected** (a single refusal at a single source column emits two rows naming DIFFERENT expected types, because two producers resolve the same declared spelling in different environments. The reader repairs from the row that names the parameter -- and that is the row the checker did NOT refuse on. **THE SYMPTOM IS THE RECOGNITION RULE, NOT THE CAUSE**: what a debugging author encounters is one column, two rows, two expected types; the instruction that saves them is DO NOT REPAIR FROM THE INHABITANCE ROW -- the compat row is what refused. Specimen, measured 2026-09-01 on gunbc c0a378c07be by calm-boar-314 and bold-carp-449 independently, on two binaries and two phases (`gunbc compile` emit-stage refusal and `gunbc run` entry-resolve), unrepaired at authoring. A module declaring `type String = FreeMonoid` and `fn takes_bare(s: String) -> Int`, called from a foreign module that IMPORTS the function, with a deliberately wrong `Int` actual so the diagnostic must name its destination, refuses with: `type mismatch: expected 'Primitive(String)', got 'Primitive(Int)'` AND `value does not inhabit its declared type at the direct call argument for parameter 's': declared 'Node(String)', produced 'Primitive(Int)'`. THE TWO PRODUCERS, read from source rather than inferred from message shape: `v1.compiler.infer direct_call_arg_mismatch_diags` renders the compat row from the formal as the CALLER environment resolves it, and `v1.compiler.infer declared_type_obligation_diags` renders the inhabitance row from `obligation.declared` through `declared_type_inhabitance`. Both reach `type_mismatch_error` and `DeclaredTypeNotInhabited` in `v1.compiler.core`, so the rendering is shared and the DIVERGENCE IS UPSTREAM OF IT -- which is why the two rows look like one mechanism disagreeing with itself. **THE TRIGGER IS TWO COMPETING BINDINGS LIVE IN ONE RESOLVING SCOPE, NOT THE IMPORT AS SUCH**, and this row states the narrower claim deliberately: the with/without-import pair is measured, but the two AGREEING cells agree for OPPOSITE reasons -- an un-imported foreign caller agrees at `Primitive(String)` because only the kernel binding is available, and an in-module caller agrees at `Node(String)` because only the local declaration is. Phrasing the trigger as the import would make a reader conclude the class does not reproduce when some other construct puts a second binding in scope. An explicit `import { }` is the ONLY construct measured to create the competition; whether any other does is UNMEASURED. Crossed arm, pre-registered before its log was read: removing the import moves ONLY the inhabitance row, from `Node(String)` to `Primitive(String)`, while the compat row is `Primitive(String)` under both -- so one producer follows the import binding and the other does not, rather than the whole reading shifting. RUNG FOUND AT: 1 (mitigatable) -- the line DOES stop, and stops located, so this is not silent wrongness; what fails is that the refusal MISDIRECTS the remedy, which is DESIGN section 5's requirement that a refusal be typed AND located read one layer deeper: located at the right column while naming the wrong destination. CEILING: 3 (structurally guaranteed) -- one resolution of a callee's declared type per call site, consumed by both producers, makes two destinations unconstructible. NEXT-RUNG TRIGGER, naming the capability rather than an artifact: a single resolved-formal authority at the call site that both the compat check and the inhabitance obligation READ, sufficient that no call-site refusal can render two different expected types for one parameter. Distinct from `meaning_fork`, which is one NAME carrying two meanings across scopes -- that is the underlying condition here and is separately rostered; this row is about two PRODUCERS disagreeing inside a single refusal, which is what makes the remedy wrong rather than merely the meaning ambiguous. Distinct from `diagnostic_name_mechanism_silent`: there the diagnostic names a mechanism that never ran; here both mechanisms ran and both reported faithfully about different environments. WHAT IS NOT CLAIMED, by either measurer: which producer is correct, and why they read the binding differently. No arm on either rig touches it, and the source reading above identifies the producers without settling which environment a call-argument formal SHOULD be resolved in.) ## Declared rung drops (§4b(3)) diff --git a/src/v1/00_core.dag b/src/v1/00_core.dag index 4009aad4f45..cd8b76d8950 100644 --- a/src/v1/00_core.dag +++ b/src/v1/00_core.dag @@ -1121,8 +1121,25 @@ fn field_node_default_value(n: Node) -> Node? { } else { none } } +// THE SINGLE AUTHORITY FOR THE OUTPUT-SOURCE PROPERTY'S NAME (DESIGN section 3). +// +// This property had TWO names for one fact and the divergence was invisible. The parser mints it +// through make_field_init_node with the synthetic name "from_key" AND with the authored `from` +// token's span; every reader reaches the name through field_init_node_name_at, which resolves it +// from the SPAN. So the name a reader gets back is always "from", the synthetic spelling is +// unobservable, and every `== "from_key"` filter in the corpus matched nothing on every field, +// always -- silently, because the emitter's Absent arm answered "stdout" for the miss. +// +// The interpreter is the tell: v1_interpreter extract_from_key reads +// `prop_name == "from_key" || prop_name == "from"`. Someone met this before and repaired it on one +// side only, which is what left the two readers agreeing about the fact and disagreeing about its +// name. Teaching the emitter the second spelling too would have converged the readers on the +// LENIENT side and given the nickname a third permanent home; the name is a single fact, so it +// gets a single declaration and both the mint and every reader are derived from it. +fn from_key_property_name() -> String { "from" } + fn field_node_from_key(n: Node, source_indices: Map) -> String? { - match find_property(props: n.properties, prop_name: "from_key", source_indices: source_indices) { + match find_property(props: n.properties, prop_name: from_key_property_name(), source_indices: source_indices) { Present { value: p } => Present { value: authored_name_at(source_indices: source_indices, node: p) } Absent => none } diff --git a/src/v1/02_parse.dag b/src/v1/02_parse.dag index f38a1b6ba75..12c60304aba 100644 --- a/src/v1/02_parse.dag +++ b/src/v1/02_parse.dag @@ -6,7 +6,7 @@ import v1.std.core { Connective, Conj, Disj, NoConnective, Arrow, is_container_type, Cardinality, Required, CardOptional, make_param_node, param_node_type_expr, param_node_default_value, - make_field_node, field_node_name_at, field_node_type_expr, field_node_cardinality, field_node_default_value, field_node_from_key, + make_field_node, field_node_name_at, field_node_type_expr, field_node_cardinality, field_node_default_value, field_node_from_key, from_key_property_name, make_variant_node, variant_node_name_at, variant_node_fields, leaf_node_with_span, ExprData, NoExprData, ExprLiteral, ExprVar, ExprFieldAccess, ExprCall, ExprMethodCall, @@ -2893,7 +2893,7 @@ fn parse_optional_from_key(tokens: TokenStream, ctx: ParseContext) -> FromKeyRes let value_mint = mint_parsed_node_identity(ctx: ctx) let property_mint = mint_parsed_node_identity(ctx: value_mint.ctx) let value = make_named_expr_node(occurrence_identity: value_mint.identity, name: key, expr_data: ExprLiteral { value: LitStr { value: key } }, children: [], inferred: none, span: token_span(tok: tok2), name_span: token_span(tok: tok2)) - let property = make_field_init_node(occurrence_identity: property_mint.identity, name: "from_key", value: value, span: token_span(tok: tok), name_span: token_span(tok: tok)) + let property = make_field_init_node(occurrence_identity: property_mint.identity, name: from_key_property_name(), value: value, span: token_span(tok: tok), name_span: token_span(tok: tok)) FromKeyResult { properties: [property], tokens: token_stream_advance(stream: tokens, n: 2), ctx: property_mint.ctx, err: none } } _ => diff --git a/src/v1/04_resolve.dag b/src/v1/04_resolve.dag index 13df5958a4d..be0e38c751e 100644 --- a/src/v1/04_resolve.dag +++ b/src/v1/04_resolve.dag @@ -11,7 +11,7 @@ import v1.std.core { authored_name_at, NewlineIndex, make_param_node, make_resolved_param_node, param_node_name_at, param_node_type_expr, param_node_default_value, generic_param_name_at, - make_field_node, field_node_name_at, field_node_type_expr, field_node_cardinality, field_node_default_value, field_node_from_key, + make_field_node, field_node_name_at, field_node_type_expr, field_node_cardinality, field_node_default_value, field_node_from_key, from_key_property_name, InferredNode, Resolved, CompilerError, TypeVariable, is_compiler_error, ErrorNode, make_error_node, InternalError, ArityMismatch, UnresolvedType, UnlistedImportUse, @@ -697,7 +697,7 @@ fn resolve_field(field: Node, env: TypeEnv, module_name: String) -> FieldResolve Present { value: result } => Present { value: result.expr } Absent => none }, - from_key_properties: field.properties |> filter(p => field_init_node_name_at(n: p, source_indices: env.source_indices) == "from_key"), + from_key_properties: field.properties |> filter(p => field_init_node_name_at(n: p, source_indices: env.source_indices) == from_key_property_name()), span: field.span, name_span: node_name_span(n: field) ), diagnostics: concat(type_diags, default_diags) diff --git a/src/v1/05_emit.dag b/src/v1/05_emit.dag index c199c5e192d..a4e3145363e 100644 --- a/src/v1/05_emit.dag +++ b/src/v1/05_emit.dag @@ -17,7 +17,7 @@ import v1.std.core { field_binding_name_at, field_binding_pattern, FieldSummary, TupleFirst, TupleSecond, expr_method_name_at, method_arg_nodes, expr_method_call_semantics, expr_field_access_summary, lambda_param_names_at, record_lit_type_name_at, arm_body, arm_pattern, arm_guard, - arg_name_at, arg_value, field_init_node_name_at, field_init_node_value, if_condition, + arg_name_at, arg_value, field_init_node_name_at, field_init_node_value, from_key_property_name, if_condition, if_then_branch, if_else_branch, match_scrutinee, match_arm_nodes, let_value, let_body, field_access_base, method_receiver, lambda_body, cast_expr, cast_target, return_value, binop_left, binop_right, foreach_variable_at, foreach_collection, foreach_body, index_base, @@ -1136,7 +1136,7 @@ fn bind_file_verb(t: Node, op_node: Node, source_indices: Map) -> FileResultField? { - match file_result_channel_of_key(key: file_output_channel_of_field(ch: ch, source_indices: source_indices)) { + match file_result_channel_of_key(key: transport_output_channel_of_field(ch: ch, source_indices: source_indices)) { Present { value: c } => Present { value: FileResultField { channel: c, optional: ch.return_cardinality == CardOptional } } Absent => none } @@ -1154,7 +1154,7 @@ fn bind_file_operation(t: Node, op_node: Node, source_indices: Map BindingRefused { cause: FileBindingRefused { refusal: FilePathNotStaticallyRenderable } } Present { value: path_tpl } => { - let declared = file_output_channel_fields(op_node: op_node) + let declared = transport_output_channel_fields(op_node: op_node) let fields = declared |> flat_map(ch => match bind_file_result_field(ch: ch, source_indices: source_indices) { Present { value: f } => [f] @@ -2113,7 +2113,7 @@ fn is_file_transport_after_dispatch_precedence(t: Node, source_indices: Map) -> String? { - match ch.properties |> filter(p => field_init_node_name_at(n: p, source_indices: source_indices) == "from_key") |> first { + match ch.properties |> filter(p => field_init_node_name_at(n: p, source_indices: source_indices) == from_key_property_name()) |> first { Present { value: prop } => match field_init_node_value(n: prop).expr_data { ExprLiteral { value: LitStr { value: s } } => Present { value: s } _ => none @@ -2201,12 +2201,12 @@ fn is_modeled_file_output_channel(key: String) -> Bool { // this pair, so "which key does this field carry" is decided once. Unwrapping a single-field // product here would lose the field's `from` key -- the shape a `from` key is most often // authored on -- and the renderer would silently answer the default channel for it. -fn file_output_channel_fields(op_node: Node) -> List { +fn transport_output_channel_fields(op_node: Node) -> List { let rt = resolved_type(n: op_node) if is_product_type(n: rt) { rt.children } else { [] } } -fn file_output_channel_of_field(ch: Node, source_indices: Map) -> String { +fn transport_output_channel_of_field(ch: Node, source_indices: Map) -> String { match child_from_key(ch: ch, source_indices: source_indices) { Present { value: k } => k Absent => authored_name_at(source_indices: source_indices, node: ch) @@ -2214,8 +2214,216 @@ fn file_output_channel_of_field(ch: Node, source_indices: Map) -> List { - file_output_channel_fields(op_node: op_node) - |> map(ch => file_output_channel_of_field(ch: ch, source_indices: source_indices)) + transport_output_channel_fields(op_node: op_node) + |> map(ch => transport_output_channel_of_field(ch: ch, source_indices: source_indices)) +} + +// THE CLOSED ROSTER OF SHELL OUTPUT CHANNELS -- the shell twin of FileResultChannel, and the +// authority that replaces a string compared against string literals at three sites. +// +// The members are MEASURED, not guessed: they are the eight keys authored on `from` across the +// shell operations in this corpus, and they are exactly the eight `v1_interpreter` +// shell_evidence_value plus map_shell_outputs answer. The interpreter is the working realization +// and therefore the oracle for what the transport MEANS; this roster reads that meaning off it +// once instead of each renderer re-deciding it. +// +// It is a coproduct rather than a String because the set is DECIDABLE from the declaration. A key +// outside it is a modelling gap that must be added here -- which is the whole difference from the +// arm this replaces, where an unrecognised key fell through to `stdout.clone()` and the emitted +// Rust bound an Int field to a String with no diagnostic (DESIGN section 5, fabricated plausible +// output, in the emitter). +type ShellResultChannel + = ShellChanStdout + | ShellChanStderr + | ShellChanStdoutLines + | ShellChanExitSuccess + | ShellChanExitCode + | ShellChanStderrTruncated + | ShellChanStderrTotalBytes + | ShellChanStderrRetainedBytes + +// The single authority for which declared output keys name a shell channel at all. +// `is_modeled_shell_output_channel` is derived from it, exactly as the file twin is. +fn shell_result_channel_of_key(key: String) -> ShellResultChannel? { + if key == "stdout" { Present { value: ShellChanStdout } } + else if key == "stderr" { Present { value: ShellChanStderr } } + else if key == "stdout_lines" { Present { value: ShellChanStdoutLines } } + else if key == "exit_success" { Present { value: ShellChanExitSuccess } } + else if key == "exit_code" { Present { value: ShellChanExitCode } } + else if key == "stderr_truncated" { Present { value: ShellChanStderrTruncated } } + else if key == "stderr_total_bytes" { Present { value: ShellChanStderrTotalBytes } } + else if key == "stderr_retained_bytes" { Present { value: ShellChanStderrRetainedBytes } } + else { none } +} + +fn is_modeled_shell_output_channel(key: String) -> Bool { + match shell_result_channel_of_key(key: key) { + Present { value: _ } => true + Absent => false + } +} + +fn shell_result_channel_key(c: ShellResultChannel) -> String { + match c { + ShellChanStdout => "stdout" + ShellChanStderr => "stderr" + ShellChanStdoutLines => "stdout_lines" + ShellChanExitSuccess => "exit_success" + ShellChanExitCode => "exit_code" + ShellChanStderrTruncated => "stderr_truncated" + ShellChanStderrTotalBytes => "stderr_total_bytes" + ShellChanStderrRetainedBytes => "stderr_retained_bytes" + } +} + +// WHICH CHANNELS A RUST REALIZATION CAN ANSWER, kept separate from the roster above for the same +// reason target_renders_file_transport is kept separate from file_binding_refusal: "this channel +// is not a shell channel" and "this target cannot render this shell channel" are two different +// facts with two different remedies, and fusing them would report a modelling gap where the truth +// is a backend capability gap. +// +// The three stderr accounting channels are the ones Rust cannot answer. They are not missing +// plumbing: they report a TRUNCATION POLICY (`WitnessStderrCapturePolicy`), and the emitted +// `std::process::Command` body captures stderr whole and models no policy at all, so there is no +// honest value for total-versus-retained bytes or for `truncated`. Emitting one would be an +// invented fact, which is why this refuses instead. +// +// NEXT-RUNG TRIGGER (DESIGN section 4b(2)): they climb when the emitted shell body carries the +// declared capture policy -- a bounded reader that records total bytes, retained bytes and a +// truncation flag as it reads the child's stderr. That is the capability, and nothing less than it +// retires this refusal. +fn rust_realizes_shell_channel(c: ShellResultChannel) -> Bool { + match c { + ShellChanStdout => true + ShellChanStderr => true + ShellChanStdoutLines => true + ShellChanExitSuccess => true + ShellChanExitCode => true + ShellChanStderrTruncated => false + ShellChanStderrTotalBytes => false + ShellChanStderrRetainedBytes => false + } +} + +// The closed vocabulary of reasons a declared `transport shell` operation cannot be emitted, in +// the shape of FileEmissionRefusal and for the same reason: every arm is a fact read off the +// declaration, so a cause outside this list is a modelling gap to be added here rather than a +// message someone writes at a call site. +type ShellEmissionRefusal + = ShellOutputKeyNotModeled { key: String } + | ShellChannelNotRealizedByTarget { key: String, target_name: String } + +fn shell_emission_refusal_fact(refusal: ShellEmissionRefusal) -> String { + match refusal { + ShellOutputKeyNotModeled { key: k } => + concat("the declared output source '", k, "' names no member of the shell channel roster (stdout, stderr, stdout_lines, exit_success, exit_code, stderr_truncated, stderr_total_bytes, stderr_retained_bytes)") + ShellChannelNotRealizedByTarget { key: k, target_name: tn } => + concat("the shell channel '", k, "' has no realization in the ", tn, " backend") + } +} + +// The TARGET-NEUTRAL half: does every declared output field name a shell channel? +fn shell_binding_refusal(op_node: Node, source_indices: Map) -> ShellEmissionRefusal? { + match transport_output_channel_fields(op_node: op_node) + |> map(ch => transport_output_channel_of_field(ch: ch, source_indices: source_indices)) + |> filter(k => !is_modeled_shell_output_channel(key: k)) + |> first { + Present { value: k } => Present { value: ShellOutputKeyNotModeled { key: k } } + Absent => none + } +} + +// ONE predicate, consumed by the wall (which mints the diagnostic) and by the rust renderer (whose +// channel expression is total over the roster and therefore cannot answer for a refused field). +// The target gate is applied SECOND and separately, so an unmodeled key and an unrealizable +// channel stay two distinct answers. +fn shell_emission_refusal(op_node: Node, target: RenderTarget, source_indices: Map) -> ShellEmissionRefusal? { + match shell_binding_refusal(op_node: op_node, source_indices: source_indices) { + Present { value: r } => Present { value: r } + Absent => + if !target_realizes_shell_channels(target: target) { none } else { + match transport_output_channel_fields(op_node: op_node) + |> map(ch => transport_output_channel_of_field(ch: ch, source_indices: source_indices)) + |> filter(k => match shell_result_channel_of_key(key: k) { + Present { value: c } => !rust_realizes_shell_channel(c: c) + Absent => false + }) + |> first { + Present { value: k } => Present { value: ShellChannelNotRealizedByTarget { key: k, target_name: render_target_name(target: target) } } + Absent => none + } + } + } +} + +// Only the rust backend renders shell OUTPUT CHANNELS at all: the go and python dispatchers hand +// the shell arm to their own renderers, which do not project the declared output contract, and the +// dag serializer realizes no transport. Asking the rust channel-capability question of them would +// report a rust gap against a target that never consults the roster. +fn target_realizes_shell_channels(target: RenderTarget) -> Bool { + match target { + Rust => true + Python => false + Go => false + Dag => false + } +} + +fn unmodeled_shell_transport_operation_diagnostics(tm: TypedModule, item: Node, target: RenderTarget) -> List { + let env = tm.type_env + let si = env.source_indices + let module_name = authored_name(env: env, node: tm.module) + let service_name = authored_name(env: env, node: item) + let fallback = service_fallback_transport(item: item) + item.children |> flat_map(op_node => { + let t = effective_operation_transport(op_node: op_node, fallback: fallback) + if is_shell_transport_after_dispatch_precedence(t: t, source_indices: si) { + match shell_emission_refusal(op_node: op_node, target: target, source_indices: si) { + Present { value: refusal } => [make_error_node( + diagnostic: TransportEmissionNotModeled { + transport_kind: "shell", + service: service_name, + operation: authored_name(env: env, node: op_node), + declaring_module: module_name, + target: render_target_name(target: target), + missing_realization_fact: shell_emission_refusal_fact(refusal: refusal), + span: op_node.span + }, + module_name: module_name)] + Absent => [] + } + } else { [] } + }) +} + +// Reads the dispatch's precedence from the one authority that declares it, exactly as the file +// twin does -- a transport carrying more than one key is emitted by the FIRST arm that claims it. +fn is_shell_transport_after_dispatch_precedence(t: Node, source_indices: Map) -> Bool { + match classify_transport(t: t, source_indices: source_indices) { + Present { value: ShellTransport } => true + _ => false + } +} + +fn unmodeled_shell_transport_diagnostics(typed: ResolvedGraph, target: RenderTarget) -> List { + match target_emission_mode(target: target) { + SerializesSubstrate => [] + RealizesTransports => + typed.modules |> flat_map(tm => + tm.items + |> filter(item => is_service_item(item: item)) + |> flat_map(item => unmodeled_shell_transport_operation_diagnostics(tm: tm, item: item, target: target)) + ) + } +} + +// ONE wall for "this target cannot realize this declared transport", over both transports that ask +// the question. emit_artifact consulted the file half alone, which is why the shell half could +// fabricate for its whole life without a single diagnostic. +fn unmodeled_transport_diagnostics(typed: ResolvedGraph, target: RenderTarget) -> List { + concat( + unmodeled_file_transport_diagnostics(typed: typed, target: target), + unmodeled_shell_transport_diagnostics(typed: typed, target: target)) } fn file_transport_path_is_renderable(t: Node, source_indices: Map) -> Bool { diff --git a/src/v1/05_emit_rust.dag b/src/v1/05_emit_rust.dag index 8ffb8f45b35..da206313fcd 100644 --- a/src/v1/05_emit_rust.dag +++ b/src/v1/05_emit_rust.dag @@ -41,7 +41,7 @@ import v1.std.core { service_config_auth_input, service_config_auth_source, service_config_endpoint, expr_has_self_call, expr_has_non_tail_self_call, field_access_base, foreach_variable_at, expr_method_name_at, let_binding_name_at, record_lit_type_name_at, arg_name_at, arg_value, - arm_body, arm_pattern, arm_guard, field_init_node_name_at, field_init_node_value, + arm_body, arm_pattern, arm_guard, field_init_node_name_at, field_init_node_value, from_key_property_name, record_lit_named_field_value_optional, find_property, make_arg_node, make_named_expr_node, let_value, let_body, lambda_body, lambda_param_names_at, foreach_collection, foreach_body, method_receiver, method_arg_nodes, @@ -203,7 +203,13 @@ import v1.compiler.emit { bind_operation_transport, transport_binding_refusal_fact, FileVerb, FileRead, FileWrite, FileWriteOwnerOnly, FileDelete, FileList, FileResultField, FileResultChannel, FileChanSuccess, FileChanByteCount, - FileChanPath, FileChanError, FileChanContent + FileChanPath, FileChanError, FileChanContent, + ShellResultChannel, ShellChanStdout, ShellChanStderr, ShellChanStdoutLines, + ShellChanExitSuccess, ShellChanExitCode, ShellChanStderrTruncated, + ShellChanStderrTotalBytes, ShellChanStderrRetainedBytes, + shell_result_channel_of_key, shell_result_channel_key, + ShellEmissionRefusal, ShellChannelNotRealizedByTarget, shell_emission_refusal_fact, + transport_output_channel_of_field, render_target_name } import v1.compiler.languages { wrap_shared_type, @@ -7319,7 +7325,7 @@ fn emit_struct_field_from_child(struct_name: String, child: Node, generic_param_ let final_ty = rust_field_carrier_final_type(rt_field: rt_child, ty: ty, recursive_types: recursive_types, shared_types: shared_types, source_indices: env.source_indices, env: env) let needs_serde = struct_needs_serde(name: struct_name, emit_info: emit_info) let rename_attr = if !needs_serde { "" } else { - match child.properties |> filter(p => field_init_node_name_at(n: p, source_indices: env.source_indices) == "from_key") |> first { + match child.properties |> filter(p => field_init_node_name_at(n: p, source_indices: env.source_indices) == from_key_property_name()) |> first { Present { value: prop } => match field_init_node_value(n: prop).expr_data { ExprLiteral { value: lv } => match lv { LitStr { value: key } => concat(" ", apply_type_template1(template: rust_serde_rename_template_text(), arg0: key), "\n") @@ -13960,7 +13966,7 @@ fn emit_response_code_handling(op_node: Node, transport: Node, source_indices: M "let status = response.status().as_u16();\n", "match status {\n", arms |> join(separator: "\n"), - "\n _ => Err(format!(\"unexpected status code: {}\", status).into())\n", + "\n _ => ", emit_rust_boxed_error_return(message_expr: "format!(\"unexpected status code: {}\", status)"), "\n", "}") } } @@ -13984,7 +13990,7 @@ fn emit_response_arm(prop: Node, op_node: Node, use_from_key: Bool, transport: N concat(" ", pattern, " => { ", emit_plain_response_body(op_node: op_node, transport: transport, source_indices: source_indices), " },") } } else { - concat(" ", pattern, " => { let err_body = response.text().await.unwrap_or_default(); Err(format!(\"HTTP {}: {}\", status, err_body).into()) },") + concat(" ", pattern, " => { let err_body = response.text().await.unwrap_or_default(); ", emit_rust_boxed_error_return(message_expr: "format!(\"HTTP {}: {}\", status, err_body)"), " },") } } @@ -13993,6 +13999,18 @@ fn has_exit_prefix(name: String) -> Bool { else { substring(s: name, start: 0, end: 5) == "exit_" } } +// ONE AUTHORITY FOR THE EMITTED ERROR RETURN (DESIGN section 3). +// +// Every operation method is declared `-> Result>`, so the error arm +// owes a Box, and `String` reaches it only through the `Into` impl. The rest arms spelled that +// conversion and the SHELL arms did not, which is one fact with two spellings and only one of them +// correct: `Err(stderr)` against a boxed error does not compile, and nothing refused, because the +// emitter does not typecheck what it emits. A shared constructor makes the divergence unwritable +// rather than caught -- there is no second site left to spell it differently. +fn emit_rust_boxed_error_return(message_expr: String) -> String { + concat("Err(", message_expr, ".into())") +} + fn emit_exit_code_handling(op_node: Node, inferred: Node, source_indices: Map) -> String { let exit_props = op_node.properties |> filter(p => has_exit_prefix(name: field_init_node_name_at(n: p, source_indices: source_indices))) if exit_props |> count == 0 { @@ -14001,7 +14019,7 @@ fn emit_exit_code_handling(op_node: Node, inferred: Node, source_indices: Map any(p => field_init_node_name_at(n: p, source_indices: source_indices) == "exit_nonzero") let exit_arms = exit_props |> map(p => emit_exit_arm(prop: p, inferred: inferred, source_indices: source_indices)) let default_arm = if has_nonzero { "" } - else { concat("\n _ => { let stderr = String::from_utf8_lossy(&output.stderr).to_string(); Err(", emit_rust_host_to_dag_string_via_seam(host_expr: "stderr"), ") },") } + else { concat("\n _ => { let stderr = String::from_utf8_lossy(&output.stderr).to_string(); ", emit_rust_boxed_error_return(message_expr: emit_rust_host_to_dag_string_via_seam(host_expr: "stderr")), " },") } concat( "let exit_code = output.status.code().unwrap_or(-1);\n", "match exit_code {\n", @@ -14020,7 +14038,7 @@ fn emit_exit_arm(prop: Node, inferred: Node, source_indices: Map { ", emit_shell_return(inferred: inferred, source_indices: source_indices), " },") } else { - concat(" ", pattern, " => { let stderr = String::from_utf8_lossy(&output.stderr).to_string(); Err(", emit_rust_host_to_dag_string_via_seam(host_expr: "stderr"), ") },") + concat(" ", pattern, " => { let stderr = String::from_utf8_lossy(&output.stderr).to_string(); ", emit_rust_boxed_error_return(message_expr: emit_rust_host_to_dag_string_via_seam(host_expr: "stderr")), " },") } } @@ -14128,56 +14146,98 @@ fn emit_shell_argv_element(arg: Node, optional_params: Map, source } } +// EMISSION FOLLOWS THE DECLARATION, one field at a time. +// +// This used to read the `from` key of the SINGLE-FIELD case only and then, for every field of a +// multi-field product, fall through `Absent => "stdout"` -- so a three-output operation declaring +// exit_success, stdout and stderr emitted `Ok((stdout.clone(), stdout.clone(), stdout.clone()))` +// against a `Result<(bool, String, String), _>` signature it had rendered correctly one line +// above. Non-compiling Rust, with zero diagnostics. It was invisible for its whole life because +// the key it filtered on could never match (see from_key_property_name): every field took the +// Absent arm, so "always the declared channel" and "always stdout" produced identical bytes. +// +// The channel is now resolved through the shell roster for EVERY field, single or not, and the +// wall has already refused any operation whose fields do not all name a realizable member -- so +// there is no unresolvable field left for a default to answer for, and no default. +// +// The no-field arm is the one place stdout is still answered without a declaration asking for it, +// and it is not a default: a shell operation whose return type is not a product declares no output +// fields at all, so there is no `from` key to read and nothing to project. The whole result IS the +// child's stdout, which is the same answer v1_interpreter map_shell_outputs gives for an +// unresolved return type -- the oracle's rule, read once, not a guess made here. fn emit_shell_return(inferred: Node, source_indices: Map) -> String { let effective = unwrap_single_field_product(n: inferred) let is_product = is_product_type(n: effective) - let single_from = if is_product && effective.children |> count == 1 { - match effective.children |> first { - Present { value: ch } => child_from_key(ch: ch, source_indices: source_indices) - Absent => none - } - } else { none } - match single_from { - Present { value: channel } => - let is_optional = effective.return_cardinality == CardOptional - emit_shell_channel_expr(channel: channel, is_optional: is_optional) - Absent => - if is_product && effective.children |> count > 1 { - let needs_stderr = effective.children |> any(ch => - match child_from_key(ch: ch, source_indices: source_indices) { - Present { value: "stderr" } => true - _ => false - }) - let stderr_line = if needs_stderr { - "let stderr = String::from_utf8_lossy(&output.stderr).to_string();" - } else { "" } - let field_exprs = effective.children |> map(ch => - let is_optional = ch.return_cardinality == CardOptional - let channel = match child_from_key(ch: ch, source_indices: source_indices) { - Present { value: c } => c - Absent => "stdout" - } - emit_shell_channel_expr(channel: channel, is_optional: is_optional) - ) - let lines = if stderr_line != "" { concat(stderr_line, "\n") } else { "" } - concat(lines, "Ok((", field_exprs |> join(separator: ", "), "))") - } else { - let is_optional = effective.return_cardinality == CardOptional - emit_shell_channel_expr(channel: "stdout", is_optional: is_optional) - } + let fields = if is_product { effective.children } else { [] } + if fields |> count == 1 { + match fields |> first { + Present { value: ch } => + let is_optional = effective.return_cardinality == CardOptional || ch.return_cardinality == CardOptional + emit_shell_field_expr(ch: ch, is_optional: is_optional, source_indices: source_indices) + Absent => emit_shell_channel_expr(channel: ShellChanStdout, is_optional: effective.return_cardinality == CardOptional) + } + } else if fields |> count > 1 { + let needs_stderr = fields |> any(ch => shell_field_channel(ch: ch, source_indices: source_indices) == ShellChanStderr) + let stderr_line = if needs_stderr { + "let stderr = String::from_utf8_lossy(&output.stderr).to_string();\n" + } else { "" } + let field_exprs = fields |> map(ch => + emit_shell_field_expr(ch: ch, is_optional: ch.return_cardinality == CardOptional, source_indices: source_indices)) + concat(stderr_line, "Ok((", field_exprs |> join(separator: ", "), "))") + } else { + emit_shell_channel_expr(channel: ShellChanStdout, is_optional: effective.return_cardinality == CardOptional) } } -fn emit_shell_channel_expr(channel: String, is_optional: Bool) -> String { - let base = if channel == "exit_success" { "output.status.success()" } - else if channel == "stderr" { "stderr.clone()" } - else if channel == "stdout_lines" { +// The field's channel, read through the one authority. `unwrap_or ShellChanStdout` is NOT a +// fallback for an unmodeled key: shell_binding_refusal has already refused the operation whose +// field carries one, so this arm is the floor beneath that wall rather than a second opinion about +// it -- the same relation bind_file_verb's final arm has to file_emission_verb_refusal. +fn shell_field_channel(ch: Node, source_indices: Map) -> ShellResultChannel { + match shell_result_channel_of_key(key: transport_output_channel_of_field(ch: ch, source_indices: source_indices)) { + Present { value: c } => c + Absent => ShellChanStdout + } +} + +fn emit_shell_field_expr(ch: Node, is_optional: Bool, source_indices: Map) -> String { + emit_shell_channel_expr(channel: shell_field_channel(ch: ch, source_indices: source_indices), is_optional: is_optional) +} + +// TOTAL OVER THE ROSTER, which is the whole point of the roster being a coproduct. The trailing +// `else { "stdout.clone()" }` this replaces answered for every key it did not recognise -- so +// `exit_code: Int from "exit_code"`, declared 107 times in this corpus, emitted a String into an +// Int field. There is no such arm to write now: a new channel that reaches here without an +// expression fails to compile. +// +// The three stderr accounting channels are unreachable here, refused by the wall before emission, +// and they still owe the match an answer because the roster has them -- so they answer with the +// located refusal rather than with a value, which is what keeps the two readers from disagreeing +// if the wall is ever weakened. +fn emit_shell_channel_expr(channel: ShellResultChannel, is_optional: Bool) -> String { + let base = match channel { + ShellChanExitSuccess => "output.status.success()" + ShellChanExitCode => "output.status.code().unwrap_or(-1) as i64" + ShellChanStderr => "stderr.clone()" + ShellChanStdout => "stdout.clone()" + ShellChanStdoutLines => rust_shared_wrap_ctor(inner_expr: "stdout.lines().filter(|l| !l.is_empty()).map(|l| l.trim().to_string()).collect()") - } - else { "stdout.clone()" } + ShellChanStderrTruncated => emit_unrealizable_shell_channel(channel: channel) + ShellChanStderrTotalBytes => emit_unrealizable_shell_channel(channel: channel) + ShellChanStderrRetainedBytes => emit_unrealizable_shell_channel(channel: channel) + } if is_optional { concat("Some(", base, ")") } else { base } } +fn emit_unrealizable_shell_channel(channel: ShellResultChannel) -> String { + emit_error_expr( + message: shell_emission_refusal_fact(refusal: ShellChannelNotRealizedByTarget { + key: shell_result_channel_key(c: channel), + target_name: render_target_name(target: Rust) + }), + target: Rust) +} + // The rust realization handler for `transport file`, now bound to FACTS rather // than to a transport node. // diff --git a/src/v1/compile.dag b/src/v1/compile.dag index 792d0631d9c..b9b587ce537 100644 --- a/src/v1/compile.dag +++ b/src/v1/compile.dag @@ -63,7 +63,7 @@ import v1.compiler.infer { reconcile, reconcile_with_census_extra } import v1.compiler.infer_types { algebra_field_kind_name } import v1.compiler.emit_core_support { EmitResult, escape_json_string } -import v1.compiler.emit { unmodeled_file_transport_diagnostics } +import v1.compiler.emit { unmodeled_transport_diagnostics } import v1.compiler.emit_rust { emit_rust_selected } @@ -235,7 +235,7 @@ fn compile_bundle_error(message: String) -> ErrorNode { } fn emit_artifact(typed: ResolvedGraph, artifact: Artifact) -> EmitResult { - let unmodeled_transports = unmodeled_file_transport_diagnostics(typed: typed, target: artifact.target) + let unmodeled_transports = unmodeled_transport_diagnostics(typed: typed, target: artifact.target) if unmodeled_transports |> count > 0 { return EmitResult { files: [], diagnostics: unmodeled_transports } } diff --git a/src/v1/compiler_tests_rust.dag b/src/v1/compiler_tests_rust.dag index 3a5dd2399eb..03ee29ef466 100644 --- a/src/v1/compiler_tests_rust.dag +++ b/src/v1/compiler_tests_rust.dag @@ -2982,32 +2982,23 @@ fn ct_declared_type_conformance_witness_test() -> String { " }\n\n") } -fn ct_shell_service_output_projection_known_hole_probe_test() -> String { +fn ct_shell_service_output_projection_control_test() -> String { concat( - " // KNOWN-HOLE PROBE (not a desired-behavior control), DESIGN section 4b(4).\n", + " // REGRESSION CONTROL for the shell service-emission fabrication, DESIGN §4b(4).\n", " //\n", - " // The shell service-emission path FABRICATES rather than refuses: every declared\n", - " // output field is bound to `stdout`, whatever source the declaration named, and the\n", - " // error arm returns a bare String against a declared Box.\n", - " // Both produce Rust that does not compile, with ZERO diagnostics -- section 5's\n", - " // fabricated-plausible-output arm, in the emitter.\n", + " // THIS PROBE WAS AUTHORED RED-ASSERTING and has now FLIPPED. It landed asserting the WRONG\n", + " // behaviour -- every declared output field bound to `stdout` whatever source it named -- as a\n", + " // known-hole probe. The wall has landed, so per §4b(4) it does not retire: it inverts and\n", + " // stays enrolled as the permanent control that the class does not come back.\n", " //\n", - " // The three-output fixture is load-bearing. The two-field case this was first seen\n", - " // on (an operation declaring `success: Bool from \"exit_success\"` beside\n", - " // `stdout: String from \"stdout\"`) cannot distinguish \"wrong tuple index\" from \"the\n", - " // declared source never arrived\": with two fields, \"always the first output\" and\n", - " // \"always stdout\" produce the same bytes. Three fields separate them, and the\n", - " // ABSENCE of the `let stderr = ...` prelude line is the positive evidence -- that\n", - " // line is emitted only when some field claims the stderr channel, so its absence\n", - " // proves the `from \"stderr\"` field was invisible to the renderer rather than\n", - " // mis-ordered. child_from_key returns Absent for all three.\n", - " //\n", - " // WHEN THE WALL LANDS THIS PROBE MUST FLIP and become a permanent regression\n", - " // control: the emitted body must bind each field to its named source, box the error\n", - " // arm, and REFUSE -- typed and located, naming the field and the unresolvable\n", - " // source -- for any source it cannot realize.\n", + " // The three-output fixture is still the load-bearing part. Two fields cannot distinguish\n", + " // \"wrong tuple index\" from \"the declared source never arrived\": with two fields, \"always the\n", + " // first output\" and \"always stdout\" produce the same bytes. Three separate them, and the\n", + " // stderr prelude line -- emitted only when some field claims the stderr channel -- is the\n", + " // positive evidence that the `from \"stderr\"` field is now VISIBLE to the renderer, where its\n", + " // absence was the evidence that it was not.\n", " #[test]\n", - " fn shell_service_output_projection_fabricates_stdout_known_hole_probe() {\n", + " fn shell_service_output_projection_binds_each_declared_channel() {\n", " let result = std::thread::Builder::new()\n", " .stack_size(32 * 1024 * 1024)\n", " .spawn(|| {\n", @@ -3026,8 +3017,9 @@ fn ct_shell_service_output_projection_known_hole_probe_test() -> String { " .collect();\n", " assert!(\n", " errors.is_empty(),\n", - " \"KNOWN HOLE today: the emitter does not refuse; it fabricates silently. \\\n", - " When the wall lands this becomes the refusal assertion. Got: {:?}\",\n", + " \"every channel this fixture declares is realizable in Rust, so the wall must \\\n", + " ADMIT it -- a refusal here means the roster or the capability predicate is \\\n", + " wrong, not that the fixture is. Got: {:?}\",\n", " errors\n", " );\n", " let emitted = r\n", @@ -3037,7 +3029,6 @@ fn ct_shell_service_output_projection_known_hole_probe_test() -> String { " .map(|f| f.content.clone())\n", " .expect(\"service module must emit src/probe.rs\");\n", "\n", - " // The signature reads the declaration correctly...\n", " assert!(\n", " emitted.contains(\n", " \"-> Result<(bool, String, String), Box>\"\n", @@ -3045,26 +3036,258 @@ fn ct_shell_service_output_projection_known_hole_probe_test() -> String { " \"signature must project the three declared output types, got:\\n{}\",\n", " emitted\n", " );\n", - " // ...and the body then ignores every declared source.\n", + " // THE FLIP. This read `Ok((stdout.clone(), stdout.clone(), stdout.clone()))`.\n", " assert!(\n", - " emitted.contains(\"Ok((stdout.clone(), stdout.clone(), stdout.clone()))\"),\n", - " \"KNOWN HOLE: every output field is bound to stdout regardless of its \\\n", - " declared source. If this assertion fails the wall may have landed -- \\\n", - " flip this probe to assert the correct per-channel binding. Got:\\n{}\",\n", + " emitted.contains(\n", + " \"Ok((output.status.success(), stdout.clone(), stderr.clone()))\"\n", + " ),\n", + " \"each output field must bind to the channel its `from` names, in declaration \\\n", + " order, got:\\n{}\",\n", " emitted\n", " );\n", - " // The stderr prelude is the absence that proves the source was never read.\n", + " // THE FLIP. This asserted the prelude was ABSENT -- the evidence that the\n", + " // `from \"stderr\"` field was invisible to the renderer.\n", " assert!(\n", - " !emitted.contains(\"String::from_utf8_lossy(&output.stderr)\"),\n", - " \"KNOWN HOLE: the `from \\\"stderr\\\"` field is invisible to the renderer, \\\n", - " so no stderr prelude line is emitted. Got:\\n{}\",\n", + " emitted.contains(\"let stderr = String::from_utf8_lossy(&output.stderr).to_string();\"),\n", + " \"the stderr prelude must be emitted when a field claims the stderr channel, \\\n", + " got:\\n{}\",\n", " emitted\n", " );\n", " })\n", " .expect(\"failed to spawn thread\")\n", " .join();\n", - " result\n", - " .expect(\"shell_service_output_projection_fabricates_stdout_known_hole_probe panicked\");\n", + " result.expect(\"shell_service_output_projection_binds_each_declared_channel panicked\");\n", + " }\n", + "\n") +} + +fn ct_shell_output_channel_roster_wall_test() -> String { + concat( + " // DISCRIMINATING RED for the shell channel roster, and its positive control.\n", + " //\n", + " // The roster is a closed coproduct, so a declared output source outside it has no expression\n", + " // to render and must REFUSE. Before the wall, `emit_shell_channel_expr`'s trailing `else`\n", + " // answered `stdout.clone()` for every key it did not recognise -- which is how\n", + " // `exit_code: Int from \"exit_code\"`, declared 107 times in this corpus, emitted a String into\n", + " // an Int field with no diagnostic.\n", + " //\n", + " // Two REDs, because the two refusals are different facts with different remedies: a key that\n", + " // names no channel at all is a MODELLING gap, and a channel Rust cannot render is a BACKEND\n", + " // CAPABILITY gap. A single fixture would leave one of the two arms unexercised.\n", + " #[test]\n", + " fn shell_output_channel_roster_refuses_and_admits() {\n", + " let result = std::thread::Builder::new()\n", + " .stack_size(32 * 1024 * 1024)\n", + " .spawn(|| {\n", + " let compile = |content: &str| {\n", + " let source = std::rc::Rc::new(crate::v1_compiler_compile::SourceFile {\n", + " path: \"probe.dag\".to_string(),\n", + " content: content.to_string(),\n", + " });\n", + " crate::v1_compiler_compile::compile_sources(\n", + " std::rc::Rc::new(im::vector![source]),\n", + " crate::v1_compiler_artifact::RenderTarget::Rust,\n", + " )\n", + " };\n", + " let transport_refusals = |r: &crate::v1_compiler_compile::PipelineResult| {\n", + " r.diagnostics\n", + " .iter()\n", + " .filter(|d| {\n", + " matches!(\n", + " *d.diagnostic,\n", + " crate::v1_std_core::CompilerDiagnostic::TransportEmissionNotModeled { .. }\n", + " )\n", + " })\n", + " .count()\n", + " };\n", + "\n", + " // RED 1 -- a source naming no member of the roster.\n", + " let unmodeled = compile(\n", + " \"module probe\\nservice Probe {\\n operation Version {\\n input {}\\n output { out: String from \\\"not_a_channel\\\" }\\n transport shell { argv: [\\\"git\\\"] }\\n }\\n}\\n\",\n", + " );\n", + " assert!(\n", + " transport_refusals(&unmodeled) > 0,\n", + " \"an output source outside the shell channel roster must refuse, got: {:?}\",\n", + " unmodeled.diagnostics\n", + " );\n", + "\n", + " // RED 2 -- a real roster member with no Rust realization. stderr_total_bytes\n", + " // reports a truncation policy the emitted body models nothing of.\n", + " let unrealizable = compile(\n", + " \"module probe\\nservice Probe {\\n operation Version {\\n input {}\\n output { total: Int from \\\"stderr_total_bytes\\\" }\\n transport shell { argv: [\\\"git\\\"] }\\n }\\n}\\n\",\n", + " );\n", + " assert!(\n", + " transport_refusals(&unrealizable) > 0,\n", + " \"a roster channel with no Rust realization must refuse rather than fabricate, got: {:?}\",\n", + " unrealizable.diagnostics\n", + " );\n", + "\n", + " // POSITIVE CONTROL -- every realizable channel, including the exit_code channel\n", + " // that used to emit `stdout.clone()` into an Int field.\n", + " let green = compile(\n", + " \"module probe\\nservice Probe {\\n operation Version {\\n input {}\\n output {\\n ok: Bool from \\\"exit_success\\\"\\n code: Int from \\\"exit_code\\\"\\n out: String from \\\"stdout\\\"\\n err: String from \\\"stderr\\\"\\n lines: List from \\\"stdout_lines\\\"\\n }\\n transport shell { argv: [\\\"git\\\"] }\\n }\\n}\\n\",\n", + " );\n", + " assert!(\n", + " transport_refusals(&green) == 0,\n", + " \"every channel here is realizable in Rust and must be admitted, got: {:?}\",\n", + " green.diagnostics\n", + " );\n", + " let emitted = green\n", + " .files\n", + " .iter()\n", + " .find(|f| f.path == \"src/probe.rs\")\n", + " .map(|f| f.content.clone())\n", + " .expect(\"service module must emit src/probe.rs\");\n", + " assert!(\n", + " emitted.contains(\"output.status.code().unwrap_or(-1) as i64\"),\n", + " \"the exit_code channel must render the exit code, not stdout, got:\\n{}\",\n", + " emitted\n", + " );\n", + " })\n", + " .expect(\"failed to spawn thread\")\n", + " .join();\n", + " result.expect(\"shell_output_channel_roster_refuses_and_admits panicked\");\n", + " }\n", + "\n") +} + +fn ct_shell_exit_arm_boxed_error_test() -> String { + concat( + " // The emitted error arm must be BOXED. Every operation method is declared\n", + " // `-> Result>`, and the shell exit arms returned a bare String\n", + " // against it -- Rust that does not compile, emitted with no diagnostic, while the rest arms\n", + " // one screen away spelled the conversion correctly. Both now read the one constructor, so this\n", + " // control is what keeps the two spellings from diverging again.\n", + " #[test]\n", + " fn shell_exit_arm_error_return_is_boxed() {\n", + " let result = std::thread::Builder::new()\n", + " .stack_size(32 * 1024 * 1024)\n", + " .spawn(|| {\n", + " let source = std::rc::Rc::new(crate::v1_compiler_compile::SourceFile {\n", + " path: \"probe.dag\".to_string(),\n", + " content: \"module probe\\nservice Probe {\\n operation Version {\\n input {}\\n output { out: String from \\\"stdout\\\" }\\n transport shell { argv: [\\\"git\\\", \\\"--version\\\"] }\\n exit {\\n 0 => Unit\\n nonzero => String \\\"failed\\\"\\n }\\n }\\n}\\n\".to_string(),\n", + " });\n", + " let r = crate::v1_compiler_compile::compile_sources(\n", + " std::rc::Rc::new(im::vector![source]),\n", + " crate::v1_compiler_artifact::RenderTarget::Rust,\n", + " );\n", + " let emitted = r\n", + " .files\n", + " .iter()\n", + " .find(|f| f.path == \"src/probe.rs\")\n", + " .map(|f| f.content.clone())\n", + " .expect(\"service module must emit src/probe.rs\");\n", + " assert!(\n", + " emitted.contains(\"Err(stderr.into())\"),\n", + " \"the shell exit arm must box its error to match the declared \\\n", + " Box, got:\\n{}\",\n", + " emitted\n", + " );\n", + " assert!(\n", + " !emitted.contains(\"Err(stderr)\"),\n", + " \"no unboxed `Err(stderr)` may survive anywhere in the emitted module, got:\\n{}\",\n", + " emitted\n", + " );\n", + " })\n", + " .expect(\"failed to spawn thread\")\n", + " .join();\n", + " result.expect(\"shell_exit_arm_error_return_is_boxed panicked\");\n", + " }\n", + "\n") +} +fn ct_function_value_adapter_bound_alignment_control_test() -> String { + concat( + " // REGRESSION CONTROL for a property that holds BY CONSTRUCTION today, and which nothing\n", + " // else would notice losing. DESIGN section 4b(4): the evidence stays enrolled.\n", + " //\n", + " // A function-VALUE realizes as `Rc`; a function-typed PARAMETER realizes as an\n", + " // `impl Fn(..) + Clone` bound, and `Rc` carries no blanket `impl Fn`, so passing the first\n", + " // into the second is an E0277 seam. rust_call_arg_function_value_adapt closes it by wrapping\n", + " // the argument in a forwarding closure, and it decides on\n", + " // `param_node_type_expr(n: param).params |> count > 0`.\n", + " //\n", + " // THAT IS THE SAME EXPRESSION ON THE SAME NODE that emit_param/emit_rust_param_type use to\n", + " // decide whether to emit the `impl Fn` bound at all. So the adapter fires exactly where the\n", + " // seam exists, and cannot fire where it does not -- the two predicates cannot disagree while\n", + " // they remain one expression. This control is what notices if they are ever forked.\n", + " //\n", + " // WHY THE BARE TYPE VARIABLE IS THE DISCRIMINATING HALF. It was reported (by me, wrongly) as a\n", + " // silent defect: a parameter declared `T` has arity 0, so no adapter is emitted, and nothing\n", + " // refuses. The bytes below are the refutation -- `T` renders as a plain generic with NO Fn\n", + " // bound, so there is no seam to close, and adapting there would be a fabricated repair.\n", + " // Arity is invariant under substitution anyway: instantiation changes an arrow's type\n", + " // ARGUMENTS, never its parameter count.\n", + " #[test]\n", + " fn function_value_adapter_fires_exactly_where_the_impl_fn_bound_is_emitted() {\n", + " let result = std::thread::Builder::new()\n", + " .stack_size(32 * 1024 * 1024)\n", + " .spawn(|| {\n", + " let source = std::rc::Rc::new(crate::v1_compiler_compile::SourceFile {\n", + " path: \"probe.dag\".to_string(),\n", + " content: \"module probe\\nfn add_one(x: Int) -> Int { x + 1 }\\nfn make_adder() -> fn(Int) -> Int { fn(n) { add_one(x: n) } }\\nfn hold_tv(value: T) -> T { value }\\nfn apply_arrow(f: fn(Int) -> Int, v: Int) -> Int { f(v) }\\nfn through_type_variable() -> Int { apply_arrow(f: hold_tv(value: make_adder()), v: 1) }\\nfn through_declared_arrow() -> Int { apply_arrow(f: make_adder(), v: 1) }\\n\".to_string(),\n", + " });\n", + " let r = crate::v1_compiler_compile::compile_sources(\n", + " std::rc::Rc::new(im::vector![source]),\n", + " crate::v1_compiler_artifact::RenderTarget::Rust,\n", + " );\n", + " let errors: Vec<_> = r\n", + " .diagnostics\n", + " .iter()\n", + " .filter(|d| crate::v1_std_core::is_error_diagnostic(d.diagnostic.clone()))\n", + " .collect();\n", + " assert!(\n", + " errors.is_empty(),\n", + " \"the fixture is ordinary admissible source; a refusal here is a compiler \\\n", + " defect rather than a defect in the fixture. Got: {:?}\",\n", + " errors\n", + " );\n", + " let emitted = r\n", + " .files\n", + " .iter()\n", + " .find(|f| f.path == \"src/probe.rs\")\n", + " .map(|f| f.content.clone())\n", + " .expect(\"probe module must emit src/probe.rs\");\n", + "\n", + " // (1) the producer side of the seam: a function VALUE is the Rc carrier.\n", + " assert!(\n", + " emitted.contains(\"pub fn make_adder() -> Rc i64>\"),\n", + " \"a function-valued return must realize as the Rc carrier, got:\\n{}\",\n", + " emitted\n", + " );\n", + " // (2) a DECLARED-ARROW parameter carries the impl Fn bound -- the seam exists here.\n", + " assert!(\n", + " emitted.contains(\"pub fn apply_arrow(f: impl Fn(i64) -> i64 + Clone, v: i64)\"),\n", + " \"a declared-arrow parameter must carry the impl Fn bound, got:\\n{}\",\n", + " emitted\n", + " );\n", + " // (3) a BARE TYPE VARIABLE parameter carries NO Fn bound -- no seam exists here.\n", + " assert!(\n", + " emitted.contains(\"pub fn hold_tv(value: T) -> T\"),\n", + " \"a bare type-variable parameter must render as a plain generic with no Fn \\\n", + " bound -- if this ever gains one, the adapter predicate must move with it, \\\n", + " got:\\n{}\",\n", + " emitted\n", + " );\n", + " // (4) THE ALIGNMENT, both directions. The adapter wraps the argument at the\n", + " // impl-Fn-bound parameter, and does NOT wrap it at the type-variable parameter --\n", + " // whose argument is equally a call result, so shape alone does not explain it.\n", + " assert!(\n", + " emitted.contains(\n", + " \"apply_arrow({ let __adapt_f = hold_tv(make_adder()); move |__adapt_a0| __adapt_f(__adapt_a0) }, 1)\"\n", + " ),\n", + " \"the adapter must close the seam at the impl Fn parameter, got:\\n{}\",\n", + " emitted\n", + " );\n", + " assert!(\n", + " !emitted.contains(\"hold_tv({ let __adapt_f\"),\n", + " \"no adapter may be injected at a parameter that carries no Fn bound -- that \\\n", + " would be a fabricated repair of a seam that does not exist, got:\\n{}\",\n", + " emitted\n", + " );\n", + " })\n", + " .expect(\"failed to spawn thread\")\n", + " .join();\n", + " result.expect(\"function_value_adapter_fires_exactly_where_the_impl_fn_bound_is_emitted panicked\");\n", " }\n", "\n") } @@ -3085,7 +3308,10 @@ fn compiler_tests_source() -> String { ct_call_shape_duplicate_wall_witness_test(), ct_function_value_named_application_controls_witness_test(), ct_function_value_field_method_known_hole_probe_test(), - ct_shell_service_output_projection_known_hole_probe_test(), + ct_shell_service_output_projection_control_test(), + ct_shell_output_channel_roster_wall_test(), + ct_shell_exit_arm_boxed_error_test(), + ct_function_value_adapter_bound_alignment_control_test(), ct_method_existence_wall_witness_test(), ct_declared_type_conformance_witness_test(), ct_sole_constructor_test(),