diff --git a/dag/gunbc/recurring_failure_mode/a_sibling_refusal_masks_a_ratchet_rows_observation.dag b/dag/gunbc/recurring_failure_mode/a_sibling_refusal_masks_a_ratchet_rows_observation.dag new file mode 100644 index 00000000000..465232d8e4c --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/a_sibling_refusal_masks_a_ratchet_rows_observation.dag @@ -0,0 +1,28 @@ +module gunbc.recurring_failure_mode.a_sibling_refusal_masks_a_ratchet_rows_observation + +import std.types { NonEmptyStr } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data a_sibling_refusal_masks_a_ratchet_rows_observation: RecurringFailureMode = RecurringFailureMode { + identity: "a_sibling_refusal_masks_a_ratchet_rows_observation" as NonEmptyStr, + + receipts: [ + "**an equality ratchet row reads observed=0 and is deleted as dissolved, while the debt it counts is still present: a SIBLING refusal on the same caller fired first and the row's own requirement was never evaluated.** The deletion looks like the ratchet working in the shrinking direction, so nothing flags it.", + + "SPECIMEN, gunbc#12047, 2026-09-23. The whole-corpus census re-deriving the resource-requirement frontier gunbc#12047 carried (withdrawn in that PR when D13 was ruled the authority) read 0 for the Filesystem row on gunbc.instruments.github_app_acquire census_app_acquisition_receipt_for, and the row was deleted with 36 others. The caller also carried an unestablished NETWORK requirement, refused first. Once Network was authored, the next census refused the same caller for Filesystem: the debt had never dissolved.", + + "CORRECTION, AND WHICH PART EACH CAUSE PRODUCED. The ring-3 census then refused that caller for Filesystem EVEN AFTER it declared `uses ... fs: std.resources.Filesystem`, which masking cannot explain. A two-module fixture located a second cause in the requirement join itself: v1.compiler.infer resource_declaration_identity compared the node each side's spelling resolved to, and a bare `Filesystem` resolves to the kernel-minted node while `std.resources.Filesystem` resolves to the corpus declaration, so a qualified callee refused every caller. The callee here, gunbc.github_app_acquisition census_app_acquisition_standing, spells its requirement qualified. So the specimen is two defects: the MASKING (the Network refusal hid the Filesystem evaluation in census 1, a real instance of this class) and the IDENTITY FORK (the Filesystem refusal persisted in census 3 because the join could not see the caller's binding), REPAIRED ON gunbc#12047 after a first attempt was refuted. The kernel-wins precedence predicate (overlay_skips_kernel_name) did NOT fix it -- Filesystem is not in std.types kernel_type_set -- and the fixture refuted that attempt, which was withdrawn. The stand-in was minted by v1.compiler.infer nominal_ref_node (via local_binding_for_item / nominal_type_ref); neat-lynx-128 located it and built the repair (parked as #12174), which #12047 carries: the stand-in now carries its declaration's spans and the join compares DeclarationRef. Measured by neat-lynx-128 over per-module compiles of the frontier's caller modules, 8 Filesystem frontier rows observed 0 once the join compared DeclarationRef: those were false refusals from the caller-environment re-lookup, not debt.", + + "A SECOND, RELATED SHAPE, from the same re-derivation: authoring a `uses` clause on a caller moves the requirement to that caller's callers, and one census reports only the ring it can see. Closing the propagation took one whole-corpus run per ring (three runs of about 20 minutes and 16 GiB each), where a derivation of the requirement's closure over the call graph would have answered in one.", + + "RECOGNITION RULE. Any ratchet whose observation for one key is produced only when no other refusal on the same subject fires first. Ask: is observed=0 a reading of the key, or the absence of a reading? A deletion is safe only when the subject carries no other unestablished requirement at the same boundary.", + + "RUNG FOUND AT: SILENT WRONGNESS (a debt row deleted while its debt stood; caught only because a later run re-evaluated the caller). RUNG NOW: MITIGATABLE, because the re-derivation re-runs after authoring. ATTAINABLE CEILING: MECHANICALLY PREVENTABLE, since both halves are decidable. NEXT TRIGGER: a census that evaluates every requirement of a caller independently of its siblings' refusals and reports observed per key as a reading, not an absence. For the second shape, the trigger is a resource-requirement closure derived over the call graph, sufficient to name every caller a new `uses` clause will reach before the edit lands.", + ], + + evidence: [ + DeclarationRef { module_path: "v1.compiler.infer", decl_name: "established_resource_binding", field: WholeDeclaration }, + DeclarationRef { module_path: "gunbc.emit_stage_blocking_population_census", decl_name: "census_run_invocation", field: WholeDeclaration }, + ], +} diff --git a/dag/gunbc/recurring_failure_mode/accepted_source_emits_uncompilable_target.dag b/dag/gunbc/recurring_failure_mode/accepted_source_emits_uncompilable_target.dag index 60b3009494f..818fc2ed12d 100644 --- a/dag/gunbc/recurring_failure_mode/accepted_source_emits_uncompilable_target.dag +++ b/dag/gunbc/recurring_failure_mode/accepted_source_emits_uncompilable_target.dag @@ -271,6 +271,7 @@ data accepted_source_emits_uncompilable_target: RecurringFailureMode = Recurring "THREE THINGS THE ABOVE KEEPS APART, because collapsing them is how a refresh cadence gets mistaken for a wall. (1) OBSERVATION: an on-demand run exposes a failure -- what the instrument does when someone invokes it. (2) REGRESSION PROTECTION: an applicable REQUIRED acceptance consumer stops the failure landing unnoticed -- which needs a phase that emits a closure AND COMPILES it, with the target compiler's refusal reaching the lane, not merely a phase that emits. (3) FRONTIER TRUTH: a standing derived from evidence applicable to the claimed subject and contract. gunbc.compiler_frontend_program_status reported SelfHostCorpusEmitsCleanly as Clear across the 2026-09-20/21 window while the capability was down, which is DESIGN section 4b(1) rung inflation -- but the repair for (3) is NOT a refresh cadence: a recent receipt can describe the wrong subject, and an older receipt stays applicable while its relevant inputs are demonstrably unchanged. Applicability, not recency, is what (3) needs.", + "OCCURRENCE, 2026-09-22 (smart-tern-891, template-to-.dag call contract lane, gunbc#12047). A declaration `fn reads(path: String) -> String uses fs: Filesystem` called from `fn plain(path: String) -> String { reads(path: path) }` was accepted with zero blocking diagnostics and emitted `reads(path.clone(), &fs).await?` inside a synchronous fn that binds no `fs`: v1.compiler.emit_rust emit_typed_call appended the CALLEE's resource binding name unconditionally. Arity agreed; the argument named nothing in the caller's scope. REPAIRED AT THE EMITTER, AT RUNG 2 AND NO HIGHER: emit_typed_call now asks v1.compiler.infer established_resource_binding for the CALLER's binding of each requirement, matched by resolved resource declaration, and renders a located compile_error! where there is none, so the target compiler refuses the artifact instead of accepting a call to an unbound identifier. THE ARTIFACT IS STILL WRITTEN: nothing refuses before publishing. Executing evidence: test.claim.resource_requirement_call_admission_witness (two reds that agree on arity and render the located refusal, one with no resource and one with the WRONG resource; a positive control whose caller binds the resource under a different name and emits `&filesystem`; a boundary control). A TYPECHECK WALL WAS BUILT AND WITHDRAWN, and the reason is the next trigger: it required every caller to AUTHOR a `uses` clause, which is the E1b migration DESIGN D13 superseded (dag/gunbc/plans/demand_engine_program.dag) -- under D13 a transparent function's dependency demand is DERIVED, not authored, and #12125 deleted 174 authored rows as restatements. NEXT-RUNG TRIGGER, A CAPABILITY: D13's DependencyDemand carrier, derived per resource DECLARATION (and logical subject) and carrying a binding identity the emitter can pass -- sufficient that a call whose caller's derived demand does not cover its callee's requirement refuses at the typecheck, before emission, and that a derived caller receives a binding rather than the compile_error! arm. IDENTITY BY DECLARATION, NOT BY NODE: a first version of the kept fold compared resolved NODES, and a bare resource name resolved to a kernel-spanned stand-in (v1.compiler.infer nominal_ref_node) while `std.resources.X` resolved to the declaration, so a caller and callee that spelled one resource differently rendered compile_error! where main had compiled (review 70670). The fold now reads the DeclarationRef off the node resolution selected in the declaring module (v1.compiler.infer resource_declaration_identity over v1.compiler.infer_env declaration_ref_of_type_node), and the stand-in carries its declaration's spans; the repair was built by neat-lynx-128 (parked as #12174) and is witnessed by the spelling-fork pair in test.claim.resource_requirement_call_admission_witness.", ], diff --git a/dag/test/claim/resource_requirement_call_admission_witness_test.dag b/dag/test/claim/resource_requirement_call_admission_witness_test.dag new file mode 100644 index 00000000000..70af8d1db1a --- /dev/null +++ b/dag/test/claim/resource_requirement_call_admission_witness_test.dag @@ -0,0 +1,117 @@ +module test.claim.resource_requirement_call_admission_witness + +import std.types { String, Bool } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + + +// A RESOURCE ARGUMENT AT A CALL SITE IS THE CALLER'S BINDING, OR A LOCATED REFUSAL -- NEVER THE +// CALLEE'S SPELLING. The subject is v1.compiler.emit_rust emit_typed_call, which asks +// v1.compiler.infer established_resource_binding for the caller's binding of each resource the +// callee requires. Before it, the emitter appended the CALLEE's binding name: the emitted call agreed +// with the declaration on arity while its argument named nothing in the caller's scope (measured +// 2026-09-22: `reads(path.clone(), &fs).await?` inside a synchronous fn that binds no `fs`). +// +// RUNG, STATED HONESTLY: MECHANICALLY PREVENTABLE (2). An unestablished requirement renders a +// located compile_error!, so the target compiler refuses the artifact -- but the artifact is still +// WRITTEN, and nothing refuses before publishing. The typecheck wall that would have refused +// earlier was withdrawn because whether a caller ESTABLISHES a resource is owned by D13's derived +// DependencyDemand, which does not exist yet; its return is the next-rung trigger on +// gunbc.recurring_failure_mode accepted_source_emits_uncompilable_target. + +// THE CALLEE, shared by every row: one parameter, one resource requirement. +data callee_source: String = "fn reads(path: String) -> String uses fs: Filesystem {\n filesystem_read(path)\n}\n" + +// RED 1: arity agrees, no resource established. The pre-repair emitter published this. +data red_plain_caller_source: String = "module probe_resource_plain_caller\nimport std.types { String }\nimport std.resources { Filesystem }\nfn reads(path: String) -> String uses fs: Filesystem {\n filesystem_read(path)\n}\nfn plain(path: String) -> String {\n reads(path: path)\n}\n" + +// RED 2: arity agrees, the caller establishes a resource -- the WRONG one. A wall that only asked +// "does the caller have any `uses`" admits this; the declaration-identity comparison refuses it. +data red_wrong_resource_caller_source: String = "module probe_resource_wrong_caller\nimport std.types { String }\nimport std.resources { Filesystem, Network }\nfn reads(path: String) -> String uses fs: Filesystem {\n filesystem_read(path)\n}\nfn networked(path: String) -> String uses net: Network {\n reads(path: path)\n}\n" + +// GREEN CONTROL: the caller establishes the same resource under a DIFFERENT binding name and spells +// the type by its qualified path. Name comparison would refuse this; declaration comparison admits it. +data green_renamed_binding_caller_source: String = "module probe_resource_renamed_caller\nimport std.types { String }\nimport std.resources { Filesystem }\nfn reads(path: String) -> String uses fs: Filesystem {\n filesystem_read(path)\n}\nfn forwards(path: String) -> String uses filesystem: std.resources.Filesystem {\n reads(path: path)\n}\n" + +// BOUNDARY CONTROL: a callee with no requirement is callable from a plain fn, so the wall is +// about the requirement and not about calls in general. +data green_no_requirement_source: String = "module probe_resource_no_requirement\nimport std.types { String }\nfn plain_reads(path: String) -> String {\n path\n}\nfn plain(path: String) -> String {\n plain_reads(path: path)\n}\n" + +test fn a_plain_callers_emitted_call_is_a_located_refusal_not_the_callees_spelling() -> Bool { + compile_dag_rust_emit_check( + red_plain_caller_source, + "src/probe_resource_plain_caller.rs", + ["compile_error!(", "requires resource fs that the calling declaration does not establish"], + ["&fs)"] + ) +} + +test fn a_wrong_resource_callers_emitted_call_is_a_located_refusal_by_declaration_identity() -> Bool { + compile_dag_rust_emit_check( + red_wrong_resource_caller_source, + "src/probe_resource_wrong_caller.rs", + ["compile_error!(", "requires resource fs that the calling declaration does not establish"], + ["&fs)"] + ) +} + +// POSITIVE CONTROL: the caller establishes the same resource under a DIFFERENT binding name, and +// the emitted call carries the CALLER's binding. +test fn the_emitted_call_passes_the_callers_binding_not_the_callees_spelling() -> Bool { + compile_dag_rust_emit_check( + green_renamed_binding_caller_source, + "src/probe_resource_renamed_caller.rs", + ["reads(path.clone(), &filesystem).await?", "filesystem: &Filesystem"], + ["&fs)", "compile_error!("] + ) +} + +// BOUNDARY CONTROL: a callee with no requirement emits an ordinary call, so the arm is about the +// requirement and not about calls in general. +test fn a_callee_without_a_requirement_emits_an_ordinary_call() -> Bool { + compile_dag_rust_emit_check( + green_no_requirement_source, + "src/probe_resource_no_requirement.rs", + ["plain_reads("], + ["compile_error!("] + ) +} + +// THE SPELLING-FORK PAIR (review 70670; the repair ported from neat-lynx-128's parked #12174). The +// callee spells its requirement QUALIFIED and the caller spells it BARE, under the same binding name. +// On main the emitter passed the callee's name and this compiled; with the join comparing resolved +// NODES it rendered compile_error!, because a bare name resolved to a kernel-spanned stand-in and the +// qualified one to the declaration. Identity is now the DeclarationRef recovered from the resolved +// node. The bare/bare row must stay admitted, and a qualified callee whose caller establishes a +// DIFFERENT resource must still refuse -- identity, not spelling-blindness, is what admits the pair. +data green_qualified_callee_bare_caller_source: String = "module probe_resource_spelling_fork\nimport std.types { String }\nimport std.resources { Filesystem }\nfn reads(path: String) -> String uses fs: std.resources.Filesystem {\n filesystem_read(path)\n}\nfn forwards(path: String) -> String uses fs: Filesystem {\n reads(path: path)\n}\n" +data green_bare_callee_bare_caller_source: String = "module probe_resource_bare_bare\nimport std.types { String }\nimport std.resources { Filesystem }\nfn reads(path: String) -> String uses fs: Filesystem {\n filesystem_read(path)\n}\nfn forwards(path: String) -> String uses fs: Filesystem {\n reads(path: path)\n}\n" +data red_qualified_callee_other_bare_resource_source: String = "module probe_resource_spelling_fork_wrong\nimport std.types { String }\nimport std.resources { Network }\nfn reads(path: String) -> String uses fs: std.resources.Filesystem {\n filesystem_read(path)\n}\nfn networked(path: String) -> String uses net: Network {\n reads(path: path)\n}\n" + +test fn a_bare_caller_passes_its_binding_to_a_qualified_callee() -> Bool { + compile_dag_rust_emit_check( + green_qualified_callee_bare_caller_source, + "src/probe_resource_spelling_fork.rs", + ["reads(path.clone(), &fs).await?"], + ["compile_error!("] + ) +} + +test fn a_bare_caller_passes_its_binding_to_a_bare_callee() -> Bool { + compile_dag_rust_emit_check( + green_bare_callee_bare_caller_source, + "src/probe_resource_bare_bare.rs", + ["reads(path.clone(), &fs).await?"], + ["compile_error!("] + ) +} + +test fn a_caller_of_a_different_resource_still_refuses_a_qualified_callee() -> Bool { + compile_dag_rust_emit_check( + red_qualified_callee_other_bare_resource_source, + "src/probe_resource_spelling_fork_wrong.rs", + ["compile_error!(", "requires resource fs that the calling declaration does not establish"], + ["&fs)"] + ) +} diff --git a/src/v1/00_core.dag b/src/v1/00_core.dag index cb79404bf5b..4bc37383ab4 100644 --- a/src/v1/00_core.dag +++ b/src/v1/00_core.dag @@ -710,8 +710,8 @@ fn diagnostic_to_message(d: CompilerDiagnostic) -> String { MethodExistenceFrontierAdmitted { method: m, receiver_type: t, trigger: tr, span: _ } => concat("method '", m, "' on receiver type '", t, "' is admitted by a declared unresolved-method frontier row; dissolves on: ", tr) AlgebraApplicationEvidenceUnavailable { receiver_type: t, argument_index: i, span: _ } => concat("algebra receiver application evidence unavailable for '", t, "' at argument ", to_string(value: i), ": structural members are not type arguments") ReceiverTypeUnestablished { method: _, span: _ } => "the receiver's own type was never established, so nothing is known about the method's existence here; this is an upstream type-propagation deficit, not a fact about the method" - FrontierOccurrenceBudgetExceeded { method: m, receiver_type: t, declared: _, observed: _, span: _ } => - concat(concat(concat("the declared frontier row for '", m), concat("' on receiver type '", t)), "' no longer matches what this module contains: its declared occurrence count and the count observed here differ, and both numbers are carried on this diagnostic. If MORE were observed, a new unresolved call has appeared and the receiver's type should be established rather than the count raised. If FEWER were observed, the deficit has partly dissolved and the row must be lowered or deleted so the ratchet keeps its new ground. The count is an equality, not a ceiling, in both directions.") + FrontierOccurrenceBudgetExceeded { method: m, receiver_type: t, declared: d, observed: o, span: _ } => + concat(concat(concat("the declared frontier row for '", m), concat("' on receiver type '", t)), concat(concat("' no longer matches what this module contains: the row declares ", to_string(value: d)), concat(" occurrence(s) and ", to_string(value: o))), " were observed here. If MORE were observed, a new unresolved call has appeared and the receiver's type should be established rather than the count raised. If FEWER were observed, the deficit has partly dissolved and the row must be lowered or deleted so the ratchet keeps its new ground. The count is an equality, not a ceiling, in both directions.") TestCodeReferenced { referrer: r, target: t, span: _ } => concat("'", r, "' references test code '", t, "': a `test` declaration is entered only by the witness runner, so no declaration may call, name or import it. Move shared logic into an ordinary fn, or delete a test that only re-asserts other tests") TestCodeReferenceAdmitted { referrer: r, target: t, span: _ } => concat("'", r, "' references test code '", t, "'; admitted by the declared test-reference debt ledger (v1.compiler.compile test_reference_debt), which may only shrink") TestCodeReferenceRowOrphaned { referrer: r, span: _ } => concat("the test-reference debt row for '", r, "' names a module that no longer exists in the corpus: it is neither compiled here nor in the loaded name census. A row that can never be observed again must be deleted, not left to persist") diff --git a/src/v1/04_infer.dag b/src/v1/04_infer.dag index 9e55c5f3da1..d5fc40d108c 100644 --- a/src/v1/04_infer.dag +++ b/src/v1/04_infer.dag @@ -130,7 +130,7 @@ import v1.compiler.infer_env { qualify_borrowed_type_names, qualify_borrowed_inferred, qualified_all_but_last, env_with_type_variable_bindings, census_declaration_type_env, declaration_substitution_basis, GlobalBareCandidate, node_with_inferred, node_with_children, ServiceCensusEntry, qualify_decl_reference_positions, - lookup_binding_by_name, lookup_binding_on_chain, binding_declares_name, type_reference_declaration_ref, type_reference_declaration, declaration_provenance_of_ref, + lookup_binding_by_name, lookup_binding_on_chain, binding_declares_name, type_reference_declaration_ref, declaration_ref_of_type_node, type_reference_declaration, declaration_provenance_of_ref, listed_import_required_bare_call_blocked, effective_visible_binding, unit_variant_index_shadow_insert, build_unit_variant_index, UnitVariantContribution @@ -190,7 +190,8 @@ import v1.compiler.infer_items { ItemKind, FnItem, TypeItem, DataItem, ServiceItem, OtherItem, ItemInfo, TypedModule, TypedGraph, ResolvedGraph, ModuleInterface, leaf_owner_modules_from_registry, ModuleTypecheckProgress, ItemsChecked, AbandonedBeforeItems, - inferred_to_outputs, item_kind, item_is_effectful_callee + inferred_to_outputs, item_kind, item_is_effectful_callee, + ResourceRequirement, resource_requirements_of_uses } import v1.compiler.infer_service { UniqueAccum, OpEntry, ServiceMethodResult, @@ -534,11 +535,27 @@ fn build_item_inductive_fields(items: List, recursive_type_set: Set` node carrying nothing but the spelling, while the qualified +// spelling `std.resources.Filesystem` resolved to the declaration itself. So ONE declaration had two +// resolved identities chosen by how a use site spelled it, and the uses-requirement join refused a +// correct caller whose spelling differed from its callee's (gunbc#12047). The leaf now carries the +// declaration's own span and ident_span, so the census join (v1.compiler.infer_env +// declaration_ref_of_type_node) recovers the same DeclarationRef from either spelling, and nothing +// kernel-minted stands where a corpus declaration exists. fn nominal_ref_node(name: String, span: SourceSpan, ident_span: SourceSpan?) -> Node { + let declared_leaf = Node { + occurrence_identity: OccurrenceSynthetic, name: name, span: span, ident_span: ident_span, children: [], + connective: NoConnective, params: [], inferred: none, return_cardinality: Required, + uses: [], body: none, transport: none, properties: [], + type_annotation: none, is_self_recursive: false, has_non_tail_self_call: false, match_pattern: none, module_item_kind: NotAModuleItem, declaration_marker: Unmarked, expr_data: NoExprData + } Node { occurrence_identity: OccurrenceSynthetic, name: name, span: span, ident_span: ident_span, children: [], connective: NoConnective, params: [], - inferred: Present { value: Resolved { node: nominal_leaf_type(name: name) } }, + inferred: Present { value: Resolved { node: declared_leaf } }, return_cardinality: Required, uses: [], body: none, transport: none, properties: [], type_annotation: none, is_self_recursive: false, has_non_tail_self_call: false, match_pattern: none, module_item_kind: NotAModuleItem, declaration_marker: Unmarked, expr_data: NoExprData @@ -1081,6 +1098,64 @@ fn constructor_call_admission_diags( } } +// THE RESOURCE BINDING A CALL PASSES IS THE CALLER'S, MATCHED BY RESOLVED DECLARATION. The Rust +// realization passes each resource a callee requires as a parameter, and before this fold the +// emitter appended the CALLEE's binding name: the emitted call agreed with the declaration's arity +// while its argument named nothing in the caller's scope. v1.compiler.emit_rust emit_typed_call +// asks this fold for the caller's binding of each requirement, and renders a located +// compile_error! where there is none. +// +// WHAT THIS FOLD IS NOT: it is not an admission wall. Whether a caller ESTABLISHES a resource is +// owned by DESIGN D13's derived DependencyDemand (docs/plans/demand-engine-program.md), under which +// a transparent function's demand is derived rather than authored. This fold reads the caller's +// AUTHORED `uses` rows only because no derived carrier with a binding identity exists yet +// (its return is the next-rung trigger on gunbc.recurring_failure_mode accepted_source_emits_uncompilable_target). A caller whose +// requirement is derived gets the compile_error! arm, so the target compiler refuses the artifact +// rather than accepting a call that names an unbound identifier. +// +// IDENTITY IS THE RESOLVED DECLARATION, NOT A SPELLING: see resource_declaration_identity below. A +// comparison of resolved NODES forked by spelling (a bare name resolved to a kernel-spanned stand-in, +// the qualified one to the declaration), which broke calls that compiled on main (review 70670; +// gunbc.recurring_failure_mode a_sibling_refusal_masks_a_ratchet_rows_observation). +// IDENTITY IS READ OFF THE RESOLVED NODE, NEVER RE-LOOKED-UP BY SPELLING. The previous body rendered +// the resource's spelling and looked it up again in the CALLER's environment, so a callee's +// requirement was re-resolved under the caller's imports, and the answer was a node compared +// structurally -- which forked by spelling, because a bare `Filesystem` resolved to a kernel-spanned +// stand-in while `std.resources.Filesystem` resolved to the declaration (gunbc#12047). The stand-in +// now carries its declaration's spans (nominal_ref_node), and this join asks the census which +// declaration the resolved node names: by the declaration span it carries, or by an authored +// qualified name whose module prefix the census row must equal. There is no bare-spelling hop. +fn resource_declaration_identity(scope: InferScope, resource: Node) -> DeclarationRef? { + let resolved = match resource.inferred { + Present { value: Resolved { node: r } } => r + _ => resource + } + declaration_ref_of_type_node(rt: resolved, source_indices: scope.type_env.source_indices, env: scope.type_env) +} + +fn established_resource_binding(scope: InferScope, required: ResourceRequirement, established: List) -> String? { + match resource_declaration_identity(scope: scope, resource: required.resource) { + Absent => none + Present { value: required_decl } => + match established |> filter(e => + match resource_declaration_identity(scope: scope, resource: e.resource) { + Present { value: e_decl } => e_decl == required_decl + Absent => false + } + ) |> first { + Present { value: e } => Present { value: e.binding_name } + Absent => none + } + } +} + +fn caller_resource_requirements(scope: InferScope) -> List { + match map_get(scope.item_registry, callable_identity(id: DeclaredCallableIdentity { owner_module_path: scope.module_name, decl_name: scope.caller_decl_name })) { + Present { value: info } => info.resource_requirements + Absent => [] + } +} + fn constructor_reference_admission_refusal( callee_name: String, span: SourceSpan, @@ -12444,7 +12519,7 @@ fn local_binding_for_item(item: Node, source_indices: Map) occurrence_identity: item.occurrence_identity, name: item.name, span: item.span, ident_span: item.ident_span, children: [], connective: NoConnective, params: item.params, inferred: item.inferred, return_cardinality: item.return_cardinality, - uses: [], body: none, transport: none, properties: item.properties, + uses: item.uses, body: none, transport: none, properties: item.properties, type_annotation: item.type_annotation, is_self_recursive: false, has_non_tail_self_call: false, match_pattern: none, module_item_kind: item.module_item_kind, declaration_marker: item.declaration_marker, expr_data: NoExprData } Present { value: TypeBinding { name: authored_name_at(source_indices: source_indices, node: item), resolved: fn_node, provenance: SubValueUnknown } } @@ -13708,7 +13783,7 @@ fn refresh_direct_service_names( fn build_item_info(item: Node, source_indices: Map, module_name: String) -> ItemInfo { let kind = item_kind(item: item) - let res_names = item.uses |> map(u => resource_use_name_at(n: u, source_indices: source_indices)) + let requirements = resource_requirements_of_uses(uses: item.uses, source_indices: source_indices) let item_name_str = authored_name_at(source_indices: source_indices, node: item) match kind { FnItem => @@ -13717,7 +13792,7 @@ fn build_item_info(item: Node, source_indices: Map, module module_name: module_name, kind: kind, service_names: if item.body == none { [] } else { collect_typed_service_calls(texpr: item.body.value, source_indices: source_indices) }, - resource_names: res_names, + resource_requirements: requirements, params: item.params, is_self_recursive: if item.body == none { false } else { expr_has_self_call(texpr: item.body.value, fn_name: item_name_str, source_indices: source_indices) }, has_non_tail_self_call: if item.body == none { false } else { expr_has_non_tail_self_call(texpr: item.body.value, fn_name: item_name_str, in_tail: true, source_indices: source_indices) } @@ -13728,7 +13803,7 @@ fn build_item_info(item: Node, source_indices: Map, module module_name: module_name, kind: kind, service_names: [], - resource_names: res_names, + resource_requirements: requirements, params: item.params, is_self_recursive: false, has_non_tail_self_call: false diff --git a/src/v1/04_items.dag b/src/v1/04_items.dag index 04c769b43d2..153b221fd87 100644 --- a/src/v1/04_items.dag +++ b/src/v1/04_items.dag @@ -27,6 +27,7 @@ import v1.std.core { node_name_span, no_span, NewlineIndex, authored_name_at, LeafOwner, SingleOwner, LeafAmbiguous, + resource_use_name_at, resource_use_resource, } import std.occurrence_identity { OccurrenceTransport, OccurrenceSynthetic } @@ -49,12 +50,40 @@ import v1.compiler.infer_emit_info { type ItemKind = FnItem | TypeItem | DataItem | ServiceItem | OtherItem +// A DECLARATION'S RESOURCE REQUIREMENT IS PART OF ITS CALL CONTRACT, NOT A NAME LIST. One row per +// `uses` entry: the binding name the declaration gave the resource (which is also the identifier +// the Rust realization receives it under) and the resource TYPE it requires, kept as the authored +// type node so a consumer can resolve it in an environment and compare declarations rather than +// spellings. The list of names alone -- what this carrier replaced -- let the Rust emitter pass a +// CALLEE's binding name at a call site in a CALLER that never bound it: the arity of the emitted +// call agreed with the declaration while its argument named nothing in scope, and only rustc +// refused, after the file was published (DESIGN section 5, silent wrongness at the realization +// boundary). It is consumed by v1.compiler.emit_rust emit_typed_call through v1.compiler.infer +// established_resource_binding, keyed on the resolved resource declaration. Whether a caller +// ESTABLISHES a requirement is D13 DependencyDemand's question, not this carrier's. +type ResourceRequirement { + binding_name: String + resource: Node +} + +fn resource_requirements_of_uses(uses: List, source_indices: Map) -> List { + uses |> map(u => ResourceRequirement { + binding_name: resource_use_name_at(n: u, source_indices: source_indices), + resource: resource_use_resource(n: u) + }) +} + +fn item_resource_names(info: ItemInfo) -> List { + info.resource_requirements |> map(r => r.binding_name) +} + + type ItemInfo { name: String module_name: String kind: ItemKind service_names: List - resource_names: List + resource_requirements: List params: List is_self_recursive: Bool has_non_tail_self_call: Bool @@ -76,7 +105,7 @@ type ItemInfo { // a typed requirement carrier. A predicate named for its answer survives that change with every // consumer untouched; one named for its fields would move six call sites. fn item_is_effectful_callee(info: ItemInfo) -> Bool { - info.service_names |> count > 0 || info.resource_names |> count > 0 + info.service_names |> count > 0 || item_resource_names(info: info) |> count > 0 } // THE ANSWER TO "WHICH MODULE DECLARES THIS LEAF" HAS THREE SHAPES, SO IT IS A COPRODUCT. diff --git a/src/v1/04_service.dag b/src/v1/04_service.dag index 895b1f4a488..9fb07502e3d 100644 --- a/src/v1/04_service.dag +++ b/src/v1/04_service.dag @@ -332,7 +332,7 @@ fn expand_transitive_services_once(module_callees: List, registry module_name: info.module_name, kind: info.kind, service_names: merged, - resource_names: info.resource_names, + resource_requirements: info.resource_requirements, params: info.params, is_self_recursive: info.is_self_recursive, has_non_tail_self_call: info.has_non_tail_self_call diff --git a/src/v1/05_emit.dag b/src/v1/05_emit.dag index e4d86c74f01..2f8e3bf9b8a 100644 --- a/src/v1/05_emit.dag +++ b/src/v1/05_emit.dag @@ -3488,7 +3488,7 @@ fn emit_typed_call_unified(func: String, args: List, call_semantics: CallS Present { value: info } => let has_effects = item_is_effectful_callee(info: info) if has_effects { - let resource_args = info.resource_names |> map(rn => emit_ident(name: rn, target: target)) + let resource_args = item_resource_names(info: info) |> map(rn => emit_ident(name: rn, target: target)) let service_args = info.service_names |> map(sn => service_var_name(service_name: sn)) concat(resource_args, service_args) } else { [] } diff --git a/src/v1/05_emit_rust.dag b/src/v1/05_emit_rust.dag index ea06e238170..de81e2fd2d0 100644 --- a/src/v1/05_emit_rust.dag +++ b/src/v1/05_emit_rust.dag @@ -217,7 +217,8 @@ import v1.compiler.infer { resolved_type_name, expand_type_for_field_access, is_where_refinement_type, - call_args_by_name + call_args_by_name, + caller_resource_requirements, established_resource_binding } import v1.compiler.infer_emit_info { EmitGraphInfo, TypeSummary, StructRepr, EnumRepr, @@ -9570,7 +9571,7 @@ fn emit_func_def( } let params_str = emit_func_params(params: params, uses: uses, service_names: service_names, shared_types: shared_types, source_indices: scope.type_env.source_indices, read_only_params: emit_info.read_only_params, variant_to_enum: emit_info.variant_to_enum, env: scope.type_env) let ret_str = emit_func_inferred(inferred: inferred, shared_types: shared_types, source_indices: scope.type_env.source_indices) - let body_scope = build_params_scope(scope: scope, params: params) + let body_scope = build_params_scope(scope: with(scope, { caller_decl_name: name }), params: params) let si = scope.type_env.source_indices let body_scope = fold(uses, init: body_scope, f: (s, u) => extend_scope(scope: s, name: resource_use_name_at(n: u, source_indices: si), resolved: resource_use_resource(n: u), provenance: SubValueUnknown) @@ -11940,6 +11941,24 @@ fn lambda_argument_scope(arg: Node, scope: InferScope) -> InferScope { // (E0599, and every argument after it `unreachable`), which is the whole defect the typed seam was // built to remove. Keeping it for the unresolved case would have kept that defect for exactly the // population the emitter knows least about. +// RESOURCE ARGUMENTS AT A CALL SITE ARE THE CALLER'S BINDINGS, NOT THE CALLEE'S SPELLINGS. In +// emit_typed_call each callee requirement (ItemInfo.resource_requirements, read by the resolved +// callee identity) is matched to the enclosing declaration's own `uses` row through +// v1.compiler.infer established_resource_binding, and the identifier emitted is the caller's. +// Before this the emitter appended `&` unconditionally: arity agreed with the +// declaration, the argument named nothing in the caller's scope, and the file was published for +// rustc to refuse. A requirement the fold cannot establish renders compile_error!, NOT the +// emitter's ordinary error expression, whose Rust template is panic!({0}): panic! has type `!`, +// which coerces to any parameter type, so rustc ACCEPTED that call and the program panicked at run +// time (review 69961). THIS IS RUNG 2, NOT A REFUSAL BEFORE PUBLISHING: the artifact is still +// written and the target compiler is what refuses it. No typecheck wall stands in front of this arm +// -- whether a caller establishes a resource is owned by D13's derived DependencyDemand, which does +// not exist yet, so a caller whose demand is derived rather than authored reaches this arm. The +// return of a pre-emission refusal is the next-rung trigger on gunbc.recurring_failure_mode +// accepted_source_emits_uncompilable_target. +// emit_func_def names the declaration it renders on the body scope (caller_decl_name) so that read +// has a subject. + fn emit_typed_call(func: String, args: List, inferred: InferredNode?, call_semantics: CallSemantics?, callee_is_function_value: Bool, call_target: CallTargetIdentity, registry: Map, scope: InferScope, depth: Int, shared_types: Set, emit_info: EmitGraphInfo) -> String { if callee_is_function_value { return emit_typed_function_value_call(func: func, args: args, registry: registry, scope: scope, depth: depth, shared_types: shared_types, emit_info: emit_info) @@ -12058,7 +12077,13 @@ fn emit_typed_call(func: String, args: List, inferred: InferredNode?, call Present { value: info } => let has_effects = item_is_effectful_callee(info: info) if has_effects { - let resource_args = info.resource_names |> map(rn => concat("&", emit_ident(name: rn, target: Rust))) + let established = caller_resource_requirements(scope: scope) + let resource_args = info.resource_requirements |> map(rr => + match established_resource_binding(scope: scope, required: rr, established: established) { + Present { value: binding_name } => concat("&", emit_ident(name: binding_name, target: Rust)) + Absent => emit_rust_compile_error_expr(message: concat("call to ", func, " requires resource ", rr.binding_name, " that the calling declaration does not establish")) + } + ) let service_args = info.service_names |> map(sn => service_var_name(service_name: sn)) concat(resource_args, service_args) } else { [] } diff --git a/src/v1/compiler_tests_rust.dag b/src/v1/compiler_tests_rust.dag index de941093212..a86930cf36b 100644 --- a/src/v1/compiler_tests_rust.dag +++ b/src/v1/compiler_tests_rust.dag @@ -1776,7 +1776,7 @@ fn ct_generic_param_declines_fail_closed_unwrap_test() -> String { " module_name: \"v2.std.diagnostic\".to_string(),\n", " kind: crate::v1_compiler_infer_items::ItemKind::FnItem,\n", " service_names: std::rc::Rc::new(im::Vector::new()),\n", - " resource_names: std::rc::Rc::new(im::Vector::new()),\n", + " resource_requirements: std::rc::Rc::new(im::Vector::new()),\n", " params: std::rc::Rc::new(vec![param].into()),\n", " is_self_recursive: false,\n", " has_non_tail_self_call: false,\n", diff --git a/src/v1/stage0/src/cli_run/emit_host.rs b/src/v1/stage0/src/cli_run/emit_host.rs index 8f3b54f047e..4565070f048 100644 --- a/src/v1/stage0/src/cli_run/emit_host.rs +++ b/src/v1/stage0/src/cli_run/emit_host.rs @@ -606,8 +606,8 @@ fn project_resolved_rust_fn_signatures( RenderTarget::Rust, )); } - for r in info.resource_names.iter() { - ordered.push(emit_ident(r.clone(), RenderTarget::Rust)); + for r in info.resource_requirements.iter() { + ordered.push(emit_ident(r.binding_name.clone(), RenderTarget::Rust)); } for sn in info.service_names.iter() { ordered.push(crate::v1_compiler_emit_core_support::service_var_name( diff --git a/src/v1/stage0/src/compiler_tests.rs b/src/v1/stage0/src/compiler_tests.rs index 9e9f7be7f92..c70e817185f 100644 --- a/src/v1/stage0/src/compiler_tests.rs +++ b/src/v1/stage0/src/compiler_tests.rs @@ -4527,7 +4527,7 @@ mod compiler_tests { module_name: "v2.std.diagnostic".to_string(), kind: crate::v1_compiler_infer_items::ItemKind::FnItem, service_names: std::rc::Rc::new(im::Vector::new()), - resource_names: std::rc::Rc::new(im::Vector::new()), + resource_requirements: std::rc::Rc::new(im::Vector::new()), params: std::rc::Rc::new(vec![param].into()), is_self_recursive: false, has_non_tail_self_call: false, diff --git a/src/v1/stage0/src/v1_compiler_compiler_tests_rust.rs b/src/v1/stage0/src/v1_compiler_compiler_tests_rust.rs index 8b5f7071093..809af6003e8 100644 --- a/src/v1/stage0/src/v1_compiler_compiler_tests_rust.rs +++ b/src/v1/stage0/src/v1_compiler_compiler_tests_rust.rs @@ -155,7 +155,7 @@ pub fn ct_coercion_tests() -> String { } pub fn ct_generic_param_declines_fail_closed_unwrap_test() -> String { - v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(" fn optional_typed_arg_node() -> std::rc::Rc {\n".to_string(), " let optional_type = shaped_type_node(\"Node\", Vec::new());\n".to_string()), " let optional_type = std::rc::Rc::new(crate::v1_std_core::Node {\n".to_string()), " return_cardinality: crate::v1_std_core::Cardinality::CardOptional,\n".to_string()), " ..(*optional_type).clone()\n".to_string()), " });\n".to_string()), " let arg = named_type_node(\"child\");\n".to_string()), " std::rc::Rc::new(crate::v1_std_core::Node {\n".to_string()), " inferred: Some(std::rc::Rc::new(crate::v1_std_core::InferredNode::Resolved {\n".to_string()), " node: optional_type,\n".to_string()), " })),\n".to_string()), " ..(*arg).clone()\n".to_string()), " })\n".to_string()), " }\n\n".to_string()), " fn callee_with_one_param(param_type_name: &str) -> Option> {\n".to_string()), " let param = shaped_type_node(\"value\", vec![named_type_node(param_type_name)]);\n".to_string()), " Some(std::rc::Rc::new(crate::v1_compiler_infer_items::ItemInfo {\n".to_string()), " name: \"outcome_accepted\".to_string(),\n".to_string()), " module_name: \"v2.std.diagnostic\".to_string(),\n".to_string()), " kind: crate::v1_compiler_infer_items::ItemKind::FnItem,\n".to_string()), " service_names: std::rc::Rc::new(im::Vector::new()),\n".to_string()), " resource_names: std::rc::Rc::new(im::Vector::new()),\n".to_string()), " params: std::rc::Rc::new(vec![param].into()),\n".to_string()), " is_self_recursive: false,\n".to_string()), " has_non_tail_self_call: false,\n".to_string()), " }))\n".to_string()), " }\n\n".to_string()), " #[test]\n".to_string()), " fn generic_parameter_declines_the_fail_closed_unwrap() {\n".to_string()), " let source_indices = std::rc::Rc::new(HashMap::new());\n".to_string()), " let arg = optional_typed_arg_node();\n".to_string()), " let generic = crate::v1_compiler_emit_rust::rust_call_arg_fail_closed_unwrap(\n".to_string()), " \"child.clone()\".to_string(),\n".to_string()), " arg.clone(),\n".to_string()), " callee_with_one_param(\"T\"),\n".to_string()), " 0,\n".to_string()), " \"outcome_accepted\".to_string(),\n".to_string()), " source_indices.clone(),\n".to_string()), " );\n".to_string()), " assert_eq!(\n".to_string()), " generic, \"child.clone()\",\n".to_string()), " \"a type-variable parameter cannot say the instantiation is non-optional, so no unwrap may be injected\"\n".to_string()), " );\n".to_string()), " let concrete = crate::v1_compiler_emit_rust::rust_call_arg_fail_closed_unwrap(\n".to_string()), " \"child.clone()\".to_string(),\n".to_string()), " arg,\n".to_string()), " callee_with_one_param(\"Node\"),\n".to_string()), " 0,\n".to_string()), " \"node_locus\".to_string(),\n".to_string()), " source_indices,\n".to_string()), " );\n".to_string()), " assert!(\n".to_string()), " concrete.contains(\".expect(\"),\n".to_string()), " \"a concrete non-optional parameter must still take the unwrap: {}\",\n".to_string()), " concrete\n".to_string()), " );\n".to_string()), " }\n\n".to_string()) + v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(" fn optional_typed_arg_node() -> std::rc::Rc {\n".to_string(), " let optional_type = shaped_type_node(\"Node\", Vec::new());\n".to_string()), " let optional_type = std::rc::Rc::new(crate::v1_std_core::Node {\n".to_string()), " return_cardinality: crate::v1_std_core::Cardinality::CardOptional,\n".to_string()), " ..(*optional_type).clone()\n".to_string()), " });\n".to_string()), " let arg = named_type_node(\"child\");\n".to_string()), " std::rc::Rc::new(crate::v1_std_core::Node {\n".to_string()), " inferred: Some(std::rc::Rc::new(crate::v1_std_core::InferredNode::Resolved {\n".to_string()), " node: optional_type,\n".to_string()), " })),\n".to_string()), " ..(*arg).clone()\n".to_string()), " })\n".to_string()), " }\n\n".to_string()), " fn callee_with_one_param(param_type_name: &str) -> Option> {\n".to_string()), " let param = shaped_type_node(\"value\", vec![named_type_node(param_type_name)]);\n".to_string()), " Some(std::rc::Rc::new(crate::v1_compiler_infer_items::ItemInfo {\n".to_string()), " name: \"outcome_accepted\".to_string(),\n".to_string()), " module_name: \"v2.std.diagnostic\".to_string(),\n".to_string()), " kind: crate::v1_compiler_infer_items::ItemKind::FnItem,\n".to_string()), " service_names: std::rc::Rc::new(im::Vector::new()),\n".to_string()), " resource_requirements: std::rc::Rc::new(im::Vector::new()),\n".to_string()), " params: std::rc::Rc::new(vec![param].into()),\n".to_string()), " is_self_recursive: false,\n".to_string()), " has_non_tail_self_call: false,\n".to_string()), " }))\n".to_string()), " }\n\n".to_string()), " #[test]\n".to_string()), " fn generic_parameter_declines_the_fail_closed_unwrap() {\n".to_string()), " let source_indices = std::rc::Rc::new(HashMap::new());\n".to_string()), " let arg = optional_typed_arg_node();\n".to_string()), " let generic = crate::v1_compiler_emit_rust::rust_call_arg_fail_closed_unwrap(\n".to_string()), " \"child.clone()\".to_string(),\n".to_string()), " arg.clone(),\n".to_string()), " callee_with_one_param(\"T\"),\n".to_string()), " 0,\n".to_string()), " \"outcome_accepted\".to_string(),\n".to_string()), " source_indices.clone(),\n".to_string()), " );\n".to_string()), " assert_eq!(\n".to_string()), " generic, \"child.clone()\",\n".to_string()), " \"a type-variable parameter cannot say the instantiation is non-optional, so no unwrap may be injected\"\n".to_string()), " );\n".to_string()), " let concrete = crate::v1_compiler_emit_rust::rust_call_arg_fail_closed_unwrap(\n".to_string()), " \"child.clone()\".to_string(),\n".to_string()), " arg,\n".to_string()), " callee_with_one_param(\"Node\"),\n".to_string()), " 0,\n".to_string()), " \"node_locus\".to_string(),\n".to_string()), " source_indices,\n".to_string()), " );\n".to_string()), " assert!(\n".to_string()), " concrete.contains(\".expect(\"),\n".to_string()), " \"a concrete non-optional parameter must still take the unwrap: {}\",\n".to_string()), " concrete\n".to_string()), " );\n".to_string()), " }\n\n".to_string()) } pub fn ct_witness_carrier_declines_non_witness_expected_type_test() -> String { diff --git a/src/v1/stage0/src/v1_compiler_emit.rs b/src/v1/stage0/src/v1_compiler_emit.rs index d046d62fdfc..8834dd67a22 100644 --- a/src/v1/stage0/src/v1_compiler_emit.rs +++ b/src/v1/stage0/src/v1_compiler_emit.rs @@ -67,6 +67,7 @@ pub use crate::v1_compiler_infer_env::UnitVariantContribution; pub use crate::v1_compiler_infer_env::{authored_name, empty_symbol_index, lookup_type_for}; pub use crate::v1_compiler_infer_env::{GlobalBareLookupState, TypeBinding, TypeEnv}; pub use crate::v1_compiler_infer_items::item_is_effectful_callee; +pub use crate::v1_compiler_infer_items::item_resource_names; pub use crate::v1_compiler_infer_items::{ItemInfo, ResolvedGraph, TypedModule}; pub use crate::v1_compiler_infer_lookup::lookup_func_sig; pub use crate::v1_compiler_infer_service::{ @@ -7243,7 +7244,11 @@ pub fn emit_typed_call_unified( { let resource_args = Rc::new({ let mut __result = Vec::new(); - for rn in info.resource_names.clone().iter().cloned() { + for rn in + crate::v1_compiler_infer_items::item_resource_names(info.clone()) + .iter() + .cloned() + { __result.push(emit_ident(rn.clone(), target.clone())); } __result diff --git a/src/v1/stage0/src/v1_compiler_emit_rust.rs b/src/v1/stage0/src/v1_compiler_emit_rust.rs index 6430a527dc9..b8a432b02da 100644 --- a/src/v1/stage0/src/v1_compiler_emit_rust.rs +++ b/src/v1/stage0/src/v1_compiler_emit_rust.rs @@ -186,9 +186,9 @@ pub use crate::v1_compiler_emit_core_support::{ pub use crate::v1_compiler_emit_core_support::{EmitResult, TestProjection}; pub use crate::v1_compiler_infer::InferScope; pub use crate::v1_compiler_infer::{ - build_emit_graph_info, build_params_scope, call_args_by_name, declared_return_type_node, - expand_type_for_field_access, expr_span, extend_scope, is_where_refinement_type, - resolved_type_name, + build_emit_graph_info, build_params_scope, call_args_by_name, caller_resource_requirements, + declared_return_type_node, established_resource_binding, expand_type_for_field_access, + expr_span, extend_scope, is_where_refinement_type, resolved_type_name, }; pub use crate::v1_compiler_infer_emit_info::DataVariantWireSpelling; use crate::v1_compiler_infer_emit_info::DataVariantWireSpelling::*; @@ -216,9 +216,9 @@ pub use crate::v1_compiler_infer_env::{ pub use crate::v1_compiler_infer_env::{ GlobalBareLookupState, TypeBinding, TypeEnv, UnitVariantPhantomLookup, }; -pub use crate::v1_compiler_infer_items::item_kind; use crate::v1_compiler_infer_items::ItemKind::{DataItem, OtherItem, TypeItem}; use crate::v1_compiler_infer_items::ItemLookup::{ItemFound, ItemLeafAmbiguous, ItemNotFound}; +pub use crate::v1_compiler_infer_items::{item_is_effectful_callee, item_kind}; pub use crate::v1_compiler_infer_items::{ ItemInfo, ItemKind, ItemLookup, ResolvedGraph, TypedModule, }; @@ -16094,8 +16094,7 @@ pub fn emit_typed_item( }), ) { Some(info) => { - (((info.service_names.clone().len() as i64) > 0) - || ((info.resource_names.clone().len() as i64) > 0)) + crate::v1_compiler_infer_items::item_is_effectful_callee(info.clone()) } std::option::Option::None => false, }; @@ -20122,8 +20121,13 @@ pub fn emit_func_def( shared_types.clone(), scope.type_env.clone().source_indices.clone(), ); - let body_scope = - crate::v1_compiler_infer::build_params_scope(scope.clone(), params.clone()); + let body_scope = crate::v1_compiler_infer::build_params_scope( + Rc::new(InferScope { + caller_decl_name: name.clone(), + ..(*scope.clone()).clone() + }), + params.clone(), + ); let si = scope.type_env.clone().source_indices.clone(); let body_scope = uses.iter() @@ -25693,20 +25697,46 @@ pub fn emit_typed_call( }); let extra_args = match callee.clone() { Some(info) => { - let has_effects = (((info.service_names.clone().len() as i64) > 0) - || ((info.resource_names.clone().len() as i64) > 0)); + let has_effects = + crate::v1_compiler_infer_items::item_is_effectful_callee(info.clone()); if has_effects.clone() { { + let established = + crate::v1_compiler_infer::caller_resource_requirements(scope.clone()); let resource_args = Rc::new({ let mut __result = Vec::new(); - for rn in info.resource_names.clone().iter().cloned() { - __result.push(v1_rt::concat( - "&".to_string(), - crate::v1_compiler_emit::emit_ident( - rn.clone(), - RenderTarget::Rust, - ), - )); + for rr in info.resource_requirements.clone().iter().cloned() { + __result.push( + match crate::v1_compiler_infer::established_resource_binding( + scope.clone(), + rr.clone(), + established.clone(), + ) { + Some(binding_name) => v1_rt::concat( + "&".to_string(), + crate::v1_compiler_emit::emit_ident( + binding_name.clone(), + RenderTarget::Rust, + ), + ), + std::option::Option::None => { + emit_rust_compile_error_expr(v1_rt::concat( + v1_rt::concat( + v1_rt::concat( + v1_rt::concat( + "call to ".to_string(), + func.clone(), + ), + " requires resource ".to_string(), + ), + rr.binding_name.clone(), + ), + " that the calling declaration does not establish" + .to_string(), + )) + } + }, + ); } __result }); @@ -25859,8 +25889,8 @@ pub fn emit_typed_call( }; match callee.clone() { Some(info) => { - let has_effects = (((info.service_names.clone().len() as i64) > 0) - || ((info.resource_names.clone().len() as i64) > 0)); + let has_effects = + crate::v1_compiler_infer_items::item_is_effectful_callee(info.clone()); if has_effects.clone() { v1_rt::concat(call_str.clone(), ".await?".to_string()) } else { diff --git a/src/v1/stage0/src/v1_compiler_infer.rs b/src/v1/stage0/src/v1_compiler_infer.rs index c1550380664..c1c976da6ed 100644 --- a/src/v1/stage0/src/v1_compiler_infer.rs +++ b/src/v1/stage0/src/v1_compiler_infer.rs @@ -99,9 +99,9 @@ use crate::v1_compiler_infer_env::GlobalBareLookupState::{ }; pub use crate::v1_compiler_infer_env::{ bare_name_miss_diagnostic, binding_declares_name, build_unit_variant_index, - census_declaration_type_env, declaration_provenance_of_ref, declaration_substitution_basis, - effective_visible_binding, empty_symbol_index, empty_type_env_cache, - env_with_type_variable_bindings, global_bare_is_ambiguous, + census_declaration_type_env, declaration_provenance_of_ref, declaration_ref_of_type_node, + declaration_substitution_basis, effective_visible_binding, empty_symbol_index, + empty_type_env_cache, env_with_type_variable_bindings, global_bare_is_ambiguous, global_bare_strict_ambiguity_candidates, inductive_fields_for, inductive_fields_list_to_map, is_recursive_type, is_recursive_type_by_name, listed_import_required_bare_call_blocked, lookup_binding_by_name, lookup_binding_on_chain, lookup_type, lookup_type_by_name, @@ -123,10 +123,11 @@ use crate::v1_compiler_infer_items::ItemKind::{ use crate::v1_compiler_infer_items::ModuleTypecheckProgress::{AbandonedBeforeItems, ItemsChecked}; pub use crate::v1_compiler_infer_items::{ inferred_to_outputs, item_is_effectful_callee, item_kind, leaf_owner_modules_from_registry, + resource_requirements_of_uses, }; pub use crate::v1_compiler_infer_items::{ - ItemInfo, ItemKind, ModuleInterface, ModuleTypecheckProgress, ResolvedGraph, TypedGraph, - TypedModule, + ItemInfo, ItemKind, ModuleInterface, ModuleTypecheckProgress, ResolvedGraph, + ResourceRequirement, TypedGraph, TypedModule, }; use crate::v1_compiler_infer_lookup::ConstructorDeclarationLookup::{ AdmissionBearingDeclarationUnavailable, ExactConstructorDeclaration, @@ -858,31 +859,56 @@ pub fn nominal_ref_node( span: Rc, ident_span: Option>, ) -> Rc { - Rc::new(Node { - occurrence_identity: Rc::new(NodeOccurrenceIdentity::OccurrenceSynthetic), - name: name.clone(), - span: span.clone(), - ident_span: ident_span.clone(), - children: Rc::new(vec![]), - connective: Connective::NoConnective, - params: Rc::new(vec![]), - inferred: Some(Rc::new(InferredNode::Resolved { - node: nominal_leaf_type(name.clone()), - })), - return_cardinality: Cardinality::Required, - uses: Rc::new(vec![]), - body: std::option::Option::None, - transport: std::option::Option::None, - properties: Rc::new(vec![]), - type_annotation: std::option::Option::None, - is_self_recursive: false, - has_non_tail_self_call: false, - match_pattern: std::option::Option::None, - module_item_kind: ParsedModuleItemKind::NotAModuleItem, - declaration_marker: DeclarationMarker::Unmarked, - expr_data: Rc::new(ExprData::NoExprData), - ident: None, - }) + { + let declared_leaf = Rc::new(Node { + occurrence_identity: Rc::new(NodeOccurrenceIdentity::OccurrenceSynthetic), + name: name.clone(), + span: span.clone(), + ident_span: ident_span.clone(), + children: Rc::new(vec![]), + connective: Connective::NoConnective, + params: Rc::new(vec![]), + inferred: std::option::Option::None, + return_cardinality: Cardinality::Required, + uses: Rc::new(vec![]), + body: std::option::Option::None, + transport: std::option::Option::None, + properties: Rc::new(vec![]), + type_annotation: std::option::Option::None, + is_self_recursive: false, + has_non_tail_self_call: false, + match_pattern: std::option::Option::None, + module_item_kind: ParsedModuleItemKind::NotAModuleItem, + declaration_marker: DeclarationMarker::Unmarked, + expr_data: Rc::new(ExprData::NoExprData), + ident: None, + }); + Rc::new(Node { + occurrence_identity: Rc::new(NodeOccurrenceIdentity::OccurrenceSynthetic), + name: name.clone(), + span: span.clone(), + ident_span: ident_span.clone(), + children: Rc::new(vec![]), + connective: Connective::NoConnective, + params: Rc::new(vec![]), + inferred: Some(Rc::new(InferredNode::Resolved { + node: declared_leaf.clone(), + })), + return_cardinality: Cardinality::Required, + uses: Rc::new(vec![]), + body: std::option::Option::None, + transport: std::option::Option::None, + properties: Rc::new(vec![]), + type_annotation: std::option::Option::None, + is_self_recursive: false, + has_non_tail_self_call: false, + match_pattern: std::option::Option::None, + module_item_kind: ParsedModuleItemKind::NotAModuleItem, + declaration_marker: DeclarationMarker::Unmarked, + expr_data: Rc::new(ExprData::NoExprData), + ident: None, + }) + } } pub fn resolved_callable_type(func_params: Rc>>, ret: Rc) -> Rc { @@ -1510,6 +1536,64 @@ match caller_decl_coords(scope.clone()) { } } +pub fn resource_declaration_identity( + scope: Rc, + resource: Rc, +) -> Option> { + { + let resolved = match resource.inferred.clone().as_deref().cloned() { + Some(InferredNode::Resolved { node: r, .. }) => r.clone(), + _ => resource.clone(), + }; + crate::v1_compiler_infer_env::declaration_ref_of_type_node( + resolved.clone(), + scope.type_env.clone().source_indices.clone(), + scope.type_env.clone(), + ) + } +} + +pub fn established_resource_binding( + scope: Rc, + required: Rc, + established: Rc>>, +) -> Option { + match resource_declaration_identity(scope.clone(), required.resource.clone()) { + std::option::Option::None => std::option::Option::None, + Some(required_decl) => match Rc::new({ + let mut __result = Vec::new(); + for e in established.iter().cloned() { + if match resource_declaration_identity(scope.clone(), e.resource.clone()) { + Some(e_decl) => (e_decl.clone() == required_decl.clone()), + std::option::Option::None => false, + } { + __result.push(e); + } + } + __result + }) + .first() + .cloned() + { + Some(e) => Some(e.binding_name.clone()), + std::option::Option::None => std::option::Option::None, + }, + } +} + +pub fn caller_resource_requirements(scope: Rc) -> Rc>> { + match v1_rt::map_get( + &scope.item_registry.clone(), + crate::v1_std_core::callable_identity(Rc::new(DeclaredCallableIdentity { + owner_module_path: scope.module_name.clone(), + decl_name: scope.caller_decl_name.clone(), + })), + ) { + Some(info) => info.resource_requirements.clone(), + std::option::Option::None => Rc::new(vec![]), + } +} + pub fn constructor_reference_admission_refusal( callee_name: String, span: Rc, @@ -21824,7 +21908,7 @@ pub fn local_binding_for_item( params: item.params.clone(), inferred: item.inferred.clone(), return_cardinality: item.return_cardinality.clone(), - uses: Rc::new(vec![]), + uses: item.uses.clone(), body: std::option::Option::None, transport: std::option::Option::None, properties: item.properties.clone(), @@ -24999,16 +25083,10 @@ pub fn build_item_info( ) -> Rc { { let kind = crate::v1_compiler_infer_items::item_kind(item.clone()); - let res_names = Rc::new({ - let mut __result = Vec::new(); - for u in item.uses.clone().iter().cloned() { - __result.push(crate::v1_std_core::resource_use_name_at( - u.clone(), - source_indices.clone(), - )); - } - __result - }); + let requirements = crate::v1_compiler_infer_items::resource_requirements_of_uses( + item.uses.clone(), + source_indices.clone(), + ); let item_name_str = crate::v1_std_core::authored_name_at(source_indices.clone(), item.clone()); match kind.clone() { @@ -25024,7 +25102,7 @@ pub fn build_item_info( source_indices.clone(), ) }, - resource_names: res_names.clone(), + resource_requirements: requirements.clone(), params: item.params.clone(), is_self_recursive: if (item.body.clone() == std::option::Option::None) { false @@ -25051,7 +25129,7 @@ pub fn build_item_info( module_name: module_name.clone(), kind: kind.clone(), service_names: Rc::new(vec![]), - resource_names: res_names.clone(), + resource_requirements: requirements.clone(), params: item.params.clone(), is_self_recursive: false, has_non_tail_self_call: false, diff --git a/src/v1/stage0/src/v1_compiler_infer_items.rs b/src/v1/stage0/src/v1_compiler_infer_items.rs index 5b9de068b3f..b188560371c 100644 --- a/src/v1/stage0/src/v1_compiler_infer_items.rs +++ b/src/v1/stage0/src/v1_compiler_infer_items.rs @@ -27,6 +27,7 @@ use crate::v1_std_core::LeafOwner::{LeafAmbiguous, SingleOwner}; pub use crate::v1_std_core::{ authored_name_at, expr_has_non_tail_self_call, expr_has_self_call, make_field_node, make_param_node, no_span, node_name_span, param_node_name_at, param_node_type_expr, + resource_use_name_at, resource_use_resource, }; pub use crate::v1_std_core::{ Cardinality, Connective, ErrorNode, InferredNode, LeafOwner, NewlineIndex, Node, @@ -57,13 +58,48 @@ pub enum ItemKind { OtherItem, } +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +pub struct ResourceRequirement { + pub binding_name: String, + pub resource: Rc, +} + +pub fn resource_requirements_of_uses( + uses: Rc>>, + source_indices: Rc>>, +) -> Rc>> { + Rc::new({ + let mut __result = Vec::new(); + for u in uses.iter().cloned() { + __result.push(Rc::new(ResourceRequirement { + binding_name: crate::v1_std_core::resource_use_name_at( + u.clone(), + source_indices.clone(), + ), + resource: crate::v1_std_core::resource_use_resource(u.clone()), + })); + } + __result + }) +} + +pub fn item_resource_names(info: Rc) -> Rc> { + Rc::new({ + let mut __result = Vec::new(); + for r in info.resource_requirements.clone().iter().cloned() { + __result.push(r.binding_name.clone()); + } + __result + }) +} + #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub struct ItemInfo { pub name: String, pub module_name: String, pub kind: ItemKind, pub service_names: Rc>, - pub resource_names: Rc>, + pub resource_requirements: Rc>>, pub params: Rc>>, pub is_self_recursive: bool, pub has_non_tail_self_call: bool, @@ -71,7 +107,7 @@ pub struct ItemInfo { pub fn item_is_effectful_callee(info: Rc) -> bool { (((info.service_names.clone().len() as i64) > 0) - || ((info.resource_names.clone().len() as i64) > 0)) + || ((item_resource_names(info.clone()).len() as i64) > 0)) } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] diff --git a/src/v1/stage0/src/v1_compiler_infer_service.rs b/src/v1/stage0/src/v1_compiler_infer_service.rs index 026a4120c39..3f04bc2c202 100644 --- a/src/v1/stage0/src/v1_compiler_infer_service.rs +++ b/src/v1/stage0/src/v1_compiler_infer_service.rs @@ -487,7 +487,9 @@ pub fn expand_transitive_services_once( module_name: info.module_name.clone(), kind: info.kind.clone(), service_names: merged.clone(), - resource_names: info.resource_names.clone(), + resource_requirements: info + .resource_requirements + .clone(), params: info.params.clone(), is_self_recursive: info.is_self_recursive.clone(), has_non_tail_self_call: info diff --git a/src/v1/stage0/src/v1_std_core.rs b/src/v1/stage0/src/v1_std_core.rs index 73353e7c797..1c42699c64b 100644 --- a/src/v1/stage0/src/v1_std_core.rs +++ b/src/v1/stage0/src/v1_std_core.rs @@ -1081,7 +1081,7 @@ pub fn diagnostic_to_message(d: Rc) -> String { CompilerDiagnostic::MethodExistenceFrontierAdmitted { method: m, receiver_type: t, trigger: tr, .. } => v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("method '".to_string(), m.clone()), "' on receiver type '".to_string()), t.clone()), "' is admitted by a declared unresolved-method frontier row; dissolves on: ".to_string()), tr.clone()), CompilerDiagnostic::AlgebraApplicationEvidenceUnavailable { receiver_type: t, argument_index: i, .. } => v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("algebra receiver application evidence unavailable for '".to_string(), t.clone()), "' at argument ".to_string()), (i.clone()).to_string()), ": structural members are not type arguments".to_string()), CompilerDiagnostic::ReceiverTypeUnestablished { .. } => "the receiver's own type was never established, so nothing is known about the method's existence here; this is an upstream type-propagation deficit, not a fact about the method".to_string(), - CompilerDiagnostic::FrontierOccurrenceBudgetExceeded { method: m, receiver_type: t, .. } => v1_rt::concat(v1_rt::concat(v1_rt::concat("the declared frontier row for '".to_string(), m.clone()), v1_rt::concat("' on receiver type '".to_string(), t.clone())), "' no longer matches what this module contains: its declared occurrence count and the count observed here differ, and both numbers are carried on this diagnostic. If MORE were observed, a new unresolved call has appeared and the receiver's type should be established rather than the count raised. If FEWER were observed, the deficit has partly dissolved and the row must be lowered or deleted so the ratchet keeps its new ground. The count is an equality, not a ceiling, in both directions.".to_string()), + CompilerDiagnostic::FrontierOccurrenceBudgetExceeded { method: m, receiver_type: t, declared: d, observed: o, .. } => v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("the declared frontier row for '".to_string(), m.clone()), v1_rt::concat("' on receiver type '".to_string(), t.clone())), v1_rt::concat(v1_rt::concat("' no longer matches what this module contains: the row declares ".to_string(), (d.clone()).to_string()), v1_rt::concat(" occurrence(s) and ".to_string(), (o.clone()).to_string()))), " were observed here. If MORE were observed, a new unresolved call has appeared and the receiver's type should be established rather than the count raised. If FEWER were observed, the deficit has partly dissolved and the row must be lowered or deleted so the ratchet keeps its new ground. The count is an equality, not a ceiling, in both directions.".to_string()), CompilerDiagnostic::TestCodeReferenced { referrer: r, target: t, .. } => v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("'".to_string(), r.clone()), "' references test code '".to_string()), t.clone()), "': a `test` declaration is entered only by the witness runner, so no declaration may call, name or import it. Move shared logic into an ordinary fn, or delete a test that only re-asserts other tests".to_string()), CompilerDiagnostic::TestCodeReferenceAdmitted { referrer: r, target: t, .. } => v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("'".to_string(), r.clone()), "' references test code '".to_string()), t.clone()), "'; admitted by the declared test-reference debt ledger (v1.compiler.compile test_reference_debt), which may only shrink".to_string()), CompilerDiagnostic::TestCodeReferenceRowOrphaned { referrer: r, .. } => v1_rt::concat(v1_rt::concat("the test-reference debt row for '".to_string(), r.clone()), "' names a module that no longer exists in the corpus: it is neither compiled here nor in the loaded name census. A row that can never be observed again must be deleted, not left to persist".to_string()), diff --git a/src/v1/stage0/src/v1_tests_claim_reference_derived_disposition_census_witness_test.rs b/src/v1/stage0/src/v1_tests_claim_reference_derived_disposition_census_witness_test.rs index 5114e1fec37..778a6304bcf 100644 --- a/src/v1/stage0/src/v1_tests_claim_reference_derived_disposition_census_witness_test.rs +++ b/src/v1/stage0/src/v1_tests_claim_reference_derived_disposition_census_witness_test.rs @@ -48,7 +48,7 @@ pub fn fixture_item_info(name: String, module_name: String) -> Rc { module_name: module_name.clone(), kind: ItemKind::FnItem, service_names: Rc::new(vec![]), - resource_names: Rc::new(vec![]), + resource_requirements: Rc::new(vec![]), params: Rc::new(vec![]), is_self_recursive: false, has_non_tail_self_call: false, diff --git a/src/v1/tests/claim/reference_derived_disposition_census_witness_test.dag b/src/v1/tests/claim/reference_derived_disposition_census_witness_test.dag index 37f3cc600c5..9f6d9fcb47d 100644 --- a/src/v1/tests/claim/reference_derived_disposition_census_witness_test.dag +++ b/src/v1/tests/claim/reference_derived_disposition_census_witness_test.dag @@ -35,7 +35,7 @@ fn fixture_item_info(name: String, module_name: String) -> ItemInfo { module_name: module_name, kind: FnItem, service_names: [], - resource_names: [], + resource_requirements: [], params: [], is_self_recursive: false, has_non_tail_self_call: false