diff --git a/dag/gunbc/plans/blackjack_onboarding.dag b/dag/gunbc/plans/blackjack_onboarding.dag index 3a17d31196e..e7ee88e3655 100644 --- a/dag/gunbc/plans/blackjack_onboarding.dag +++ b/dag/gunbc/plans/blackjack_onboarding.dag @@ -4,6 +4,7 @@ import std.dissolution { unbound_dissolution } import gunbc.plan { Plan, PlanRetiresWhen, PlanProjectsCommittedMarkdown } import std.markdown { MarkdownBlock, MarkdownTableRow, CodeBlock, TableBlock, AlignNone } import gunbc.plans.md_helpers { h2, h3, p, li, ul, ol, cell, row, quote, hr } +import extdeps.entropy { Urandom } fn code(text: String) -> MarkdownBlock { CodeBlock { code: text } } diff --git a/dag/gunbc/recurring_failure_mode/surface_shorthand_preempts_resolved_identity.dag b/dag/gunbc/recurring_failure_mode/surface_shorthand_preempts_resolved_identity.dag index 38943f5dd26..d4e9f605e83 100644 --- a/dag/gunbc/recurring_failure_mode/surface_shorthand_preempts_resolved_identity.dag +++ b/dag/gunbc/recurring_failure_mode/surface_shorthand_preempts_resolved_identity.dag @@ -27,6 +27,10 @@ data surface_shorthand_preempts_resolved_identity: RecurringFailureMode = Recurr "FOURTH SPECIMEN (2026-09-24, gunbc#12185, found by side-chat review). The service-receiver stamp joins on the bare NAME, not the owning declaration: v1.compiler.infer infer_var_binding_kind stamps ServiceValueBinding when the service registry holds the local's name and the local's type is the nominal reference the scope fold inserted (local_is_service_root_binding), without comparing that reference's owner module to the registry row's. gunbc.runner_guest_image calls `Filesystem.Write(...)` and reads `.success`/`.error`, which is extdeps.filesystem.filesystem_io's service Filesystem. When #12185 first named a source for that bare read, it imported std.resources { Filesystem } (a resource whose capability is lowercase `write` with output `written`), and the module still type-checked and emitted, because the registry row matched by name. The import was corrected to extdeps.filesystem.filesystem_io { Filesystem }, checked by reading the call: `.success` and `.error` exist only on that service's Write, not on the resource's `write`. The join itself is unrepaired. THE DISCRIMINATING RED IS UNAUTHORABLE ON EVERY ROUTE TODAY: on the seed, the name-only join is the defect, so a mutant import back to std.resources type-checks and emits green; on v2, v2.extdeps.languages.dag has no resource production (the resource shape refuses at parse) and v2.compiler.namespace_graft places only fn/data/type declarations in the containment tree, so a service receiver has no declaring path to assert. Next rung, two capabilities, either of which makes the red authorable: (a) the seed's service stamp requires the local's resolved declaration and the registry row to be the SAME declaration (owner module plus name), so a two-declaration fixture refuses the mutant import at the call; (b) v2 grafts service declarations into the containment tree and gains a resource production, so the receiver's declaring path is assertable.", + "FOURTH SPECIMEN CLIMBED (2026-09-24, node adhoc-a42f3901-8f2). The seed's service registry was the closure-wide census keyed by leaf name, and every bare-declared service's root was seeded as a local of every module, so the join could not see owners. A bare-declared service is now in a module's scope only when the module declares it or imports its owner by that name (v1.compiler.infer census_service_in_scope, imported_service_in_scope), and a receiver that would have reached the old join through another module's same-named declaration refuses ServiceOwnerMismatch, typed and located at the receiver, with no fallback (service_owner_mismatch_refusal). Collisions are decided by POSITION, never by insertion order: two VALUES under one name refuse at every receiver, while a type or resource in a type or uses position does not compete with a service in receiver position. The census itself was a second instance of the class -- it held ONE service per spelling, so a second same-named service replaced the first before any ambiguity was judged -- and now holds every declaration of a name (v1.compiler.infer_env service_census_candidates); a module's candidates are collected from its declarations and ALL its imports, `import *` included, and decided only over the whole set: exactly one service is admitted, two or more services or a service beside a function or data value refuse as AmbiguousReference, and the bare-reference closure (cli_run census_single_service_owner) refuses a plural pull rather than picking one. Evidence: test.claim.service_owner_identity_witness (service import accepted with the service's output typed; resource-only import refuses at the receiver; both imported, the uses-typed resource and the service receiver each resolve; two same-named designed services refuse in either import order and through `import *`, either one alone is admitted while both exist, and service + resource + data refuses in either order). The dotted service roots (shell., git., github., ...) never reach this registry, and scoping their root locals is a separate, resolver-censused change.", + + "RESIDUAL, recorded and not fixed: one spelling meaning two declarations in one module is still a meaning fork (DESIGN 3), made unambiguous but not removed. gunbc.runner_microvm_slot_controller writes Filesystem for std.resources's resource in its uses clauses and for extdeps.filesystem.filesystem_io's service at its receivers. Trigger, a capability: qualified or aliased import references, so a module can spell std.resources.Filesystem and the filesystem_io service differently in source.", + "Next-rung trigger: host literals and intentional primitive representations are typed parser/resolver constructions, and evaluator value construction is total over every resolved binding with no raw-spelling arm able to override it.)", ], diff --git a/dag/gunbc/roadmap/roadmap_served_observation.dag b/dag/gunbc/roadmap/roadmap_served_observation.dag index 51d6e080e84..d6440a3535e 100644 --- a/dag/gunbc/roadmap/roadmap_served_observation.dag +++ b/dag/gunbc/roadmap/roadmap_served_observation.dag @@ -30,6 +30,7 @@ import gunbc.roadmap_belt_actuate { belt_workflow_attempts_observed_json_for_instance, belt_workflow_attempts_http_status, BeltWorkflowAttempts, WorkflowAttemptsObserved, WorkflowAttemptsRefused, } +import extdeps.filesystem.filesystem_io { Filesystem } // THE DASHBOARD IS DISAGGREGATED INTO ONE PRODUCER AND ANY NUMBER OF STATELESS READERS. Before this // module the serve process observed the host on every request: each page render forked ~26 diff --git a/dag/gunbc/runner/runner_attempt_launch.dag b/dag/gunbc/runner/runner_attempt_launch.dag index 25feda59a2e..36f25577efe 100644 --- a/dag/gunbc/runner/runner_attempt_launch.dag +++ b/dag/gunbc/runner/runner_attempt_launch.dag @@ -69,8 +69,8 @@ import extdeps.systemd.systemd_run { SystemdRunProperty } import extdeps.shell.exec { ProcessOutcome, ProcessOutputPresent, ProcessOutputAbsent, ProcessRefused } import extdeps.posix.test_utility { PathExistenceReading, PathPresent, PathAbsentOrUnstattable, PathExistenceUnreadable } import extdeps.tools.stat { stat_owner_uid_command, stat_mode_command, stat_size_bytes_command, StatFieldReading, StatFieldRead, StatFieldUnread, stat_field_reading } -import std.resources { Filesystem } import gunbc.command_runner { run_shell_command_capture } +import extdeps.filesystem.filesystem_io { Filesystem } data runner_attempt_launch_disposition: ConstructionMechanism = SingleAuthority diff --git a/dag/gunbc/runner/runner_microvm_boot_probe.dag b/dag/gunbc/runner/runner_microvm_boot_probe.dag index 0588834d76b..5bf87a430cb 100644 --- a/dag/gunbc/runner/runner_microvm_boot_probe.dag +++ b/dag/gunbc/runner/runner_microvm_boot_probe.dag @@ -35,6 +35,7 @@ import gunbc.runner_microvm_host_ready { import gunbc.command_runner { ProcessObservation, run_shell_command_observe, run_shell_commands, LocalExec, } +import extdeps.filesystem.filesystem_io { Filesystem } data runner_microvm_boot_probe_disposition: ConstructionMechanism = SingleAuthority diff --git a/dag/gunbc/runner/runner_microvm_cell_readiness_store.dag b/dag/gunbc/runner/runner_microvm_cell_readiness_store.dag index 66e331ba21f..62cf258f699 100644 --- a/dag/gunbc/runner/runner_microvm_cell_readiness_store.dag +++ b/dag/gunbc/runner/runner_microvm_cell_readiness_store.dag @@ -6,7 +6,6 @@ import std.checked_arithmetic { checked_int_magnitude, CheckedNat, CheckedNatRea import std.algebra { trim } import std.disposition { ConstructionMechanism, SingleAuthority } import std.dissolution { DissolutionCondition, unbound_dissolution } -import std.resources { Filesystem } import extdeps.exec.command { LocalExec } import extdeps.shell.exec { ProcessOutcome, ProcessOutputPresent, ProcessOutputAbsent, ProcessRefused } import extdeps.tools.stat { @@ -43,6 +42,7 @@ import gunbc.runner_microvm_cell_readiness { CellReadinessWrite, CellReadinessWriteResult, WriteAccepted, apply_cell_readiness_write, record_of_write, record_cell, } +import extdeps.filesystem.filesystem_io { Filesystem } data runner_microvm_cell_readiness_store_disposition: ConstructionMechanism = SingleAuthority diff --git a/dag/gunbc/runner/runner_microvm_lifecycle_realize.dag b/dag/gunbc/runner/runner_microvm_lifecycle_realize.dag index e1437751408..7bb5144a6ef 100644 --- a/dag/gunbc/runner/runner_microvm_lifecycle_realize.dag +++ b/dag/gunbc/runner/runner_microvm_lifecycle_realize.dag @@ -4,7 +4,6 @@ import std.types { String, NonEmptyStr, Bool, List, Int } import std.nat { Nat } import std.disposition { ConstructionMechanism, SingleAuthority } import std.dissolution { DissolutionCondition, unbound_dissolution } -import std.resources { Filesystem } import std.measure { Second, second, second_count, ByteSize, byte_size } import std.algebra { trim } import extdeps.exec.command { ArgvCommand, LocalExec, argv_words } @@ -86,6 +85,7 @@ import gunbc.runner_microvm_lifecycle { UnitInvocationReading, UnitInvocationRead, UnitInvocationUnread, attempt_teardown_readback, settle_teardown, TeardownSettlement, } +import extdeps.filesystem.filesystem_io { Filesystem } data runner_microvm_lifecycle_realize_disposition: ConstructionMechanism = SingleAuthority diff --git a/dag/gunbc/runner/runner_microvm_slot_controller.dag b/dag/gunbc/runner/runner_microvm_slot_controller.dag index 61dd1131671..1b1602161f6 100644 --- a/dag/gunbc/runner/runner_microvm_slot_controller.dag +++ b/dag/gunbc/runner/runner_microvm_slot_controller.dag @@ -86,6 +86,7 @@ import gunbc.runner_microvm_lifecycle_realize { ControllerRefusedAdmission, ControllerRefusedBinding, ControllerQuarantinedUnattributedResidue, ControllerStartNotRecorded, ControllerRecovered, ControllerRanAttempt, } +import extdeps.filesystem.filesystem_io { Filesystem } data runner_microvm_slot_controller_disposition: ConstructionMechanism = SingleAuthority diff --git a/dag/gunbc/scm/repository_load.dag b/dag/gunbc/scm/repository_load.dag index a49ea539c7c..958662c7d1f 100644 --- a/dag/gunbc/scm/repository_load.dag +++ b/dag/gunbc/scm/repository_load.dag @@ -48,6 +48,7 @@ import extdeps.filesystem.filesystem_io { FilesystemReadSucceeded, FilesystemReadRefused, filesystem_read_outcome, + Filesystem } import extdeps.languages.json.parse { parse_json_document, diff --git a/dag/gunbc/seed_growth_admission.dag b/dag/gunbc/seed_growth_admission.dag index db4be865e54..ecdfca78cd6 100644 --- a/dag/gunbc/seed_growth_admission.dag +++ b/dag/gunbc/seed_growth_admission.dag @@ -4,6 +4,7 @@ import gunbc.evaluation_store_address_census_seed_growth { evaluation_store_addr import gunbc.primitive_egress_census_seed_growth { primitive_egress_census_seed_growth_justification } import gunbc.required_lane_resolution_census_seed_growth { required_lane_resolution_census_seed_growth_justification } import gunbc.bare_reference_channel_outcome_seed_growth { bare_reference_channel_outcome_seed_growth_justification } +import gunbc.service_owner_pull_seed_growth { service_owner_pull_seed_growth_justification } import gunbc.unimported_bare_provider_gate_seed_growth { unimported_bare_provider_gate_seed_growth_justification } import gunbc.persisted_resolved_graph_consumer_test_seed_growth { persisted_resolved_graph_format_wall_seed_growth_justification, @@ -311,6 +312,7 @@ fn seed_growth_justification_roster() -> List { primitive_egress_census_seed_growth_justification, required_lane_resolution_census_seed_growth_justification, bare_reference_channel_outcome_seed_growth_justification, + service_owner_pull_seed_growth_justification, floor_corpus_census_allowance_seed_growth_justification, unimported_bare_provider_gate_seed_growth_justification, enrolment_dead_band_seed_growth_justification diff --git a/dag/gunbc/service_owner_pull_seed_growth.dag b/dag/gunbc/service_owner_pull_seed_growth.dag new file mode 100644 index 00000000000..893302d6bfe --- /dev/null +++ b/dag/gunbc/service_owner_pull_seed_growth.dag @@ -0,0 +1,25 @@ +module gunbc.service_owner_pull_seed_growth + +import gunbc.roadmap_model { RoadmapNodeId } +import gunbc.seed_growth { SeedGrowthJustification } +import std.decl_ref { DeclarationRef, WholeDeclaration } + +// FORWARD-FREEZE RECEIPT for the hand Rust the plural service census required in the bare-reference +// closure (gunb-ai/gunbc#12265). PURPOSE (gunbc.v1_maintenance_standing v1_seed_standing): the +// closure is seed host code that already read `census.services` to decide which module a bare +// service head pulls; v1.compiler.infer_env now holds every declaration of a service name, so that +// reader had to decide over a set instead of reading one entry. Taking an element would have +// reintroduced the order-dependent join the change removes (gunbc.recurring_failure_mode +// surface_shorthand_preempts_resolved_identity), so the decision is one named, unit-tested function +// that defers to inference whenever the file imports an owner and refuses an unimported plural name rather than picking a winner. +data service_owner_pull_seed_growth_justification: SeedGrowthJustification = SeedGrowthJustification { + hand_authored_declarations: [ + DeclarationRef { module_path: "v1_compiler.cli_run", decl_name: "ServiceOwnerPull", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run", decl_name: "decide_service_owner_pull", field: WholeDeclaration }, + DeclarationRef { module_path: "v1_compiler.cli_run", decl_name: "census_single_service_owner", field: WholeDeclaration } + ], + reason: "The bare-reference provider walk (visit_bare_reference_providers) read the services census as one entry per name; the census now carries every declaration of a name, so its reader decides over the whole set. The walk runs for import-less files and, through unimported_bare_providers, for importing files too, so it reads the file's imports by module identity in both forms: when any owner of the name is imported the import provides it and v1.compiler.infer service_owner_conflicts_of decides (one admitted, several refused as AmbiguousReference); only a name no import reaches is resolved here, to exactly one owner or a refusal naming every owner. Pinned by cli_run service_owner_pull_tests.", + owning_dissolution_lane: "v1-hand-queue-drain" as RoadmapNodeId, + trigger: "Delete these items when the loader's bare-reference pull is decided by a .dag fold over typed import identity -- the same candidate authority v1.compiler.infer uses -- so the closure reads that decision instead of re-deriving which module a service head names from file text.", + current_boundary: "Called only from the bare-reference closure's resolve loop; reads the census and the file's import lines, writes nothing, and never substitutes a module for a refused plural pull." +} diff --git a/dag/gunbc/srv3/srv3_os_install_actuate.dag b/dag/gunbc/srv3/srv3_os_install_actuate.dag index c522a408944..4b2a46c5736 100644 --- a/dag/gunbc/srv3/srv3_os_install_actuate.dag +++ b/dag/gunbc/srv3/srv3_os_install_actuate.dag @@ -1,4 +1,13 @@ module gunbc.srv3_os_install_actuate +import extdeps.filesystem.filesystem_io { Filesystem } +import gunbc.auth.access_token_source { access_token_upsert_required_reason } +import gunbc.nbd_proxy_virtual_media_install { srv3_os_install_actuator_plan_standing } +import gunbc.srv3_actuate_shell_paths { srv3_bmcweb_login_body_path } +import gunbc.srv3_bmc_credential_resolve { bmcweb_login_body_content, materialize_bmc_login_password, observe_and_resolve_srv3_bmc_credential, srv3_os_install_actuate_credential_intent } +import gunbc.srv3_host_effect_apply { srv3_apply_to_process_exit, srv3_bmcweb_session_login_apply, srv3_bmcweb_token_extract_apply, srv3_nbd_proxy_serve_apply } +import std.process { exit_failure } +import gunbc.host_effect_realize { host_effect_apply, host_effect_apply_gated } +import gunbc.srv3_seeded_install_media_artifact { srv3_seeded_install_media_resolve } data gunbc_srv3_os_install_actuate_scope_disposition: Disposition = std.disposition.Terminal { diff --git a/dag/gunbc/tools/card_intake.dag b/dag/gunbc/tools/card_intake.dag index 5c0a01ffe7c..be0c52b1eca 100644 --- a/dag/gunbc/tools/card_intake.dag +++ b/dag/gunbc/tools/card_intake.dag @@ -1,4 +1,12 @@ module gunbc.tools.card_intake +import extdeps.filesystem.filesystem_io { Filesystem } +import extdeps.llm.anthropic_messages_api { AnthropicImageBlock, Base64Image, ImageJpeg, UserMessage, UserTextBlock } +import std.disposition { Disposition, Scaffold, SingleAuthority } +import extdeps.llm.anthropic_rest +import extdeps.tcgplayer.catalog +import extdeps.tcgplayer.pricing +import extdeps.tcgplayer.store +import extdeps.tcgplayer.tcgplayer data tcgplayer_client_id_source: CredentialSource = std.credentials.EnvVar { name: "TCGPLAYER_CLIENT_ID" } diff --git a/dag/test/claim/clock_freshness_witness.dag b/dag/test/claim/clock_freshness_witness.dag index 0efd23762cc..657b48d59dd 100644 --- a/dag/test/claim/clock_freshness_witness.dag +++ b/dag/test/claim/clock_freshness_witness.dag @@ -1,4 +1,5 @@ module test.claim.clock_freshness_witness +import extdeps.clock { Clock } fn witness_clock_now_positive_epoch() -> Bool { diff --git a/dag/test/claim/emit_host_typed_smoke_test.dag b/dag/test/claim/emit_host_typed_smoke_test.dag index b53d181216a..b137f8e214e 100644 --- a/dag/test/claim/emit_host_typed_smoke_test.dag +++ b/dag/test/claim/emit_host_typed_smoke_test.dag @@ -1,6 +1,7 @@ module test.claim.emit_host_typed_smoke_test import tools.emit_host_transport { expected_stdout_nul_run } +import extdeps.filesystem.filesystem_io { Filesystem } // Discriminating controls for the typed emit-host smokes (tools.emit_host_transport). The smokes // assert CONTENT equality against the in-substrate NUL golden; the control below emits five SPACES diff --git a/dag/test/claim/filesystem_list_hermetic_witness_test.dag b/dag/test/claim/filesystem_list_hermetic_witness_test.dag index 00886f8c9a5..0c7ac5b5057 100644 --- a/dag/test/claim/filesystem_list_hermetic_witness_test.dag +++ b/dag/test/claim/filesystem_list_hermetic_witness_test.dag @@ -3,8 +3,8 @@ module test.claim.filesystem_list_hermetic_witness_test import extdeps.filesystem.filesystem_io import std.types { Bool } +import gunbc.output_policy { ExpectSuccess } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } -import v2.extdeps.file_system { Filesystem } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly diff --git a/dag/test/claim/filesystem_write_closure_scale_witness.dag b/dag/test/claim/filesystem_write_closure_scale_witness.dag index 6ef9035cb0b..678cfe8bd0e 100644 --- a/dag/test/claim/filesystem_write_closure_scale_witness.dag +++ b/dag/test/claim/filesystem_write_closure_scale_witness.dag @@ -1,4 +1,6 @@ module test.claim.filesystem_write_closure_scale_witness +import extdeps.filesystem.filesystem_io { Filesystem } +import std.resources { ResourceHandle } data filesystem_write_closure_scale_anchor: ResourceHandle = ResourceHandle { diff --git a/dag/test/claim/filesystem_write_witness.dag b/dag/test/claim/filesystem_write_witness.dag index a1b62d26e3e..f3189b5222f 100644 --- a/dag/test/claim/filesystem_write_witness.dag +++ b/dag/test/claim/filesystem_write_witness.dag @@ -1,6 +1,6 @@ module test.claim.filesystem_write_witness +import extdeps.filesystem.filesystem_io { Filesystem } -import v2.extdeps.file_system { Filesystem } fn witness_write_then_read_roundtrip() -> Bool { let target = "/tmp/gunbc_fs_write_witness.txt" diff --git a/dag/test/claim/m4_universal_corpus_witness.dag b/dag/test/claim/m4_universal_corpus_witness.dag index efea2f931c7..debe25cf197 100644 --- a/dag/test/claim/m4_universal_corpus_witness.dag +++ b/dag/test/claim/m4_universal_corpus_witness.dag @@ -1,4 +1,5 @@ module test.claim.m4_universal_corpus_witness +import test.fixture.m4_universal_governed_probe { GovernedUniversalProbe } fn witness_universal_published_realizes() -> Bool { diff --git a/dag/test/claim/random_bytes_csprng_witness_test.dag b/dag/test/claim/random_bytes_csprng_witness_test.dag index 37943bbfa78..c81ae167c9c 100644 --- a/dag/test/claim/random_bytes_csprng_witness_test.dag +++ b/dag/test/claim/random_bytes_csprng_witness_test.dag @@ -1,4 +1,7 @@ module test.claim.random_bytes_csprng_witness +import extdeps.entropy { Urandom } +import std.encoding { base64_decode, base64_encode, base64_octets } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly diff --git a/dag/test/claim/service_owner_identity_witness_test.dag b/dag/test/claim/service_owner_identity_witness_test.dag new file mode 100644 index 00000000000..07d77d1d51d --- /dev/null +++ b/dag/test/claim/service_owner_identity_witness_test.dag @@ -0,0 +1,182 @@ +module test.claim.service_owner_identity_witness + +import gunbc.compile_diagnostic_census { + CompileDiagnosticCensus, + CompileDiagnosticCensusRow, + CensusObserved, + CensusNotRunnable, + census_count_at_key, + census_blocking_rows, + census_total_count +} +import std.types { String, Bool, Int } +import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } + +data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree + +// A SERVICE RECEIVER IS JOINED BY DECLARATION IDENTITY -- OWNER MODULE PLUS NAME -- NOT BY SPELLING. +// Two real declarations share the name Filesystem: extdeps.filesystem.filesystem_io's SERVICE +// (operation Write -> success, bytes_written, path, error) and std.resources's RESOURCE (capability +// write -> written). The seed's infer used to seed every bare-declared service of the corpus into +// every module and join a receiver to it by leaf name, so importing the resource and calling Write +// was typed from the service's operation (#12185 runner_guest_image; gunbc.recurring_failure_mode +// surface_shorthand_preempts_resolved_identity). The pair below is the discriminating fixture: ONE +// producer, and the arms differ only in which module the import names. +fn census_of(source: String) -> CompileDiagnosticCensus { + compile_dag_diagnostic_census(source) +} + +// EXACT: this class, this subject, blocking only. The NotRunnable arm is a visible sentinel, never a +// zero, so a harness that could not compile reads as a failure of both arms rather than a pass of one. +fn keyed_count(source: String, wanted_class: String, wanted_subject: String) -> Int { + match census_of(source: source) { + CensusObserved { rows: rows } => + census_count_at_key(rows: rows, diagnostic_class: wanted_class, subject_name: wanted_subject, blocking: true) + CensusNotRunnable { cause: _ } => 0 - 1 + } +} + +fn blocking_total(source: String) -> Int { + match census_of(source: source) { + CensusObserved { rows: rows } => census_total_count(rows: census_blocking_rows(rows: rows)) + CensusNotRunnable { cause: _ } => 0 - 1 + } +} + +fn owner_mismatch_count(source: String) -> Int { + keyed_count(source: source, wanted_class: "ServiceOwnerMismatch", wanted_subject: "Filesystem") +} + +// THE ONE-AXIS PRODUCER. The body reads the SERVICE's output field `success`, so the accepted arm +// asserts the ROUTE as well as the verdict: it compiles clean only if Write was typed from the +// service's operation. +// The constant import puts the service's module into the probe's closure WITHOUT naming the +// service, which is the shape of every real specimen: gunbc.runner_attempt_launch reached +// extdeps.filesystem.filesystem_io transitively while importing only std.resources's Filesystem. +fn write_probe_source(owner_module: String) -> String { + "module probe_service_owner_identity\nimport std.types { String, Bool }\nimport extdeps.filesystem.filesystem_io { filesystem_read_outcome }\nimport " + + owner_module + + " { Filesystem }\nfn stage(path: String, content: String) -> Bool {\n let wrote = Filesystem.Write(path: path, content: content)\n wrote.success\n}\n" +} + +data service_owner: String = "extdeps.filesystem.filesystem_io" +data resource_owner: String = "std.resources" + +// GREEN: the service imported by its owner, Write called -- no blocking diagnostic at all. +test fn the_imported_service_write_is_accepted() -> Bool { + blocking_total(source: write_probe_source(owner_module: service_owner)) == 0 +} + +// RED: the same program importing the same-named RESOURCE refuses, exactly once, at the receiver. +test fn the_resource_homonym_write_refuses_at_the_receiver() -> Bool { + owner_mismatch_count(source: write_probe_source(owner_module: resource_owner)) == 1 +} + +// The refusal belongs to the receiver, not to the import: a module that imports the resource and +// never uses the name as a receiver is not refused. +data resource_import_only_source: String = "module probe_service_owner_identity_import_only\nimport std.types { Bool }\nimport std.resources { Filesystem }\nfn nothing() -> Bool {\n true\n}\n" + +test fn importing_the_resource_alone_is_not_refused() -> Bool { + owner_mismatch_count(source: resource_import_only_source) == 0 +} + +// CONTROL, not a discriminator: with no import the service is not in the probe's closure, so the +// bare name was never joined -- it is an undefined variable before and after this change. It pins +// that the scoping did not turn the unbound case into a service join by another route. +data unimported_write_source: String = "module probe_service_owner_identity_unimported\nimport std.types { String, Bool }\nfn stage(path: String, content: String) -> Bool {\n let wrote = Filesystem.Write(path: path, content: content)\n wrote.success\n}\n" + +test fn an_unimported_service_is_not_reached_by_its_bare_name() -> Bool { + keyed_count(source: unimported_write_source, wanted_class: "InternalError", wanted_subject: "variable:Filesystem") == 1 +} + +// CONTROL for the collision rule: the service AND the same-named resource both imported -- the shape +// of gunbc.runner_microvm_slot_controller, which names the resource in a `uses fs: Filesystem` +// requirement and calls the service's Filesystem.Read. A resource is not a value in receiver +// position, so the pair is two declarations in two positions: the `uses fs: Filesystem` requirement +// names the resource and the receiver's Write is the service's. Asserted by key rather than by a +// blocking total, because std.resources's closure carries blocking diagnostics of its own that are +// not this claim's subject: no owner refusal, and Write's existence established. That key is not +// vacuous: it is exactly what the resource-only arm reports when no service answers Write. +data service_and_resource_source: String = "module probe_service_owner_identity_both\nimport std.types { String, Bool }\nimport extdeps.filesystem.filesystem_io { Filesystem }\nimport std.resources { Filesystem }\nfn stage(path: String, content: String) -> Bool {\n let wrote = Filesystem.Write(path: path, content: content)\n wrote.success\n}\nfn stage_under_requirement(path: String, content: String) -> Bool\n uses fs: Filesystem\n{\n let wrote = Filesystem.Write(path: path, content: content)\n wrote.success\n}\n" + +test fn a_resource_beside_the_imported_service_does_not_collide() -> Bool { + owner_mismatch_count(source: service_and_resource_source) == 0 + && keyed_count(source: service_and_resource_source, wanted_class: "MethodExistenceUndecided", wanted_subject: "Write") == 0 +} + +// RED for the collision rule: two VALUES under one name -- a bare service and a data declaration, +// designed in test.fixture.service_owner_identity because no real pair exists -- have no position to +// tell them apart, so the receiver refuses rather than taking whichever the scope fold inserted last. +data two_values_source: String = "module probe_service_owner_identity_two_values\nimport std.types { String }\nimport test.fixture.service_owner_identity.service { OwnerIdentityProbe }\nimport test.fixture.service_owner_identity.value { OwnerIdentityProbe }\nfn ping() -> String {\n let r = OwnerIdentityProbe.OwnerIdentityPing()\n r.ok\n}\n" + +test fn two_values_under_one_name_refuse_at_the_receiver() -> Bool { + keyed_count(source: two_values_source, wanted_class: "AmbiguousReference", wanted_subject: "OwnerIdentityProbe") == 1 +} + +// THE PLURAL HOMONYM, decided over the whole candidate set. Two modules declare a service named +// OwnerIdentityProbe (test.fixture.service_owner_identity.service and .service_b); a data value and a +// resource of that name live beside them. One producer, whose only axis is the ORDERED list of +// import lines, so every order is the same program up to import order -- and the verdict may not +// move with it. +fn owner_probe_source(import_lines: String) -> String { + "module probe_service_owner_plural\nimport std.types { String }\n" + + import_lines + + "fn ping() -> String {\n let r = OwnerIdentityProbe.OwnerIdentityPing()\n r.ok\n}\n" +} + +data svc_a_named: String = "import test.fixture.service_owner_identity.service { OwnerIdentityProbe }\n" +data svc_b_named: String = "import test.fixture.service_owner_identity.service_b { OwnerIdentityProbe }\n" +data svc_a_all: String = "import test.fixture.service_owner_identity.service\n" +data svc_b_all: String = "import test.fixture.service_owner_identity.service_b\n" +data resource_named: String = "import test.fixture.service_owner_identity.resource { OwnerIdentityProbe }\n" +data value_named: String = "import test.fixture.service_owner_identity.value { OwnerIdentityProbe }\n" + +fn ambiguous_count(import_lines: String) -> Int { + keyed_count(source: owner_probe_source(import_lines: import_lines), wanted_class: "AmbiguousReference", wanted_subject: "OwnerIdentityProbe") +} + +// ADMITTED, and by the route: no ambiguity, no owner refusal, and OwnerIdentityPing's existence established -- +// the key the unresolved arm reports, so a zero here is not vacuous. +fn admitted(import_lines: String) -> Bool { + let src = owner_probe_source(import_lines: import_lines) + keyed_count(source: src, wanted_class: "AmbiguousReference", wanted_subject: "OwnerIdentityProbe") == 0 + && keyed_count(source: src, wanted_class: "ServiceOwnerMismatch", wanted_subject: "OwnerIdentityProbe") == 0 + && keyed_count(source: src, wanted_class: "MethodExistenceUndecided", wanted_subject: "OwnerIdentityPing") == 0 +} + +test fn two_same_named_services_refuse_in_either_order() -> Bool { + ambiguous_count(import_lines: svc_a_named + svc_b_named) == 1 + && ambiguous_count(import_lines: svc_b_named + svc_a_named) == 1 +} + +test fn two_same_named_services_through_import_all_refuse_in_either_order() -> Bool { + ambiguous_count(import_lines: svc_a_all + svc_b_all) == 1 + && ambiguous_count(import_lines: svc_b_all + svc_a_all) == 1 +} + +test fn either_service_alone_is_admitted_while_both_exist() -> Bool { + admitted(import_lines: svc_a_named) && admitted(import_lines: svc_b_named) +} + +test fn either_service_alone_through_import_all_is_admitted() -> Bool { + admitted(import_lines: svc_a_all) && admitted(import_lines: svc_b_all) +} + +test fn a_service_beside_a_same_named_resource_is_admitted_in_either_order() -> Bool { + admitted(import_lines: svc_a_named + resource_named) && admitted(import_lines: resource_named + svc_a_named) +} + +test fn service_resource_and_value_refuse_in_either_order() -> Bool { + ambiguous_count(import_lines: svc_a_named + resource_named + value_named) == 1 + && ambiguous_count(import_lines: value_named + resource_named + svc_a_named) == 1 +} + +// TWO SERVICES AND A VALUE, in both orders: one refusal, whatever order the imports were written in. +// The census row keys the class and the name; the candidate population it reports -- all three +// owners, sorted -- is pinned where the decision is a supplied-value function, +// v1.compiler.infer service_candidates_conflict (cli_run service_candidates_conflict_tests), and +// this row is its inhabitance on the real compile path. +test fn two_services_and_a_value_refuse_once_in_either_order() -> Bool { + ambiguous_count(import_lines: svc_a_named + svc_b_named + value_named) == 1 + && ambiguous_count(import_lines: value_named + svc_b_named + svc_a_named) == 1 +} diff --git a/dag/test/claim/spark/pair_serving_authority_log_real_execution_witness_test.dag b/dag/test/claim/spark/pair_serving_authority_log_real_execution_witness_test.dag index eefe69eb596..9b0e57cda80 100644 --- a/dag/test/claim/spark/pair_serving_authority_log_real_execution_witness_test.dag +++ b/dag/test/claim/spark/pair_serving_authority_log_real_execution_witness_test.dag @@ -40,6 +40,7 @@ import gunbc.spark.pair_serving_authority_log { AuthorityTransition, AuthorityTransitionAppended, AuthorityTransitionStale, AuthorityTransitionRefused, pair_serving_authority_transition, authority_eq, } +import extdeps.filesystem.filesystem_io { Filesystem } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly diff --git a/dag/test/claim/tailscale_acl_witness_test.dag b/dag/test/claim/tailscale_acl_witness_test.dag index c9175a5083a..7f5651f7733 100644 --- a/dag/test/claim/tailscale_acl_witness_test.dag +++ b/dag/test/claim/tailscale_acl_witness_test.dag @@ -1,4 +1,23 @@ module test.claim.tailscale_acl_witness +import std.types { String, Bool } +import v2.std.algebra { length } +import std.disposition { Disposition, Scaffold, Terminal } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import extdeps.filesystem.filesystem_io { Filesystem } +import extdeps.tailscale.acl { + operator_tailscale_acl_policy, + tag_ci_quarantine_holds, + tag_dashboard_admin_plane_holds, + dashboard_cutover_ip_is_transient_in_tag_ci_grant, + dashboard_cutover_mac_dissolution_trigger, + ssh_members_reach_ci_nonroot_only, + ssh_dashboard_reaches_ci_root_and_nonroot, + operator_acl_security_spine_holds, + tag_dashboard, + tag_ci, + tailscale_grants_external_authority_holds +} +import gunbc.tailscale_acl_emit { expected_tailscale_acl_json } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly diff --git a/dag/test/fixture/effect_reach_conjunction/host_sink_provider.dag b/dag/test/fixture/effect_reach_conjunction/host_sink_provider.dag index 238ef301249..a9cb4f6067e 100644 --- a/dag/test/fixture/effect_reach_conjunction/host_sink_provider.dag +++ b/dag/test/fixture/effect_reach_conjunction/host_sink_provider.dag @@ -1,4 +1,5 @@ module test.fixture.effect_reach_conjunction.host_sink_provider +import extdeps.filesystem.filesystem_io { Filesystem } // The host-effect half of the fixture, and it carries no path literal of its own. The two // terms the derivation conjoins are deliberately split across separate modules here because diff --git a/dag/test/fixture/service_owner_identity/probe_named_a.dag b/dag/test/fixture/service_owner_identity/probe_named_a.dag new file mode 100644 index 00000000000..bc36d5a70ea --- /dev/null +++ b/dag/test/fixture/service_owner_identity/probe_named_a.dag @@ -0,0 +1,16 @@ +module test.fixture.service_owner_identity.probe_named_a + +import std.types { String } +import test.fixture.service_owner_identity.service { OwnerIdentityProbe } + +// A REAL MODULE, not a synthetic census source: importing ONE of the two same-named +// OwnerIdentityProbe services -- by name or by wildcard -- must compile clean. The provider walk +// reads this file's imports by module identity (cli_run decide_service_owner_pull) and names no +// provider for an imported owner, so which declaration the name means is decided only by +// v1.compiler.infer over the typed candidate set. The refusing forms (both imported) are not +// writable as corpus files; they are pinned by test.claim.service_owner_identity_witness and +// cli_run service_owner_pull_tests. +fn ping() -> String { + let r = OwnerIdentityProbe.OwnerIdentityPing() + r.ok +} diff --git a/dag/test/fixture/service_owner_identity/probe_named_b.dag b/dag/test/fixture/service_owner_identity/probe_named_b.dag new file mode 100644 index 00000000000..42e61af13e2 --- /dev/null +++ b/dag/test/fixture/service_owner_identity/probe_named_b.dag @@ -0,0 +1,16 @@ +module test.fixture.service_owner_identity.probe_named_b + +import std.types { String } +import test.fixture.service_owner_identity.service_b { OwnerIdentityProbe } + +// A REAL MODULE, not a synthetic census source: importing ONE of the two same-named +// OwnerIdentityProbe services -- by name or by wildcard -- must compile clean. The provider walk +// reads this file's imports by module identity (cli_run decide_service_owner_pull) and names no +// provider for an imported owner, so which declaration the name means is decided only by +// v1.compiler.infer over the typed candidate set. The refusing forms (both imported) are not +// writable as corpus files; they are pinned by test.claim.service_owner_identity_witness and +// cli_run service_owner_pull_tests. +fn ping() -> String { + let r = OwnerIdentityProbe.OwnerIdentityPing() + r.ok +} diff --git a/dag/test/fixture/service_owner_identity/probe_wildcard_a.dag b/dag/test/fixture/service_owner_identity/probe_wildcard_a.dag new file mode 100644 index 00000000000..cb2811cfa65 --- /dev/null +++ b/dag/test/fixture/service_owner_identity/probe_wildcard_a.dag @@ -0,0 +1,16 @@ +module test.fixture.service_owner_identity.probe_wildcard_a + +import std.types { String } +import test.fixture.service_owner_identity.service + +// A REAL MODULE, not a synthetic census source: importing ONE of the two same-named +// OwnerIdentityProbe services -- by name or by wildcard -- must compile clean. The provider walk +// reads this file's imports by module identity (cli_run decide_service_owner_pull) and names no +// provider for an imported owner, so which declaration the name means is decided only by +// v1.compiler.infer over the typed candidate set. The refusing forms (both imported) are not +// writable as corpus files; they are pinned by test.claim.service_owner_identity_witness and +// cli_run service_owner_pull_tests. +fn ping() -> String { + let r = OwnerIdentityProbe.OwnerIdentityPing() + r.ok +} diff --git a/dag/test/fixture/service_owner_identity/probe_wildcard_b.dag b/dag/test/fixture/service_owner_identity/probe_wildcard_b.dag new file mode 100644 index 00000000000..4bcbbfa2bb1 --- /dev/null +++ b/dag/test/fixture/service_owner_identity/probe_wildcard_b.dag @@ -0,0 +1,16 @@ +module test.fixture.service_owner_identity.probe_wildcard_b + +import std.types { String } +import test.fixture.service_owner_identity.service_b + +// A REAL MODULE, not a synthetic census source: importing ONE of the two same-named +// OwnerIdentityProbe services -- by name or by wildcard -- must compile clean. The provider walk +// reads this file's imports by module identity (cli_run decide_service_owner_pull) and names no +// provider for an imported owner, so which declaration the name means is decided only by +// v1.compiler.infer over the typed candidate set. The refusing forms (both imported) are not +// writable as corpus files; they are pinned by test.claim.service_owner_identity_witness and +// cli_run service_owner_pull_tests. +fn ping() -> String { + let r = OwnerIdentityProbe.OwnerIdentityPing() + r.ok +} diff --git a/dag/test/fixture/service_owner_identity/resource.dag b/dag/test/fixture/service_owner_identity/resource.dag new file mode 100644 index 00000000000..4f9907d2c00 --- /dev/null +++ b/dag/test/fixture/service_owner_identity/resource.dag @@ -0,0 +1,11 @@ +module test.fixture.service_owner_identity.resource + +// A resource named OwnerIdentityProbe, beside the fixture services and the fixture data value of the +// same name: a resource occupies a type or `uses` position, so it is never a rival to a service in +// receiver position, while a data value is. +resource OwnerIdentityProbe { + kind: Capability + mode: Read + acquire {} + release {} +} diff --git a/dag/test/fixture/service_owner_identity/service.dag b/dag/test/fixture/service_owner_identity/service.dag new file mode 100644 index 00000000000..d9fb7aca7b1 --- /dev/null +++ b/dag/test/fixture/service_owner_identity/service.dag @@ -0,0 +1,23 @@ +module test.fixture.service_owner_identity.service + +import std.types { String, Unit } + +// A bare-declared service -- its namespace root IS its name -- whose only purpose is to share a +// spelling with test.fixture.service_owner_identity.value's data declaration, so +// test.claim.service_owner_identity_witness can import two VALUES under one name and observe the +// refusal. Designed, not real: no real pair of value declarations shares a bare service's name. +service OwnerIdentityProbe { + operation OwnerIdentityPing { + input {} + output { ok: String from "stdout" } + readonly + transport shell { argv: ["true"] } + exit { + 0 => Unit + nonzero => String "unreachable: hermetic never dispatches wet" + } + mock_response { + 0 => { ok: "pong" } "fixture op realizes hermetically" + } + } +} diff --git a/dag/test/fixture/service_owner_identity/service_b.dag b/dag/test/fixture/service_owner_identity/service_b.dag new file mode 100644 index 00000000000..b265dc856bc --- /dev/null +++ b/dag/test/fixture/service_owner_identity/service_b.dag @@ -0,0 +1,23 @@ +module test.fixture.service_owner_identity.service_b + +import std.types { String, Unit } + +// The SECOND bare-declared service named OwnerIdentityProbe: two modules declaring one service name is +// the plural homonym test.claim.service_owner_identity_witness must decide over the whole candidate +// set, never by import or census order. +// Designed, not real: no two real modules declare one service name today. +service OwnerIdentityProbe { + operation OwnerIdentityPing { + input {} + output { ok: String from "stdout" } + readonly + transport shell { argv: ["true"] } + exit { + 0 => Unit + nonzero => String "unreachable: hermetic never dispatches wet" + } + mock_response { + 0 => { ok: "pong-b" } "fixture op realizes hermetically" + } + } +} diff --git a/dag/test/fixture/service_owner_identity/value.dag b/dag/test/fixture/service_owner_identity/value.dag new file mode 100644 index 00000000000..03497eacf63 --- /dev/null +++ b/dag/test/fixture/service_owner_identity/value.dag @@ -0,0 +1,6 @@ +module test.fixture.service_owner_identity.value + +import std.types { Int } + +// The second VALUE named OwnerIdentityProbe (see test.fixture.service_owner_identity.service). +data OwnerIdentityProbe: Int = 1 diff --git a/src/v1/00_core.dag b/src/v1/00_core.dag index 7ce6fb0a279..7ef0248c15e 100644 --- a/src/v1/00_core.dag +++ b/src/v1/00_core.dag @@ -438,6 +438,7 @@ type CompilerDiagnostic | ReferenceDerivedImportExportUnproven { name: String, referencing_module: String, provider_module: String, span: SourceSpan } | UnlistedVariantValueUse { name: String, span: SourceSpan } | AmbiguousReference { name: String, candidates: List, span: SourceSpan } + | ServiceOwnerMismatch { name: String, service_owner: String, bound_owner: String, span: SourceSpan } | AmbiguousAnonymousRecordLiteral { candidates: List, span: SourceSpan } | EffectfulSelfRecursionUnrealized { name: String, span: SourceSpan } | ModuleFilenameCollision { filename: String, modules: List, span: SourceSpan } @@ -666,6 +667,7 @@ fn diagnostic_to_span(d: CompilerDiagnostic) -> SourceSpan { ReferenceDerivedImportExportUnproven { name: _, referencing_module: _, provider_module: _, span: s } => s UnlistedVariantValueUse { name: _, span: s } => s AmbiguousReference { name: _, candidates: _, span: s } => s + ServiceOwnerMismatch { name: _, service_owner: _, bound_owner: _, span: s } => s AmbiguousAnonymousRecordLiteral { candidates: _, span: s } => s EffectfulSelfRecursionUnrealized { name: _, span: s } => s ModuleFilenameCollision { filename: _, modules: _, span: s } => s @@ -770,6 +772,7 @@ fn diagnostic_to_message(d: CompilerDiagnostic) -> String { concat("the provider of '", n, "' is present but its export could not be established: module '", rm, "' references the name, the registry maps it to '", p, "', and that module's transitive export surface does not establish it. The remedy is NOT an import -- the provider is already in the closure -- it is that the emitter cannot prove an export the provider may well hold") UnlistedVariantValueUse { name: n, span: _ } => concat("unlisted variant value use '", n, "' (a coproduct arm referenced but not in any import's name list)") AmbiguousReference { name: n, candidates: cs, span: _ } => concat("ambiguous reference '", n, "': ", to_string(value: cs |> count), " candidates: ", join(cs, separator: ", "), " — qualify by containment path, alias, or rename") + ServiceOwnerMismatch { name: n, service_owner: so, bound_owner: bo, span: _ } => concat("receiver '", n, "' is bound to '", bo, ".", n, "', not to the service '", so, ".", n, "': a service's operations are reached only through the declaration the module imports, never through a service that merely shares the name. Import the service from '", so, "' if its operations are meant, or call the operations '", bo, ".", n, "' declares") AmbiguousAnonymousRecordLiteral { candidates: cs, span: _ } => concat("ambiguous anonymous record literal shape matches ", to_string(value: cs |> count), " structs: ", join(cs, separator: ", "), " — add a nominal type") EffectfulSelfRecursionUnrealized { name: n, span: _ } => concat("effectful declaration '", n, "' calls itself: the Rust realization renders an effectful declaration as `async fn`, and rustc refuses recursion in an async fn without boxing (E0733), which no emitter performs. Realize the recursion as a loop, or move the self-call into a pure helper.") ModuleFilenameCollision { filename: f, modules: ms, span: _ } => concat("module filename collision: ", to_string(value: ms |> count), " modules render one emitted file name '", f, "': ", join(ms, separator: ", "), " — module_to_filename maps '.' to '_', so these names are indistinguishable at the emitted path; rename one module segment") @@ -910,6 +913,7 @@ fn diagnostic_disposition(d: CompilerDiagnostic) -> DiagnosticDisposition { ReferenceDerivedImportExportUnproven { name: _, referencing_module: _, provider_module: _, span: _ } => DiagnosticDisposition { severity: SeverityNonError, gate: GateAdvisoryTypecheck } UnlistedVariantValueUse { name: _, span: _ } => DiagnosticDisposition { severity: SeverityNonError, gate: GateAdvisoryTypecheck } AmbiguousReference { name: _, candidates: _, span: _ } => DiagnosticDisposition { severity: SeverityError, gate: GateBlocking } + ServiceOwnerMismatch { name: _, service_owner: _, bound_owner: _, span: _ } => DiagnosticDisposition { severity: SeverityError, gate: GateBlocking } AmbiguousAnonymousRecordLiteral { candidates: _, span: _ } => DiagnosticDisposition { severity: SeverityError, gate: GateBlocking } EffectfulSelfRecursionUnrealized { name: _, span: _ } => DiagnosticDisposition { severity: SeverityError, gate: GateBlocking } ModuleFilenameCollision { filename: _, modules: _, span: _ } => DiagnosticDisposition { severity: SeverityError, gate: GateBlocking } diff --git a/src/v1/04_env.dag b/src/v1/04_env.dag index c6517248dcf..7246952b993 100644 --- a/src/v1/04_env.dag +++ b/src/v1/04_env.dag @@ -218,7 +218,7 @@ type ServiceCensusEntry { type SymbolIndex { entries: Map global_bare: Map - services: Map + services: Map> transparent_alias_rep: Map type_head_exposures: Map } @@ -307,11 +307,23 @@ fn symbol_index_insert_decl(index: SymbolIndex, module_path: String, binding: Ty } } +// ONE ROW PER DECLARATION, NOT PER SPELLING. A service's census key is its authored name, and two +// modules may each declare a service of that name; keying the value by name alone made the second +// insertion REPLACE the first, so which declaration a name reached depended on census order. The +// value is therefore every declaration of that name -- identity is (module_path, name) -- and the +// consumer that must pick one decides over the whole set: exactly one, or a typed refusal. +fn service_census_candidates(services: Map>, name: String) -> List { + match map_get(services, name) { + Present { value: entries } => entries + Absent => [] + } +} + fn symbol_index_insert_service(index: SymbolIndex, name: String, module_path: String, item: Node) -> SymbolIndex { SymbolIndex { entries: index.entries, global_bare: index.global_bare, - services: map_insert(index.services, name, ServiceCensusEntry { module_path: module_path, item: item }), + services: map_insert(index.services, name, list_push(service_census_candidates(services: index.services, name: name), ServiceCensusEntry { module_path: module_path, item: item })), transparent_alias_rep: index.transparent_alias_rep, type_head_exposures: index.type_head_exposures } diff --git a/src/v1/04_infer.dag b/src/v1/04_infer.dag index 1ad9d6bfc67..89c75928391 100644 --- a/src/v1/04_infer.dag +++ b/src/v1/04_infer.dag @@ -51,7 +51,7 @@ import v1.std.core { Unmarked, ErrorNode, make_error_node, is_error_diagnostic, is_interpreter_blocking_diagnostic, diagnostic_to_span, AlgebraApplicationEvidenceUnavailable, FrontierOccurrenceKey, diagnostic_frontier_occurrence_key, - InternalError, TypeMismatch, TypeParameterInValuePosition, VariantCollision, SoleConstructorViolation, OptionalCastNotEliminated, BareNoneNotAdmittedByFieldType, ConstructorCallAdmissionRefused, UnresolvedType, FieldNotFound, MethodNotFound, MethodExistenceUndecided, MethodExistenceFrontierAdmitted, ReceiverTypeUnestablished, FrontierOccurrenceBudgetExceeded, MissingField, AmbiguousReference, ServiceConfigReferenceJudgmentDeferred, UnlistedVariantValueUse, + InternalError, TypeMismatch, TypeParameterInValuePosition, VariantCollision, SoleConstructorViolation, OptionalCastNotEliminated, BareNoneNotAdmittedByFieldType, ConstructorCallAdmissionRefused, UnresolvedType, FieldNotFound, MethodNotFound, MethodExistenceUndecided, MethodExistenceFrontierAdmitted, ReceiverTypeUnestablished, FrontierOccurrenceBudgetExceeded, MissingField, AmbiguousReference, ServiceOwnerMismatch, ServiceConfigReferenceJudgmentDeferred, UnlistedVariantValueUse, CallArgumentNameUnknown, CallPositionalSurplus, CallPositionalDeficit, CallNamedArgOnFunctionValue, CallArgumentDuplicate, EqualityOnFunctionMember, EqualityMemberUnjudgeable, TypeArgumentArityMismatch, @@ -251,6 +251,7 @@ type ModuleContext { resolved_items: List func_env: ResolvedFuncEnv svc_registry: Map> + svc_owner_conflicts: Map locals: Map variant_locals: Map item_registry: Map @@ -280,6 +281,7 @@ type InferScope { match_bound_names: Map module_name: String service_registry: Map> + service_owner_conflicts: Map item_registry: Map lambda_param_provenance: Map caller_decl_name: String @@ -424,7 +426,7 @@ fn merge_scope_from_imports(remaining: List, parent_index: Map - if titem.transport != none && titem.children |> count > 0 { + if titem.transport != none && titem.children |> count > 0 && import_path_decides_service(name: authored_name_at(source_indices: env.source_indices, node: titem), root: namespace_root_from_properties(properties: titem.properties, name: authored_name_at(source_indices: env.source_indices, node: titem), source_indices: env.source_indices), census: env.symbol_index.services) { let entries = titem.children |> map(c => OpEntry { name: authored_name_at(source_indices: env.source_indices, node: c), outputs: inferred_to_outputs(inferred: c.inferred, span: c.span, source_indices: env.source_indices), params: c.params } ) @@ -934,6 +936,175 @@ fn local_is_service_root_binding(scope: InferScope, name: String, binding: TypeB } } +// THE SERVICE A BARE NAME REACHES IS THE ONE THE MODULE NAMES, NEVER ONE THAT SHARES ITS SPELLING. +// The service registry used to be the corpus-wide census keyed by leaf name, and every service's +// namespace root was seeded as a local of every module. So `import std.resources { Filesystem }` -- +// a resource whose capabilities are lowercase -- reached extdeps.filesystem.filesystem_io's service +// Filesystem and typed `Filesystem.Write(...)` from that service's operation: two declarations joined +// by spelling, with the owner module never compared (gunbc.recurring_failure_mode +// surface_shorthand_preempts_resolved_identity). A bare-declared service -- one whose namespace root +// IS its name -- is now in a module's scope only when the module declares it or imports its owner +// naming it, so the join is on declaration identity: owner module plus name. A dotted service +// (`shell.Exec`) is keyed by its dotted name, which no bare local reaches, and is not scoped here; +// scoping its root locals is a separate, resolver-censused change. +fn import_names(imp: ResolvedImport, name: String) -> Bool { + imp.is_all || (imp.specific_names |> any(n => n == name)) +} + +// The import fold registers an imported module's services one import at a time, so a name two +// imports both supply would be decided by import order. It keeps only what that order cannot +// change: a dotted service with exactly one declaration in the census. Every bare-declared service +// and every plural name is decided by the census fold over the whole candidate set. +fn import_path_decides_service(name: String, root: String, census: Map>) -> Bool { + root != name && (service_census_candidates(services: census, name: name) |> count) <= 1 +} + +fn census_service_in_scope(owner_module: String, name: String, root: String, module_name: String, resolved_imports: List) -> Bool { + root != name || owner_module == module_name || (resolved_imports |> any(imp => imp.module_path == owner_module && import_names(imp: imp, name: name))) +} + +// WHAT A SERVICE NAME MEANS IN ONE MODULE IS DECIDED OVER EVERY CANDIDATE, NEVER OVER THE FIRST. +// The census holds every declaration of a service name (v1.compiler.infer_env +// service_census_candidates), and a module's candidates are the ones it declares or imports -- +// by name or by `import *` (census_service_in_scope). The decision is taken only after the whole +// set is collected, so no insertion or import order can pick a winner: +// - exactly one service candidate, and nothing else the imports bind to that name as a VALUE: +// admitted (a type or a resource under the same name is a different position, not a rival); +// - two or more service candidates, or one service beside a function or data value of that +// name: ServiceCandidatesAmbiguous, refused as AmbiguousReference wherever the name is read; +// - no service candidate while another declaration of that name is imported and a service of +// that name is in the closure: ServiceNotInScope, refused as ServiceOwnerMismatch where the +// old bare-name join would have answered. +type ServiceOwnerConflict + = ServiceNotInScope { service_owners: List, bound_owners: List } + | ServiceCandidatesAmbiguous { candidates: List } + +fn service_scope_candidates(entries: List, name: String, module_name: String, resolved_imports: List, source_indices: Map) -> List { + entries |> filter(e => census_service_in_scope(owner_module: e.module_path, name: name, root: namespace_root_from_properties(properties: e.item.properties, name: name, source_indices: source_indices), module_name: module_name, resolved_imports: resolved_imports)) +} + +// Every module OTHER than a service owner that this module's imports bind `name` from: a name +// list that states it, or an `import *` of a module that declares it. +fn non_service_import_owners(name: String, service_owners: List, resolved_imports: List, parent_index: Map, source_indices: Map) -> List { + sorted_distinct_names(names: resolved_imports + |> filter(imp => !(service_owners |> any(o => o == imp.module_path))) + |> filter(imp => (imp.specific_names |> any(n => n == name)) || (imp.is_all && module_declares_named(parent_index: parent_index, module_path: imp.module_path, name: name, source_indices: source_indices))) + |> map(imp => imp.module_path)) +} + +// Order-free and duplicate-free: a module set, not a sequence, so no caller can read a winner off it. +fn sorted_distinct_names(names: List) -> List { + let members: Map = fold(names, init: empty_map(), f: (acc, n) => map_insert(acc, n, true)) + sorted_map_keys(members) +} + +fn service_owner_conflicts_of(services: Map>, resolved_imports: List, parent_index: Map, module_name: String, source_indices: Map) -> Map { + fold(services |> map_keys, init: empty_map(), f: (acc, sname) => + match map_get(services, sname) { + Absent => acc + Present { value: entries } => + let scope_owners = sorted_distinct_names(names: service_scope_candidates(entries: entries, name: sname, module_name: module_name, resolved_imports: resolved_imports, source_indices: source_indices) |> map(e => e.module_path)) + let all_owners = sorted_distinct_names(names: entries |> map(e => e.module_path)) + let others = non_service_import_owners(name: sname, service_owners: all_owners, resolved_imports: resolved_imports, parent_index: parent_index, source_indices: source_indices) + let value_owners = others |> filter(o => module_declares_value_named(parent_index: parent_index, module_path: o, name: sname, source_indices: source_indices)) + match service_candidates_conflict(name: sname, service_owners: scope_owners, value_owners: value_owners) { + Present { value: conflict } => map_insert(acc, sname, conflict) + Absent => + if (scope_owners |> count) == 0 && (others |> count) > 0 { + map_insert(acc, sname, ServiceNotInScope { service_owners: all_owners, bound_owners: others }) + } else { acc } + } + } + ) +} + +// THE VALUE-POSITION DECISION, over the whole candidate population and nothing else. Its inputs are +// every in-scope service owner and every imported owner of a function or data value of that name, +// and its only answers are "no rival" or the full sorted union of both sets -- so two services and +// a value report all three, in one order, whatever order the imports were written in. +fn service_candidates_conflict(name: String, service_owners: List, value_owners: List) -> ServiceOwnerConflict? { + let services = service_owners |> count + if services >= 2 || (services == 1 && (value_owners |> count) >= 1) { + Present { value: ServiceCandidatesAmbiguous { candidates: sorted_distinct_names(names: concat(service_owners, value_owners)) |> map(o => concat(o, ".", name)) } } + } else { none } +} + +// A CONFLICTED NAME HAS NO REGISTRY ROW AND NO ROOT LOCAL. The conflict is decided before either +// is written, so no consumer can read a winner by skipping the conflict map: the census fold does +// not write one, and a module's own declared service of that name is dropped here too. +fn without_conflicted_names(registry: Map>, conflicts: Map) -> Map> { + fold(sorted_map_keys(registry), init: empty_map(), f: (acc, k) => + if map_has(conflicts, k) { acc } else { + match map_get(registry, k) { + Present { value: v } => map_insert(acc, k, v) + Absent => acc + } + } + ) +} + +fn without_conflicted_bindings(bindings: Map, conflicts: Map) -> Map { + fold(sorted_map_keys(bindings), init: empty_map(), f: (acc, k) => + if map_has(conflicts, k) { acc } else { + match map_get(bindings, k) { + Present { value: v } => map_insert(acc, k, v) + Absent => acc + } + } + ) +} + +fn module_declares_named(parent_index: Map, module_path: String, name: String, source_indices: Map) -> Bool { + match map_get(parent_index, module_path) { + Present { value: typed_parent } => + typed_parent.items |> any(titem => authored_name_at(source_indices: source_indices, node: titem) == name) + Absent => false + } +} + +fn module_declares_value_named(parent_index: Map, module_path: String, name: String, source_indices: Map) -> Bool { + match map_get(parent_index, module_path) { + Present { value: typed_parent } => + typed_parent.items |> any(titem => authored_name_at(source_indices: source_indices, node: titem) == name && match titem.module_item_kind { + ModuleItemFunction => true + ModuleItemDataValue => true + _ => false + }) + Absent => false + } +} + +// THE REFUSAL IS AT THE RECEIVER, and it fires exactly where the bare-name join used to decide: the +// name is unbound here (the old census root would have answered it) or bound to the nominal reference +// a service root carries -- or, for a two-values collision, wherever the name is read. A body +// binder -- a parameter, a `let`, a `uses` resource -- is its own declaration and is judged by its +// own type, so a resource +// reached through `uses` keeps its lowercase capabilities and is not refused. There is no fallback +// to the service: the name refuses, typed, and the author picks the declaration. +fn service_owner_mismatch_refusal(name: String, span: SourceSpan, scope: InferScope) -> InferResult? { + match map_get(scope.service_owner_conflicts, name) { + Absent => none + Present { value: conflict } => + if map_has(scope.body_locals, name) { none } else { + match conflict { + ServiceCandidatesAmbiguous { candidates: cs } => + Present { value: ambiguous_reference_refusal(name: name, candidates: cs, span: span, scope: scope) } + ServiceNotInScope { service_owners: sos, bound_owners: bos } => + let reaches_join = match map_get(scope.locals, name) { + Absent => true + Present { value: binding } => binding.resolved == nominal_type_ref(name: name) + } + if !reaches_join { none } else { + Present { value: InferResult { + typed: semantic_expr_error_node(message: concat("service owner mismatch for '", name, "'"), span: span), + diagnostics: [make_error_node(diagnostic: ServiceOwnerMismatch { name: name, service_owner: join(sos, separator: " | "), bound_owner: join(bos, separator: " | "), span: span }, module_name: scope.module_name)] + } } + } + } + } + } +} + // THE CAUSE IS THE TYPE'S, THE LOCATION IS THE CALL'S. algebra_child_or_placeholder mints // AlgebraApplicationEvidenceUnavailable spanned at the CARRIER DECLARATION, which is the only // span it has -- it is reasoning about a type, not about a call site. Emitted unchanged, that @@ -6360,6 +6531,7 @@ fn build_params_scope(scope: InferScope, params: List) -> InferScope { match_bound_names: scope.match_bound_names, module_name: scope.module_name, service_registry: scope.service_registry, + service_owner_conflicts: scope.service_owner_conflicts, item_registry: scope.item_registry, caller_decl_name: scope.caller_decl_name, lambda_param_provenance: empty_map(), @@ -6396,6 +6568,7 @@ fn extend_scope(scope: InferScope, name: String, resolved: Node, provenance: Sub match_bound_names: scope.match_bound_names, module_name: scope.module_name, service_registry: scope.service_registry, + service_owner_conflicts: scope.service_owner_conflicts, item_registry: scope.item_registry, caller_decl_name: scope.caller_decl_name, lambda_param_provenance: scope.lambda_param_provenance, @@ -6413,6 +6586,7 @@ fn extend_scope_match_bound(scope: InferScope, name: String, resolved: Node, pro match_bound_names: map_insert(scope.match_bound_names, name, true), module_name: scope.module_name, service_registry: scope.service_registry, + service_owner_conflicts: scope.service_owner_conflicts, item_registry: scope.item_registry, caller_decl_name: scope.caller_decl_name, lambda_param_provenance: scope.lambda_param_provenance, @@ -6436,6 +6610,7 @@ fn extend_scope_with_params(scope: InferScope, params: List) -> InferSco match_bound_names: scope.match_bound_names, module_name: scope.module_name, service_registry: scope.service_registry, + service_owner_conflicts: scope.service_owner_conflicts, item_registry: scope.item_registry, caller_decl_name: scope.caller_decl_name, lambda_param_provenance: scope.lambda_param_provenance, @@ -6591,6 +6766,7 @@ fn infer_method_args_with_fold(method_name: String?, method_args: List, fo match_bound_names: scope.match_bound_names, module_name: scope.module_name, service_registry: scope.service_registry, + service_owner_conflicts: scope.service_owner_conflicts, item_registry: scope.item_registry, caller_decl_name: scope.caller_decl_name, lambda_param_provenance: prov_map, @@ -6614,6 +6790,7 @@ fn infer_method_args_with_fold(method_name: String?, method_args: List, fo match_bound_names: scope.match_bound_names, module_name: scope.module_name, service_registry: scope.service_registry, + service_owner_conflicts: scope.service_owner_conflicts, item_registry: scope.item_registry, caller_decl_name: scope.caller_decl_name, lambda_param_provenance: nf_prov_map, @@ -6856,6 +7033,9 @@ fn qualified_or_service_projection(texpr: Node, scope: InferScope, span: SourceS match qualified_value_projection(texpr: texpr, scope: scope, span: span, expected: expected) { Present { value: proj } => Present { value: proj } Absent => + match dotted_service_ambiguity_refusal(texpr: texpr, scope: scope, span: span) { + Present { value: refusal } => Present { value: refusal } + Absent => match service_spine_projection(texpr: texpr, scope: scope) { Absent => none Present { value: svc_type } => @@ -6870,6 +7050,24 @@ fn qualified_or_service_projection(texpr: Node, scope: InferScope, span: SourceS name_span: kernel_span(name: svc_name), )) } } + } + } +} + +// A dotted service name with two or more declarations in scope refuses at the spine, over the +// whole candidate set, exactly as a bare one does at its receiver: the registry holds no row for a +// plural name, so without this the reference would fall through to an unrelated arm. +fn dotted_service_ambiguity_refusal(texpr: Node, scope: InferScope, span: SourceSpan) -> InferResult? { + match field_access_spine(texpr: texpr, source_indices: scope.type_env.source_indices) { + Absent => none + Present { value: spine } => + if spine_root_is_shadowed(scope: scope, root: spine.root) { none } else { + match map_get(scope.service_owner_conflicts, spine.dotted) { + Present { value: ServiceCandidatesAmbiguous { candidates: cs } } => + Present { value: ambiguous_reference_refusal(name: spine.dotted, candidates: cs, span: span, scope: scope) } + _ => none + } + } } } @@ -7145,6 +7343,9 @@ fn infer_expr_body(texpr: Node, scope: InferScope, expected: Node?) -> InferResu match constructor_reference_admission_unshadowed(callee_name: name, span: span, scope: scope) { Present { value: refusal } => refusal Absent => + match service_owner_mismatch_refusal(name: name, span: span, scope: scope) { + Present { value: refusal } => refusal + Absent => match map_get(scope.locals, name) { Present { value: binding } => let scope_parent = lookup_variant_parent_enum(scope: scope, name: name) @@ -7317,6 +7518,7 @@ fn infer_expr_body(texpr: Node, scope: InferScope, expected: Node?) -> InferResu } } } + } ExprFieldAccess { summary: _ } => let field_name = field_access_field_at(texpr: texpr, source_indices: scope.type_env.source_indices) @@ -8505,7 +8707,7 @@ fn infer_expr_body(texpr: Node, scope: InferScope, expected: Node?) -> InferResu type_env: lam_scope.type_env, func_env: lam_scope.func_env, locals: lam_scope.locals, body_locals: lam_scope.body_locals, match_bound_names: lam_scope.match_bound_names, - module_name: lam_scope.module_name, service_registry: lam_scope.service_registry, + module_name: lam_scope.module_name, service_registry: lam_scope.service_registry, service_owner_conflicts: lam_scope.service_owner_conflicts, item_registry: lam_scope.item_registry, caller_decl_name: lam_scope.caller_decl_name, lambda_param_provenance: empty_map(), in_flight_lambda_param_names: concat(lam_scope.in_flight_lambda_param_names, lam_params), enclosing_declared_type_param_names: lam_scope.enclosing_declared_type_param_names @@ -12171,6 +12373,7 @@ fn infer_item(item: Node, scope: InferScope) -> TypedItemResult { match_bound_names: fn_scope.match_bound_names, module_name: fn_scope.module_name, service_registry: fn_scope.service_registry, + service_owner_conflicts: fn_scope.service_owner_conflicts, item_registry: fn_scope.item_registry, caller_decl_name: fn_decl_name, lambda_param_provenance: fn_scope.lambda_param_provenance, @@ -13595,9 +13798,9 @@ fn census_with_resolved_fn_sigs(index: SymbolIndex, source_indices: Map match map_get(index.services, k) { - Present { value: sentry } => - let upgraded = census_upgrade_service_item(item: sentry.item, module_path: sentry.module_path, census: index, source_indices: source_indices) - if upgraded == sentry.item { acc } else { map_insert(acc, k, ServiceCensusEntry { module_path: sentry.module_path, item: upgraded }) } + Present { value: sentries } => + let upgraded = sentries |> map(sentry => ServiceCensusEntry { module_path: sentry.module_path, item: census_upgrade_service_item(item: sentry.item, module_path: sentry.module_path, census: index, source_indices: source_indices) }) + if upgraded == sentries { acc } else { map_insert(acc, k, upgraded) } Absent => acc } ) @@ -14623,9 +14826,13 @@ fn build_module_context(contributions: List, ) let variant_collision_errors = variant_fold.collision_errors let env_variant_locals = merge_kernel_variant_locals_low_priority(env: env, locals: variant_fold.locals) + let service_conflicts = service_owner_conflicts_of(services: env.symbol_index.services, resolved_imports: resolved_imports, parent_index: parent_index, module_name: module_name, source_indices: env.source_indices) let census_scope = fold(env.symbol_index.services |> map_keys, init: InferScopeComponents { svc_registry: empty_map(), svc_locals: empty_map() }, f: (acc, sname) => match map_get(env.symbol_index.services, sname) { - Present { value: sentry } => + Present { value: sentries } => + let candidates = service_scope_candidates(entries: sentries, name: sname, module_name: module_name, resolved_imports: resolved_imports, source_indices: env.source_indices) + if (candidates |> count) != 1 || map_has(service_conflicts, sname) { acc } else { + fold(candidates, init: acc, f: (acc, sentry) => let sitem = sentry.item let entries = sitem.children |> map(c => OpEntry { @@ -14643,6 +14850,8 @@ fn build_module_context(contributions: List, svc_registry: map_insert(acc.svc_registry, sname, entries), svc_locals: map_insert(acc.svc_locals, root, nominal_type_binding(name: root)) } + ) + } Absent => acc } ) @@ -14650,8 +14859,8 @@ fn build_module_context(contributions: List, remaining: resolved_imports, parent_index: parent_index, env: env, - svc_registry: map_merge(census_scope.svc_registry, local.svc_registry), - svc_locals: map_merge(census_scope.svc_locals, local.svc_locals) + svc_registry: map_merge(census_scope.svc_registry, without_conflicted_names(registry: local.svc_registry, conflicts: service_conflicts)), + svc_locals: map_merge(census_scope.svc_locals, without_conflicted_bindings(bindings: local.svc_locals, conflicts: service_conflicts)) ) let parent_envs = merge_func_env_views_by_owner(flat_map(resolved_imports, imp => match map_get(parent_index, imp.module_path) { @@ -14674,6 +14883,7 @@ fn build_module_context(contributions: List, resolved_items: local.resolved_items, func_env: bind_local_func_conformance(func_env: resolve_result.func_env, env: env, module_name: module_name), svc_registry: merged_scope.svc_registry, + svc_owner_conflicts: service_conflicts, locals: all_locals, variant_locals: env_variant_locals, item_registry: local.item_registry, @@ -14748,6 +14958,7 @@ fn typecheck_module(resolved: ResolvedModule, parent_index: Map InferScope { match_bound_names: empty_map(), module_name: "", service_registry: empty_map(), + service_owner_conflicts: empty_map(), item_registry: empty_map(), lambda_param_provenance: empty_map(), caller_decl_name: "", @@ -289,6 +290,7 @@ fn module_emit_scope(typed_module: TypedModule) -> InferScope { match_bound_names: empty_map(), module_name: authored_name_at(source_indices: typed_module.type_env.source_indices, node: typed_module.module), service_registry: empty_map(), + service_owner_conflicts: empty_map(), item_registry: typed_module.item_registry, lambda_param_provenance: empty_map(), caller_decl_name: "", diff --git a/src/v1/stage0/src/bin/infer_semantics_witness.rs b/src/v1/stage0/src/bin/infer_semantics_witness.rs index b8356b8e9d9..8d092e8f976 100644 --- a/src/v1/stage0/src/bin/infer_semantics_witness.rs +++ b/src/v1/stage0/src/bin/infer_semantics_witness.rs @@ -367,6 +367,7 @@ fn empty_infer_scope() -> Rc { match_bound_names: Rc::new(im::HashMap::new()), module_name: "test".to_string(), service_registry: Rc::new(im::HashMap::new()), + service_owner_conflicts: Rc::new(im::HashMap::new()), item_registry: Rc::new(im::HashMap::new()), lambda_param_provenance: Rc::new(im::HashMap::new()), caller_decl_name: "".to_string(), diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 000812a2cfb..302ef3b80d5 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -9616,6 +9616,8 @@ fn visit_bare_reference_providers( }); let self_declared = module_self_declared_names(&sf.content); let explicit_imports = explicit_import_member_names(&sf.content); + let imported_modules: BTreeSet = + extract_import_paths(&sf.content).into_iter().collect(); // SUBSTRATE VOCABULARY IS NOT A MODULE MEMBER. The 8 kernel type names // (`std_types::kernel_type_set` -- String/Int/Bool/Float/Secret/Json/Unit/Bytes) and the // container carrier spellings (`std_types::container_type_arity`, itself derived from the @@ -9666,8 +9668,7 @@ fn visit_bare_reference_providers( |census: &Rc| -> Result<(Option, &'static str), String> { if service_head { return Ok(( - v1_rt::map_get(&census.services, name.clone()) - .map(|entry| entry.module_path.clone()), + census_single_service_owner(census, &name, &file_rel, &imported_modules)?, "service", )); } @@ -9729,8 +9730,12 @@ fn visit_bare_reference_providers( }, None => ( if in_call_position { - v1_rt::map_get(&census.services, name.clone()) - .map(|entry| entry.module_path.clone()) + census_single_service_owner( + census, + &name, + &file_rel, + &imported_modules, + )? } else { None }, @@ -22052,6 +22057,208 @@ pub enum ClosureBareDisposition { NoOnChainCandidate, } +/// THE SERVICES CENSUS HOLDS EVERY DECLARATION OF A NAME (v1.compiler.infer_env +/// service_census_candidates), so the one module a bare pull resolves to is decided over that +/// whole set, and never by taking an element of it. +/// +/// THE FILE'S IMPORTS COME FIRST, BY MODULE IDENTITY, IN BOTH FORMS. The provider walk runs for +/// import-less files on the bare channel and, through `unimported_bare_providers`, for files that +/// DO import. When the file imports any owner of the name -- `import m { Name }` or `import m` -- +/// that import already provides the module, and which declaration the name means is +/// v1.compiler.infer's decision over the typed candidate set (service_owner_conflicts_of), which +/// admits one owner and refuses several with a located AmbiguousReference. The walk therefore +/// names no provider there and decides nothing: a narrower reading of the imports here is how it +/// came to refuse a wildcard import the compiler admits. Only a name no import reaches is the +/// walk's to resolve: exactly one owner, or a refusal naming every owner. +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum ServiceOwnerPull { + /// No module declares a service of that name. + NoServiceOwner, + /// An imported module owns it; the import provides it and inference decides. + DecidedByImport, + /// No import reaches it and exactly one module declares it. + Pull(String), +} + +pub(crate) fn decide_service_owner_pull( + owners: &BTreeSet, + imported_modules: &BTreeSet, +) -> Result> { + if owners.is_empty() { + return Ok(ServiceOwnerPull::NoServiceOwner); + } + if owners.iter().any(|o| imported_modules.contains(o)) { + return Ok(ServiceOwnerPull::DecidedByImport); + } + if owners.len() == 1 { + return Ok(ServiceOwnerPull::Pull( + owners.iter().next().cloned().unwrap_or_default(), + )); + } + Err(owners.iter().cloned().collect()) +} + +fn census_single_service_owner( + census: &Rc, + name: &str, + file_rel: &str, + imported_modules: &BTreeSet, +) -> Result, String> { + let owners: BTreeSet = v1_rt::map_get(&census.services, name.to_string()) + .map(|entries| entries.iter().map(|e| e.module_path.clone()).collect()) + .unwrap_or_default(); + match decide_service_owner_pull(&owners, imported_modules) { + Ok(ServiceOwnerPull::Pull(module_path)) => Ok(Some(module_path)), + Ok(ServiceOwnerPull::NoServiceOwner | ServiceOwnerPull::DecidedByImport) => Ok(None), + Err(all) => Err(format!( + "bare_reference_closure: service '{name}' in '{file_rel}' is AMBIGUOUS -- {} modules \ + declare a service of that name ({}) and '{file_rel}' imports none of them, and this \ + resolver does not rank candidates. Import the one you mean.", + all.len(), + all.join(", ") + )), + } +} + +// THE CANDIDATE POPULATION, supplied rather than compiled: v1.compiler.infer +// service_candidates_conflict is the one place the value-position decision is taken, so the +// structural claim -- two services and a value report all three owners, sorted, in whatever order +// they arrive -- is asserted on it directly. Its route through a real compile is +// test.claim.service_owner_identity_witness two_services_and_a_value_refuse_once_in_either_order. +#[cfg(test)] +mod service_candidates_conflict_tests { + use crate::v1_compiler_infer::{service_candidates_conflict, ServiceOwnerConflict}; + use std::rc::Rc; + + fn owners(xs: &[&str]) -> Rc> { + Rc::new(xs.iter().map(|x| x.to_string()).collect()) + } + + fn candidates(c: Option>) -> Option> { + c.map(|c| match c.as_ref() { + ServiceOwnerConflict::ServiceCandidatesAmbiguous { candidates } => { + candidates.iter().cloned().collect() + } + ServiceOwnerConflict::ServiceNotInScope { .. } => vec!["".to_string()], + }) + } + + const N: &str = "OwnerIdentityProbe"; + + #[test] + fn two_services_and_a_value_list_every_candidate_in_either_order() { + let want = Some(vec![ + "test.fixture.service_owner_identity.service.OwnerIdentityProbe".to_string(), + "test.fixture.service_owner_identity.service_b.OwnerIdentityProbe".to_string(), + "test.fixture.service_owner_identity.value.OwnerIdentityProbe".to_string(), + ]); + let a = "test.fixture.service_owner_identity.service"; + let b = "test.fixture.service_owner_identity.service_b"; + let v = "test.fixture.service_owner_identity.value"; + assert_eq!( + candidates(service_candidates_conflict( + N.to_string(), + owners(&[a, b]), + owners(&[v]) + )), + want + ); + assert_eq!( + candidates(service_candidates_conflict( + N.to_string(), + owners(&[b, a]), + owners(&[v]) + )), + want + ); + } + + #[test] + fn one_service_with_no_value_rival_is_no_conflict() { + assert_eq!( + candidates(service_candidates_conflict( + N.to_string(), + owners(&["m"]), + owners(&[]) + )), + None + ); + } +} + +#[cfg(test)] +mod service_owner_pull_tests { + use super::{decide_service_owner_pull, extract_import_paths, ServiceOwnerPull}; + use std::collections::BTreeSet; + + fn set(xs: &[&str]) -> BTreeSet { + xs.iter().map(|x| x.to_string()).collect() + } + + fn imports_of(source: &str) -> BTreeSet { + extract_import_paths(source).into_iter().collect() + } + + const A: &str = "test.fixture.service_owner_identity.service"; + const B: &str = "test.fixture.service_owner_identity.service_b"; + + // Each import FORM, read by the same reader the walk uses, reaches the same decision. + #[test] + fn either_owner_imported_alone_by_name_or_wildcard_is_decided_by_the_import() { + let owners = set(&[A, B]); + for src in [ + format!("module p\nimport {A} {{ OwnerIdentityProbe }}\n"), + format!("module p\nimport {A}\n"), + format!("module p\nimport {B} {{ OwnerIdentityProbe }}\n"), + format!("module p\nimport {B}\n"), + ] { + assert_eq!( + decide_service_owner_pull(&owners, &imports_of(&src)), + Ok(ServiceOwnerPull::DecidedByImport), + "{src}" + ); + } + } + + #[test] + fn both_owners_imported_in_either_order_or_form_pick_no_winner() { + // Inference refuses these with AmbiguousReference over both owners. + let owners = set(&[A, B]); + for src in [ + format!("module p\nimport {A} {{ OwnerIdentityProbe }}\nimport {B} {{ OwnerIdentityProbe }}\n"), + format!("module p\nimport {B}\nimport {A}\n"), + ] { + assert_eq!( + decide_service_owner_pull(&owners, &imports_of(&src)), + Ok(ServiceOwnerPull::DecidedByImport), + "{src}" + ); + } + } + + #[test] + fn an_unimported_plural_name_refuses_naming_every_owner_in_either_order() { + let want = Err(vec![A.to_string(), B.to_string()]); + assert_eq!(decide_service_owner_pull(&set(&[A, B]), &set(&[])), want); + assert_eq!( + decide_service_owner_pull(&set(&[B, A]), &set(&["unrelated.module"])), + want + ); + } + + #[test] + fn an_unimported_single_owner_is_pulled_and_an_unknown_name_is_not() { + assert_eq!( + decide_service_owner_pull(&set(&[A]), &set(&[])), + Ok(ServiceOwnerPull::Pull(A.to_string())) + ); + assert_eq!( + decide_service_owner_pull(&set(&[]), &set(&[A])), + Ok(ServiceOwnerPull::NoServiceOwner) + ); + } +} + pub fn closure_bare_disposition( referencing_module: &str, candidates: Rc>>, diff --git a/src/v1/stage0/src/cli_run/compile_clean.rs b/src/v1/stage0/src/cli_run/compile_clean.rs index eb791ac10a6..42a6b1438b8 100644 --- a/src/v1/stage0/src/cli_run/compile_clean.rs +++ b/src/v1/stage0/src/cli_run/compile_clean.rs @@ -1096,6 +1096,7 @@ pub fn compile_clean_diagnostic_class_specimen() -> Vec { ReferenceDerivedImportExportUnproven { name: s(), referencing_module: s(), provider_module: s(), span: no_span() }, UnlistedVariantValueUse { name: s(), span: no_span() }, AmbiguousReference { name: s(), candidates: istrings(), span: no_span() }, + ServiceOwnerMismatch { name: s(), service_owner: s(), bound_owner: s(), span: no_span() }, AmbiguousAnonymousRecordLiteral { candidates: istrings(), span: no_span() }, EffectfulSelfRecursionUnrealized { name: s(), span: no_span() }, ModuleFilenameCollision { filename: s(), modules: istrings(), span: no_span() }, @@ -1576,6 +1577,7 @@ pub fn compile_clean_diagnostic_histogram_key(d: &Rc) -> (String, Str } CompilerDiagnostic::UnlistedImportUse { .. } => "UnlistedImportUse", CompilerDiagnostic::AmbiguousReference { .. } => "AmbiguousReference", + CompilerDiagnostic::ServiceOwnerMismatch { .. } => "ServiceOwnerMismatch", CompilerDiagnostic::AmbiguousAnonymousRecordLiteral { .. } => { "AmbiguousAnonymousRecordLiteral" } @@ -1686,6 +1688,7 @@ pub fn compile_clean_diagnostic_histogram_key(d: &Rc) -> (String, Str CompilerDiagnostic::ReferenceDerivedImportProviderUnknown { name, .. } => name.clone(), CompilerDiagnostic::ReferenceDerivedImportExportUnproven { name, .. } => name.clone(), CompilerDiagnostic::AmbiguousReference { name, .. } => name.clone(), + CompilerDiagnostic::ServiceOwnerMismatch { name, .. } => name.clone(), CompilerDiagnostic::AmbiguousAnonymousRecordLiteral { candidates, .. } => { candidates.iter().cloned().collect::>().join("|") } diff --git a/src/v1/stage0/src/v1_compiler_emit.rs b/src/v1/stage0/src/v1_compiler_emit.rs index 2d5e4b453af..07e31426e57 100644 --- a/src/v1/stage0/src/v1_compiler_emit.rs +++ b/src/v1/stage0/src/v1_compiler_emit.rs @@ -53,6 +53,8 @@ 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::ServiceOwnerConflict; +use crate::v1_compiler_infer::ServiceOwnerConflict::*; pub use crate::v1_compiler_infer::{ build_params_scope, call_param_caller_labels, extend_scope, is_where_refinement_type, }; @@ -618,6 +620,7 @@ pub fn empty_emit_scope() -> Rc { match_bound_names: v1_rt::rc_empty_map::(), module_name: "".to_string(), service_registry: v1_rt::rc_empty_map::>>>(), + service_owner_conflicts: v1_rt::rc_empty_map::>(), item_registry: v1_rt::rc_empty_map::>(), lambda_param_provenance: v1_rt::rc_empty_map::>(), caller_decl_name: "".to_string(), @@ -638,6 +641,7 @@ pub fn module_emit_scope(typed_module: Rc) -> Rc { typed_module.module.clone(), ), service_registry: v1_rt::rc_empty_map::>>>(), + service_owner_conflicts: v1_rt::rc_empty_map::>(), item_registry: typed_module.item_registry.clone(), lambda_param_provenance: v1_rt::rc_empty_map::>(), caller_decl_name: "".to_string(), diff --git a/src/v1/stage0/src/v1_compiler_infer.rs b/src/v1/stage0/src/v1_compiler_infer.rs index e6f8876d5d8..dd0ef4b0d30 100644 --- a/src/v1/stage0/src/v1_compiler_infer.rs +++ b/src/v1/stage0/src/v1_compiler_infer.rs @@ -11,6 +11,7 @@ use self::InhabitanceVerdict::*; use self::LiteralBoundary::*; use self::RefinementInhabitance::*; use self::ServiceConfigFieldJudgment::*; +use self::ServiceOwnerConflict::*; pub use crate::extdeps_container_oci_digest::{ oci_other_digest_algorithm, oci_other_digest_encoded, }; @@ -241,7 +242,7 @@ use crate::v1_std_core::CompilerDiagnostic::{ EqualityMemberUnjudgeable, EqualityOnFunctionMember, FieldNotFound, FrontierOccurrenceBudgetExceeded, InternalError, MethodExistenceFrontierAdmitted, MethodExistenceUndecided, MethodNotFound, MissingField, OptionalCastNotEliminated, - ReceiverTypeUnestablished, ServiceConfigReferenceJudgmentDeferred, + ReceiverTypeUnestablished, ServiceConfigReferenceJudgmentDeferred, ServiceOwnerMismatch, SiblingOperandEffectOrderUndetermined, SoleConstructorViolation, TypeArgumentArityMismatch, TypeMismatch, TypeParameterInValuePosition, UnlistedVariantValueUse, UnresolvedType, VariantCollision, @@ -337,6 +338,7 @@ pub struct ModuleContext { pub resolved_items: Rc>>, pub func_env: Rc, pub svc_registry: Rc>>>>, + pub svc_owner_conflicts: Rc>>, pub locals: Rc>>, pub variant_locals: Rc>>, pub item_registry: Rc>>, @@ -370,6 +372,7 @@ pub struct InferScope { pub match_bound_names: Rc>, pub module_name: String, pub service_registry: Rc>>>>, + pub service_owner_conflicts: Rc>>, pub item_registry: Rc>>, pub lambda_param_provenance: Rc>>, pub caller_decl_name: String, @@ -569,7 +572,7 @@ pub fn merge_scope_from_imports( let parent_result = typed_parent.items.clone().iter().cloned().fold(Rc::new(InferScopeComponents { svc_registry: svc_registry.clone(), svc_locals: svc_locals.clone(), -}), |acc: Rc, titem: Rc| if ((titem.transport.clone() != std::option::Option::None) && ((titem.children.clone().len() as i64) > 0)) { +}), |acc: Rc, titem: Rc| if (((titem.transport.clone() != std::option::Option::None) && ((titem.children.clone().len() as i64) > 0)) && import_path_decides_service(crate::v1_std_core::authored_name_at(env.source_indices.clone(), titem.clone()), namespace_root_from_properties(titem.properties.clone(), crate::v1_std_core::authored_name_at(env.source_indices.clone(), titem.clone()), env.source_indices.clone()), env.symbol_index.clone().services.clone())) { { let entries = Rc::new({ let mut __result = Vec::new(); for c in titem.children.clone().iter().cloned() { __result.push(Rc::new(OpEntry { name: crate::v1_std_core::authored_name_at(env.source_indices.clone(), c.clone()), @@ -1379,6 +1382,449 @@ pub fn local_is_service_root_binding( } } +pub fn import_names(imp: Rc, name: String) -> bool { + (imp.is_all.clone() || { + let mut __found = false; + for n in imp.specific_names.clone().iter().cloned() { + if (n.clone() == name.clone()) { + __found = true; + break; + } + } + __found + }) +} + +pub fn import_path_decides_service( + name: String, + root: String, + census: Rc>>>>, +) -> bool { + ((root.clone() != name.clone()) + && ((crate::v1_compiler_infer_env::service_census_candidates(census.clone(), name.clone()) + .len() as i64) + <= 1)) +} + +pub fn census_service_in_scope( + owner_module: String, + name: String, + root: String, + module_name: String, + resolved_imports: Rc>>, +) -> bool { + (((root.clone() != name.clone()) || (owner_module.clone() == module_name.clone())) || { + let mut __found = false; + for imp in resolved_imports.iter().cloned() { + if ((imp.module_path.clone() == owner_module.clone()) + && import_names(imp.clone(), name.clone())) + { + __found = true; + break; + } + } + __found + }) +} + +#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(tag = "_variant")] +pub enum ServiceOwnerConflict { + ServiceNotInScope { + service_owners: Rc>, + bound_owners: Rc>, + }, + ServiceCandidatesAmbiguous { + candidates: Rc>, + }, +} + +pub fn service_scope_candidates( + entries: Rc>>, + name: String, + module_name: String, + resolved_imports: Rc>>, + source_indices: Rc>>, +) -> Rc>> { + Rc::new({ + let mut __result = Vec::new(); + for e in entries.iter().cloned() { + if census_service_in_scope( + e.module_path.clone(), + name.clone(), + namespace_root_from_properties( + e.item.clone().properties.clone(), + name.clone(), + source_indices.clone(), + ), + module_name.clone(), + resolved_imports.clone(), + ) { + __result.push(e); + } + } + __result + }) +} + +pub fn non_service_import_owners( + name: String, + service_owners: Rc>, + resolved_imports: Rc>>, + parent_index: Rc>>, + source_indices: Rc>>, +) -> Rc> { + sorted_distinct_names(Rc::new({ + let mut __result = Vec::new(); + for imp in Rc::new({ + let mut __result = Vec::new(); + for imp in Rc::new({ + let mut __result = Vec::new(); + for imp in resolved_imports.iter().cloned() { + if !{ + let mut __found = false; + for o in service_owners.iter().cloned() { + if (o.clone() == imp.module_path.clone()) { + __found = true; + break; + } + } + __found + } { + __result.push(imp); + } + } + __result + }) + .iter() + .cloned() + { + if ({ + let mut __found = false; + for n in imp.specific_names.clone().iter().cloned() { + if (n.clone() == name.clone()) { + __found = true; + break; + } + } + __found + } || (imp.is_all.clone() + && module_declares_named( + parent_index.clone(), + imp.module_path.clone(), + name.clone(), + source_indices.clone(), + ))) + { + __result.push(imp); + } + } + __result + }) + .iter() + .cloned() + { + __result.push(imp.module_path.clone()); + } + __result + })) +} + +pub fn sorted_distinct_names(names: Rc>) -> Rc> { + { + let members = names.iter().cloned().fold( + v1_rt::rc_empty_map::(), + |acc: Rc>, n: String| v1_rt::rc_map_insert(acc, n.clone(), true), + ); + Rc::new(v1_rt::sorted_map_keys(&members)) + } +} + +pub fn service_owner_conflicts_of( + services: Rc>>>>, + resolved_imports: Rc>>, + parent_index: Rc>>, + module_name: String, + source_indices: Rc>>, +) -> Rc>> { + Rc::new(v1_rt::map_keys(&services)).iter().cloned().fold( + v1_rt::rc_empty_map::>(), + |acc: Rc>>, sname: String| match v1_rt::map_get( + &services, + sname.clone(), + ) { + std::option::Option::None => acc.clone(), + Some(entries) => { + let scope_owners = sorted_distinct_names(Rc::new({ + let mut __result = Vec::new(); + for e in service_scope_candidates( + entries.clone(), + sname.clone(), + module_name.clone(), + resolved_imports.clone(), + source_indices.clone(), + ) + .iter() + .cloned() + { + __result.push(e.module_path.clone()); + } + __result + })); + let all_owners = sorted_distinct_names(Rc::new({ + let mut __result = Vec::new(); + for e in entries.iter().cloned() { + __result.push(e.module_path.clone()); + } + __result + })); + let others = non_service_import_owners( + sname.clone(), + all_owners.clone(), + resolved_imports.clone(), + parent_index.clone(), + source_indices.clone(), + ); + let value_owners = Rc::new({ + let mut __result = Vec::new(); + for o in others.iter().cloned() { + if module_declares_value_named( + parent_index.clone(), + o.clone(), + sname.clone(), + source_indices.clone(), + ) { + __result.push(o); + } + } + __result + }); + match service_candidates_conflict( + sname.clone(), + scope_owners.clone(), + value_owners.clone(), + ) { + Some(conflict) => { + v1_rt::rc_map_insert(acc.clone(), sname.clone(), conflict.clone()) + } + std::option::Option::None => { + if (((scope_owners.clone().len() as i64) == 0) + && ((others.clone().len() as i64) > 0)) + { + v1_rt::rc_map_insert( + acc.clone(), + sname.clone(), + Rc::new(ServiceOwnerConflict::ServiceNotInScope { + service_owners: all_owners.clone(), + bound_owners: others.clone(), + }), + ) + } else { + acc.clone() + } + } + } + } + }, + ) +} + +pub fn service_candidates_conflict( + name: String, + service_owners: Rc>, + value_owners: Rc>, +) -> Option> { + { + let services = (service_owners.clone().len() as i64); + if ((services.clone() >= 2) + || ((services.clone() == 1) && ((value_owners.clone().len() as i64) >= 1))) + { + Some(Rc::new(ServiceOwnerConflict::ServiceCandidatesAmbiguous { + candidates: Rc::new({ + let mut __result = Vec::new(); + for o in sorted_distinct_names(v1_rt::concat( + service_owners.clone(), + value_owners.clone(), + )) + .iter() + .cloned() + { + __result.push(v1_rt::concat( + v1_rt::concat(o.clone(), ".".to_string()), + name.clone(), + )); + } + __result + }), + })) + } else { + std::option::Option::None + } + } +} + +pub fn without_conflicted_names( + registry: Rc>>>>, + conflicts: Rc>>, +) -> Rc>>>> { + Rc::new(v1_rt::sorted_map_keys(®istry)) + .iter() + .cloned() + .fold( + v1_rt::rc_empty_map::>>>(), + |acc: Rc>>>>, k: String| { + if v1_rt::map_has(&conflicts, k.clone()) { + acc.clone() + } else { + match v1_rt::map_get(®istry, k.clone()) { + Some(v) => v1_rt::rc_map_insert(acc.clone(), k.clone(), v.clone()), + std::option::Option::None => acc.clone(), + } + } + }, + ) +} + +pub fn without_conflicted_bindings( + bindings: Rc>>, + conflicts: Rc>>, +) -> Rc>> { + Rc::new(v1_rt::sorted_map_keys(&bindings)) + .iter() + .cloned() + .fold( + v1_rt::rc_empty_map::>(), + |acc: Rc>>, k: String| { + if v1_rt::map_has(&conflicts, k.clone()) { + acc.clone() + } else { + match v1_rt::map_get(&bindings, k.clone()) { + Some(v) => v1_rt::rc_map_insert(acc.clone(), k.clone(), v.clone()), + std::option::Option::None => acc.clone(), + } + } + }, + ) +} + +pub fn module_declares_named( + parent_index: Rc>>, + module_path: String, + name: String, + source_indices: Rc>>, +) -> bool { + match v1_rt::map_get(&parent_index, module_path.clone()) { + Some(typed_parent) => { + let mut __found = false; + for titem in typed_parent.items.clone().iter().cloned() { + if (crate::v1_std_core::authored_name_at(source_indices.clone(), titem.clone()) + == name.clone()) + { + __found = true; + break; + } + } + __found + } + std::option::Option::None => false, + } +} + +pub fn module_declares_value_named( + parent_index: Rc>>, + module_path: String, + name: String, + source_indices: Rc>>, +) -> bool { + match v1_rt::map_get(&parent_index, module_path.clone()) { + Some(typed_parent) => { + let mut __found = false; + for titem in typed_parent.items.clone().iter().cloned() { + if ((crate::v1_std_core::authored_name_at(source_indices.clone(), titem.clone()) + == name.clone()) + && match titem.module_item_kind.clone() { + ParsedModuleItemKind::ModuleItemFunction => true, + ParsedModuleItemKind::ModuleItemDataValue => true, + _ => false, + }) + { + __found = true; + break; + } + } + __found + } + std::option::Option::None => false, + } +} + +pub fn service_owner_mismatch_refusal( + name: String, + span: Rc, + scope: Rc, +) -> Option> { + match v1_rt::map_get(&scope.service_owner_conflicts.clone(), name.clone()) { + std::option::Option::None => std::option::Option::None, + Some(conflict) => { + if v1_rt::map_has(&scope.body_locals.clone(), name.clone()) { + std::option::Option::None + } else { + match (*conflict.clone()).clone() { + ServiceOwnerConflict::ServiceCandidatesAmbiguous { candidates: cs, .. } => { + Some(ambiguous_reference_refusal( + name.clone(), + cs.clone(), + span.clone(), + scope.clone(), + )) + } + ServiceOwnerConflict::ServiceNotInScope { + service_owners: sos, + bound_owners: bos, + .. + } => { + let reaches_join = match v1_rt::map_get(&scope.locals.clone(), name.clone()) + { + std::option::Option::None => true, + Some(binding) => { + (binding.resolved.clone() + == crate::v1_compiler_infer_types::nominal_type_ref( + name.clone(), + )) + } + }; + if !reaches_join.clone() { + std::option::Option::None + } else { + Some(Rc::new(InferResult { + typed: semantic_expr_error_node( + v1_rt::concat( + v1_rt::concat( + "service owner mismatch for '".to_string(), + name.clone(), + ), + "'".to_string(), + ), + span.clone(), + ), + diagnostics: Rc::new(vec![crate::v1_std_core::make_error_node( + Rc::new(CompilerDiagnostic::ServiceOwnerMismatch { + name: name.clone(), + service_owner: sos.clone().join(&" | ".to_string()), + bound_owner: bos.clone().join(&" | ".to_string()), + span: span.clone(), + }), + scope.module_name.clone(), + )]), + })) + } + } + } + } + } + } +} + pub fn relocated_algebra_evidence_diags( diags: Rc>>, span: Rc, @@ -9426,6 +9872,7 @@ pub fn build_params_scope(scope: Rc, params: Rc>>) -> R match_bound_names: scope.match_bound_names.clone(), module_name: scope.module_name.clone(), service_registry: scope.service_registry.clone(), + service_owner_conflicts: scope.service_owner_conflicts.clone(), item_registry: scope.item_registry.clone(), caller_decl_name: scope.caller_decl_name.clone(), lambda_param_provenance: v1_rt::rc_empty_map::>(), @@ -9476,6 +9923,7 @@ pub fn extend_scope( match_bound_names: scope.match_bound_names.clone(), module_name: scope.module_name.clone(), service_registry: scope.service_registry.clone(), + service_owner_conflicts: scope.service_owner_conflicts.clone(), item_registry: scope.item_registry.clone(), caller_decl_name: scope.caller_decl_name.clone(), lambda_param_provenance: scope.lambda_param_provenance.clone(), @@ -9510,6 +9958,7 @@ pub fn extend_scope_match_bound( ), module_name: scope.module_name.clone(), service_registry: scope.service_registry.clone(), + service_owner_conflicts: scope.service_owner_conflicts.clone(), item_registry: scope.item_registry.clone(), caller_decl_name: scope.caller_decl_name.clone(), lambda_param_provenance: scope.lambda_param_provenance.clone(), @@ -9546,6 +9995,7 @@ pub fn extend_scope_with_params(scope: Rc, params: Rc>) match_bound_names: scope.match_bound_names.clone(), module_name: scope.module_name.clone(), service_registry: scope.service_registry.clone(), + service_owner_conflicts: scope.service_owner_conflicts.clone(), item_registry: scope.item_registry.clone(), caller_decl_name: scope.caller_decl_name.clone(), lambda_param_provenance: scope.lambda_param_provenance.clone(), @@ -9971,6 +10421,7 @@ let fold_scope = Rc::new(InferScope { match_bound_names: scope.match_bound_names.clone(), module_name: scope.module_name.clone(), service_registry: scope.service_registry.clone(), + service_owner_conflicts: scope.service_owner_conflicts.clone(), item_registry: scope.item_registry.clone(), caller_decl_name: scope.caller_decl_name.clone(), lambda_param_provenance: prov_map.clone(), @@ -9999,6 +10450,7 @@ let nf_scope = if is_lambda_expr(nf_lam_value.clone()) { match_bound_names: scope.match_bound_names.clone(), module_name: scope.module_name.clone(), service_registry: scope.service_registry.clone(), + service_owner_conflicts: scope.service_owner_conflicts.clone(), item_registry: scope.item_registry.clone(), caller_decl_name: scope.caller_decl_name.clone(), lambda_param_provenance: nf_prov_map.clone(), @@ -10449,28 +10901,71 @@ pub fn qualified_or_service_projection( ) -> Option> { match qualified_value_projection(texpr.clone(), scope.clone(), span.clone(), expected.clone()) { Some(proj) => Some(proj.clone()), - std::option::Option::None => match service_spine_projection(texpr.clone(), scope.clone()) { - std::option::Option::None => std::option::Option::None, - Some(svc_type) => { - let svc_name = crate::v1_std_core::authored_name_at( - scope.type_env.clone().source_indices.clone(), - svc_type.clone(), - ); - Some(ok_infer(crate::v1_std_core::make_named_expr_node( - texpr.occurrence_identity.clone(), - svc_name.clone(), - Rc::new(ExprData::ExprVar { - binding_kind: Some(Rc::new(VarBindingKind::ServiceValueBinding)), - }), - Rc::new(vec![]), - Some(Rc::new(InferredNode::Resolved { - node: svc_type.clone(), - })), - span.clone(), - kernel_span(svc_name.clone()), - ))) + std::option::Option::None => { + match dotted_service_ambiguity_refusal(texpr.clone(), scope.clone(), span.clone()) { + Some(refusal) => Some(refusal.clone()), + std::option::Option::None => { + match service_spine_projection(texpr.clone(), scope.clone()) { + std::option::Option::None => std::option::Option::None, + Some(svc_type) => { + let svc_name = crate::v1_std_core::authored_name_at( + scope.type_env.clone().source_indices.clone(), + svc_type.clone(), + ); + Some(ok_infer(crate::v1_std_core::make_named_expr_node( + texpr.occurrence_identity.clone(), + svc_name.clone(), + Rc::new(ExprData::ExprVar { + binding_kind: Some(Rc::new( + VarBindingKind::ServiceValueBinding, + )), + }), + Rc::new(vec![]), + Some(Rc::new(InferredNode::Resolved { + node: svc_type.clone(), + })), + span.clone(), + kernel_span(svc_name.clone()), + ))) + } + } + } } - }, + } + } +} + +pub fn dotted_service_ambiguity_refusal( + texpr: Rc, + scope: Rc, + span: Rc, +) -> Option> { + match crate::v1_std_core::field_access_spine( + texpr.clone(), + scope.type_env.clone().source_indices.clone(), + ) { + std::option::Option::None => std::option::Option::None, + Some(spine) => { + if spine_root_is_shadowed(scope.clone(), spine.root.clone()) { + std::option::Option::None + } else { + match v1_rt::map_get(&scope.service_owner_conflicts.clone(), spine.dotted.clone()) + .as_deref() + .cloned() + { + Some(ServiceOwnerConflict::ServiceCandidatesAmbiguous { + candidates: cs, + .. + }) => Some(ambiguous_reference_refusal( + spine.dotted.clone(), + cs.clone(), + span.clone(), + scope.clone(), + )), + _ => std::option::Option::None, + } + } + } } } @@ -10923,6 +11418,8 @@ pub fn infer_expr_body( let span = texpr.span.clone(); match constructor_reference_admission_unshadowed(name.clone(), span.clone(), scope.clone()) { Some(refusal) => refusal.clone(), + std::option::Option::None => match service_owner_mismatch_refusal(name.clone(), span.clone(), scope.clone()) { + Some(refusal) => refusal.clone(), std::option::Option::None => match v1_rt::map_get(&scope.locals.clone(), name.clone()) { Some(binding) => { let scope_parent = lookup_variant_parent_enum(scope.clone(), name.clone()); @@ -11067,6 +11564,7 @@ Rc::new(InferResult { }, }, }, +}, } } ExprData::ExprFieldAccess { summary: _, .. } => { @@ -13564,6 +14062,7 @@ crate::v1_compiler_infer_types::resolve_type_variables_from_template(t.clone(), match_bound_names: lam_scope.match_bound_names.clone(), module_name: lam_scope.module_name.clone(), service_registry: lam_scope.service_registry.clone(), + service_owner_conflicts: lam_scope.service_owner_conflicts.clone(), item_registry: lam_scope.item_registry.clone(), caller_decl_name: lam_scope.caller_decl_name.clone(), lambda_param_provenance: v1_rt::rc_empty_map::>(), @@ -21002,6 +21501,7 @@ pub fn infer_item(item: Rc, scope: Rc) -> Rc match_bound_names: fn_scope.match_bound_names.clone(), module_name: fn_scope.module_name.clone(), service_registry: fn_scope.service_registry.clone(), + service_owner_conflicts: fn_scope.service_owner_conflicts.clone(), item_registry: fn_scope.item_registry.clone(), caller_decl_name: fn_decl_name.clone(), lambda_param_provenance: fn_scope.lambda_param_provenance.clone(), @@ -23852,31 +24352,32 @@ pub fn census_with_resolved_fn_sigs( let service_keys = Rc::new(v1_rt::sorted_map_keys(&index.services.clone())); let services2 = service_keys.iter().cloned().fold( index.services.clone(), - |acc: Rc>>, k: String| match v1_rt::map_get( - &index.services.clone(), - k.clone(), - ) { - Some(sentry) => { - let upgraded = census_upgrade_service_item( - sentry.item.clone(), - sentry.module_path.clone(), - index.clone(), - source_indices.clone(), - ); - if (upgraded.clone() == sentry.item.clone()) { - acc.clone() - } else { - v1_rt::rc_map_insert( - acc.clone(), - k.clone(), - Rc::new(ServiceCensusEntry { - module_path: sentry.module_path.clone(), - item: upgraded.clone(), - }), - ) + |acc: Rc>>>>, k: String| { + match v1_rt::map_get(&index.services.clone(), k.clone()) { + Some(sentries) => { + let upgraded = Rc::new({ + let mut __result = Vec::new(); + for sentry in sentries.iter().cloned() { + __result.push(Rc::new(ServiceCensusEntry { + module_path: sentry.module_path.clone(), + item: census_upgrade_service_item( + sentry.item.clone(), + sentry.module_path.clone(), + index.clone(), + source_indices.clone(), + ), + })); + } + __result + }); + if (upgraded.clone() == sentries.clone()) { + acc.clone() + } else { + v1_rt::rc_map_insert(acc.clone(), k.clone(), upgraded.clone()) + } } + std::option::Option::None => acc.clone(), } - std::option::Option::None => acc.clone(), }, ); Rc::new(SymbolIndex { @@ -26476,73 +26977,51 @@ pub fn build_module_context( let variant_collision_errors = variant_fold.collision_errors.clone(); let env_variant_locals = merge_kernel_variant_locals_low_priority(env.clone(), variant_fold.locals.clone()); - let census_scope = Rc::new(v1_rt::map_keys(&env.symbol_index.clone().services.clone())) - .iter() - .cloned() - .fold( - Rc::new(InferScopeComponents { - svc_registry: v1_rt::rc_empty_map::>>>(), - svc_locals: v1_rt::rc_empty_map::>(), - }), - |acc: Rc, sname: String| match v1_rt::map_get( - &env.symbol_index.clone().services.clone(), - sname.clone(), - ) { - Some(sentry) => { - let sitem = sentry.item.clone(); - let entries = Rc::new({ - let mut __result = Vec::new(); - for c in sitem.children.clone().iter().cloned() { - __result.push(Rc::new(OpEntry { - name: crate::v1_std_core::authored_name_at( - env.source_indices.clone(), - c.clone(), - ), - outputs: crate::v1_compiler_infer_items::inferred_to_outputs( - crate::v1_compiler_infer_env::qualify_borrowed_inferred( - c.inferred.clone(), - sentry.module_path.clone(), - env.clone(), - v1_rt::rc_empty_map::(), - ), - c.span.clone(), - env.source_indices.clone(), - ), - params: c.params.clone(), - })); - } - __result - }); - let root = namespace_root_from_properties( - sitem.properties.clone(), - sname.clone(), - env.source_indices.clone(), - ); - Rc::new(InferScopeComponents { - svc_registry: v1_rt::rc_map_insert( - acc.svc_registry.clone(), - sname.clone(), - entries.clone(), - ), - svc_locals: v1_rt::rc_map_insert( - acc.svc_locals.clone(), - root.clone(), - nominal_type_binding(root.clone()), - ), - }) - } - std::option::Option::None => acc.clone(), - }, - ); + let service_conflicts = service_owner_conflicts_of( + env.symbol_index.clone().services.clone(), + resolved_imports.clone(), + parent_index.clone(), + module_name.clone(), + env.source_indices.clone(), + ); + let census_scope = Rc::new(v1_rt::map_keys(&env.symbol_index.clone().services.clone())).iter().cloned().fold(Rc::new(InferScopeComponents { + svc_registry: v1_rt::rc_empty_map::>>>(), + svc_locals: v1_rt::rc_empty_map::>(), +}), |acc: Rc, sname: String| match v1_rt::map_get(&env.symbol_index.clone().services.clone(), sname.clone()) { + Some(sentries) => { + let candidates = service_scope_candidates(sentries.clone(), sname.clone(), module_name.clone(), resolved_imports.clone(), env.source_indices.clone()); +if (((candidates.clone().len() as i64) != 1) || v1_rt::map_has(&service_conflicts, sname.clone())) { + acc.clone() + } else { + candidates.iter().cloned().fold(acc.clone(), |acc: Rc, sentry: Rc| { let acc = v1_rt::take_owned(acc); { + let sitem = sentry.item.clone(); +let entries = Rc::new({ let mut __result = Vec::new(); for c in sitem.children.clone().iter().cloned() { __result.push(Rc::new(OpEntry { + name: crate::v1_std_core::authored_name_at(env.source_indices.clone(), c.clone()), + outputs: crate::v1_compiler_infer_items::inferred_to_outputs(crate::v1_compiler_infer_env::qualify_borrowed_inferred(c.inferred.clone(), sentry.module_path.clone(), env.clone(), v1_rt::rc_empty_map::()), c.span.clone(), env.source_indices.clone()), + params: c.params.clone(), +})); } __result }); +let root = namespace_root_from_properties(sitem.properties.clone(), sname.clone(), env.source_indices.clone()); +Rc::new(InferScopeComponents { + svc_registry: v1_rt::rc_map_insert(acc.svc_registry, sname.clone(), entries.clone()), + svc_locals: v1_rt::rc_map_insert(acc.svc_locals, root.clone(), nominal_type_binding(root.clone())), +}) +} }) + } +}, + std::option::Option::None => acc.clone(), +}); let merged_scope = merge_scope_from_imports( resolved_imports.clone(), parent_index.clone(), env.clone(), v1_rt::rc_map_merge( census_scope.svc_registry.clone(), - local.svc_registry.clone(), + without_conflicted_names(local.svc_registry.clone(), service_conflicts.clone()), + ), + v1_rt::rc_map_merge( + census_scope.svc_locals.clone(), + without_conflicted_bindings(local.svc_locals.clone(), service_conflicts.clone()), ), - v1_rt::rc_map_merge(census_scope.svc_locals.clone(), local.svc_locals.clone()), ); let parent_envs = merge_func_env_views_by_owner(Rc::new({ let mut __result = Vec::new(); @@ -26586,6 +27065,7 @@ pub fn build_module_context( module_name.clone(), ), svc_registry: merged_scope.svc_registry.clone(), + svc_owner_conflicts: service_conflicts.clone(), locals: all_locals.clone(), variant_locals: env_variant_locals.clone(), item_registry: local.item_registry.clone(), @@ -26734,6 +27214,7 @@ pub fn typecheck_module( match_bound_names: v1_rt::rc_empty_map::(), module_name: resolved_module_name.clone(), service_registry: ctx.svc_registry.clone(), + service_owner_conflicts: ctx.svc_owner_conflicts.clone(), item_registry: ctx.item_registry.clone(), caller_decl_name: "".to_string(), lambda_param_provenance: v1_rt::rc_empty_map::>(), diff --git a/src/v1/stage0/src/v1_compiler_infer_env.rs b/src/v1/stage0/src/v1_compiler_infer_env.rs index 6d4071edaac..c9f6b7894b8 100644 --- a/src/v1/stage0/src/v1_compiler_infer_env.rs +++ b/src/v1/stage0/src/v1_compiler_infer_env.rs @@ -339,7 +339,7 @@ pub struct ServiceCensusEntry { pub struct SymbolIndex { pub entries: Rc>>, pub global_bare: Rc>>, - pub services: Rc>>, + pub services: Rc>>>>, pub transparent_alias_rep: Rc>, pub type_head_exposures: Rc>>, } @@ -353,7 +353,7 @@ pub fn empty_symbol_index() -> Rc { Rc::new(SymbolIndex { entries: v1_rt::rc_empty_map::>(), global_bare: v1_rt::rc_empty_map::>(), - services: v1_rt::rc_empty_map::>(), + services: v1_rt::rc_empty_map::>>>(), transparent_alias_rep: v1_rt::rc_empty_map::(), type_head_exposures: v1_rt::rc_empty_map::>(), }) @@ -512,6 +512,16 @@ pub fn symbol_index_insert_decl( }) } +pub fn service_census_candidates( + services: Rc>>>>, + name: String, +) -> Rc>> { + match v1_rt::map_get(&services, name.clone()) { + Some(entries) => entries.clone(), + std::option::Option::None => Rc::new(vec![]), + } +} + pub fn symbol_index_insert_service( index: Rc, name: String, @@ -524,10 +534,13 @@ pub fn symbol_index_insert_service( services: v1_rt::rc_map_insert( index.services.clone(), name.clone(), - Rc::new(ServiceCensusEntry { - module_path: module_path.clone(), - item: item.clone(), - }), + v1_rt::rc_list_push( + service_census_candidates(index.services.clone(), name.clone()), + Rc::new(ServiceCensusEntry { + module_path: module_path.clone(), + item: item.clone(), + }), + ), ), transparent_alias_rep: index.transparent_alias_rep.clone(), type_head_exposures: index.type_head_exposures.clone(), diff --git a/src/v1/stage0/src/v1_std_core.rs b/src/v1/stage0/src/v1_std_core.rs index 17e83e5b883..3b0a5a3ecf8 100644 --- a/src/v1/stage0/src/v1_std_core.rs +++ b/src/v1/stage0/src/v1_std_core.rs @@ -787,6 +787,12 @@ pub enum CompilerDiagnostic { candidates: Rc>, span: Rc, }, + ServiceOwnerMismatch { + name: String, + service_owner: String, + bound_owner: String, + span: Rc, + }, AmbiguousAnonymousRecordLiteral { candidates: Rc>, span: Rc, @@ -1036,6 +1042,7 @@ pub fn diagnostic_to_span(d: Rc) -> Rc { CompilerDiagnostic::ReferenceDerivedImportExportUnproven { span: s, .. } => s.clone(), CompilerDiagnostic::UnlistedVariantValueUse { span: s, .. } => s.clone(), CompilerDiagnostic::AmbiguousReference { span: s, .. } => s.clone(), + CompilerDiagnostic::ServiceOwnerMismatch { span: s, .. } => s.clone(), CompilerDiagnostic::AmbiguousAnonymousRecordLiteral { span: s, .. } => s.clone(), CompilerDiagnostic::EffectfulSelfRecursionUnrealized { span: s, .. } => s.clone(), CompilerDiagnostic::ModuleFilenameCollision { span: s, .. } => s.clone(), @@ -1112,6 +1119,7 @@ pub fn diagnostic_to_message(d: Rc) -> String { CompilerDiagnostic::ReferenceDerivedImportExportUnproven { name: n, referencing_module: rm, provider_module: p, .. } => v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("the provider of '".to_string(), n.clone()), "' is present but its export could not be established: module '".to_string()), rm.clone()), "' references the name, the registry maps it to '".to_string()), p.clone()), "', and that module's transitive export surface does not establish it. The remedy is NOT an import -- the provider is already in the closure -- it is that the emitter cannot prove an export the provider may well hold".to_string()), CompilerDiagnostic::UnlistedVariantValueUse { name: n, .. } => v1_rt::concat(v1_rt::concat("unlisted variant value use '".to_string(), n.clone()), "' (a coproduct arm referenced but not in any import's name list)".to_string()), CompilerDiagnostic::AmbiguousReference { name: n, candidates: cs, .. } => v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("ambiguous reference '".to_string(), n.clone()), "': ".to_string()), ((cs.clone().len() as i64)).to_string()), " candidates: ".to_string()), cs.clone().join(&", ".to_string())), " — qualify by containment path, alias, or rename".to_string()), + CompilerDiagnostic::ServiceOwnerMismatch { name: n, service_owner: so, bound_owner: bo, .. } => 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("receiver '".to_string(), n.clone()), "' is bound to '".to_string()), bo.clone()), ".".to_string()), n.clone()), "', not to the service '".to_string()), so.clone()), ".".to_string()), n.clone()), "': a service's operations are reached only through the declaration the module imports, never through a service that merely shares the name. Import the service from '".to_string()), so.clone()), "' if its operations are meant, or call the operations '".to_string()), bo.clone()), ".".to_string()), n.clone()), "' declares".to_string()), CompilerDiagnostic::AmbiguousAnonymousRecordLiteral { candidates: cs, .. } => v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("ambiguous anonymous record literal shape matches ".to_string(), ((cs.clone().len() as i64)).to_string()), " structs: ".to_string()), cs.clone().join(&", ".to_string())), " — add a nominal type".to_string()), CompilerDiagnostic::EffectfulSelfRecursionUnrealized { name: n, .. } => v1_rt::concat(v1_rt::concat("effectful declaration '".to_string(), n.clone()), "' calls itself: the Rust realization renders an effectful declaration as `async fn`, and rustc refuses recursion in an async fn without boxing (E0733), which no emitter performs. Realize the recursion as a loop, or move the self-call into a pure helper.".to_string()), CompilerDiagnostic::ModuleFilenameCollision { filename: f, modules: ms, .. } => v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("module filename collision: ".to_string(), ((ms.clone().len() as i64)).to_string()), " modules render one emitted file name '".to_string()), f.clone()), "': ".to_string()), ms.clone().join(&", ".to_string())), " — module_to_filename maps '.' to '_', so these names are indistinguishable at the emitted path; rename one module segment".to_string()), @@ -1363,6 +1371,10 @@ pub fn diagnostic_disposition(d: Rc) -> Rc Rc::new(DiagnosticDisposition { severity: DiagnosticSeverity::SeverityError, gate: Rc::new(DiagnosticGateDisposition::GateBlocking), +}), + CompilerDiagnostic::ServiceOwnerMismatch { .. } => Rc::new(DiagnosticDisposition { + severity: DiagnosticSeverity::SeverityError, + gate: Rc::new(DiagnosticGateDisposition::GateBlocking), }), CompilerDiagnostic::AmbiguousAnonymousRecordLiteral { .. } => Rc::new(DiagnosticDisposition { severity: DiagnosticSeverity::SeverityError, diff --git a/src/v2/test/claim/complexity/accumulator_copy_unreadable_subject_test.dag b/src/v2/test/claim/complexity/accumulator_copy_unreadable_subject_test.dag index 137eba95996..20d83404c9d 100644 --- a/src/v2/test/claim/complexity/accumulator_copy_unreadable_subject_test.dag +++ b/src/v2/test/claim/complexity/accumulator_copy_unreadable_subject_test.dag @@ -3,7 +3,8 @@ module v2.test.claim.complexity.accumulator_copy_unreadable_subject import extdeps.filesystem.filesystem_io { FilesystemReadRefused, FilesystemReadSucceeded, - filesystem_read_outcome + filesystem_read_outcome, + Filesystem } import v2.lens.common.source_analysis_standing { SourceUnreadable } import v2.lens.complexity_accumulator_copy.roster_gate { diff --git a/src/v2/workflow/floor_unimported_bare_provider_debt_roster.dag b/src/v2/workflow/floor_unimported_bare_provider_debt_roster.dag index 79b3690cdc6..7a06f5b27ab 100644 --- a/src/v2/workflow/floor_unimported_bare_provider_debt_roster.dag +++ b/src/v2/workflow/floor_unimported_bare_provider_debt_roster.dag @@ -1184,8 +1184,8 @@ data unimported_bare_provider_dispositions: List