Skip to content
Merged
181 changes: 181 additions & 0 deletions dag/test/claim/phantom_marker_type_argument_identity_witness_test.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
module test.claim.phantom_marker_type_argument_identity_witness

import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly }
import std.dissolution { DissolutionCondition, unbound_dissolution }
import std.types { NonEmptyStr }

data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly

// Distinct zero-payload variants used as type arguments are distinct declared
// identities, even though neither carries a runtime value. This fixture drives
// the real Rust emitter and requires both identities to remain writable in the
// projection. The pre-fix emitter rendered both arguments as `()`, so the two
// includes were absent and this witness was red. The excludes keep a future
// position-dependent collapse from satisfying the positive strings beside an
// erased spelling.

fn w_distinct_phantom_markers_remain_distinct() -> Bool {
compile_dag_rust_emit_check(
"module phantom_marker_identity\n\ntype Quantity = Time | Memory\n\ntype Measure<Q> {\n count: Int\n}\n\ntype DistinctMeasures {\n time: Measure<Time>\n memory: Measure<Memory>\n}\n",
"src/phantom_marker_identity.rs",
["Measure<Time>", "Measure<Memory>"],
["Measure<()>"]
)
}

test fn distinct_phantom_markers_do_not_render_identically() -> Bool {
w_distinct_phantom_markers_remain_distinct()
}

// The same applied-argument decision must survive every outer type renderer.
// These five uniquely named declarations exercise alias RHS, record field,
// parameter, return, and top-level data positions.

fn w_outer_positions_share_marker_identity() -> Bool {
compile_dag_rust_emit_check(
"module phantom_marker_outer_positions\n\ntype Quantity = Time | Memory\n\ntype Measure<Q> { count: Int }\n\ntype AliasPosition = Measure<Time>\n\ntype FieldPosition { field: Measure<Time> }\n\nfn parameter_position(value: Measure<Time>) -> Int { value.count }\n\nfn return_position() -> Measure<Time> { Measure { count: 0 } }\n\ndata data_position: Measure<Time> = Measure { count: 0 }\n",
"src/phantom_marker_outer_positions.rs",
["AliasPosition", "FieldPosition", "parameter_position", "return_position", "data_position", "Measure<Time>"],
["Measure<()>"]
)
}

test fn every_outer_renderer_preserves_the_same_marker_argument() -> Bool {
w_outer_positions_share_marker_identity()
}

// A bare unit variant in a type position is not thereby an applied argument.
// The broad all-type-surface predicate changes this use-line to the direct ZST
// marker import, making this control red and exposing the former 70-file cut.

fn w_non_applied_variant_keeps_value_import() -> Bool {
compile_dag_rust_emit_check(
"module phantom_marker_non_applied\n\nimport std.measure { Time }\n\ntype NonApplied {\n value: Time\n}\n",
"src/phantom_marker_non_applied.rs",
["std_measure::Quantity"],
[]
)
}

test fn non_applied_unit_variant_does_not_become_marker_import() -> Bool {
w_non_applied_variant_keeps_value_import()
}

// Both consumers are in one closure and bind the same leaf spelling to
// different declarations. The marker consumer must read its own module env;
// a closure-global env selecting the plain record loses the marker identity.

fn w_marker_module_uses_its_own_type_environment() -> Bool {
let entry = "module phantom_marker_module_context\n\nimport test.fixture.phantom_marker_identity.marker_consumer { MarkerConsumer }\nimport test.fixture.phantom_marker_identity.plain_consumer { PlainConsumer }\n\ntype Both { marker: MarkerConsumer, plain: PlainConsumer }\n"
compile_dag_rust_emit_check(
entry,
"src/test_fixture_phantom_marker_identity_marker_consumer.rs",
["Measure<Time"],
["Quantity::\\\{Time}"]
)
}

fn w_plain_module_uses_its_own_type_environment() -> Bool {
let entry = "module phantom_marker_module_context\n\nimport test.fixture.phantom_marker_identity.marker_consumer { MarkerConsumer }\nimport test.fixture.phantom_marker_identity.plain_consumer { PlainConsumer }\n\ntype Both { marker: MarkerConsumer, plain: PlainConsumer }\n"
compile_dag_rust_emit_check(
entry,
"src/test_fixture_phantom_marker_identity_plain_consumer.rs",
["PlainBox<Time>"],
["std_measure::Quantity"]
)
}

test fn module_context_not_global_context_decides_marker_identity() -> Bool {
w_marker_module_uses_its_own_type_environment()
&& w_plain_module_uses_its_own_type_environment()
}

// The unrelated record deliberately shares the leaf SharedMarker with the
// marker provider in the same closure. Its resolved declaration identity,
// not last-segment equality, determines that it is an ordinary type argument.

test fn declaration_identity_wins_over_same_leaf_spelling() -> Bool {
compile_dag_rust_emit_check(
"module phantom_marker_identity_over_spelling\n\nimport test.fixture.phantom_marker_identity.marker_consumer { MarkerConsumer }\nimport test.fixture.phantom_marker_identity.plain_provider { PlainBox, Time }\n\ntype Subject { marker: MarkerConsumer, plain: PlainBox<Time> }\n",
"src/phantom_marker_identity_over_spelling.rs",
["PlainBox<Time>"],
["std_measure::Time"]
)
}

// Position is recomputed at each immediate parent edge. In
// Outer<Inner<Time>>, Time qualifies from the Inner edge, not transitively from
// Outer, while the applied head Inner is never mistaken for its own argument.

test fn nested_applied_argument_keeps_inner_marker_identity() -> Bool {
compile_dag_rust_emit_check(
"module phantom_marker_nested\n\ntype Quantity = Time | Memory\n\ntype Inner<Q> { value: Int }\n\ntype Outer<T> { value: Int }\n\ntype Nested { value: Outer<Inner<Time>> }\n",
"src/phantom_marker_nested.rs",
["Inner<Time>"],
["Inner<()>"]
)
}

// Root and applied-child positions share one alias renderer, so both must live
// in this one module: the valid applied use deliberately makes the Time marker
// declaration available and prevents the bare alias from passing merely because
// its wrong marker spelling happened to be unbound.
test fn alias_root_does_not_borrow_applied_child_marker_identity() -> Bool {
compile_dag_rust_emit_check(
"module phantom_marker_alias_position\n\ntype Quantity = Time | Memory\n\ntype Box<Q> { value: Int }\n\ntype AppliedAlias = Box<Time>\ntype BareAlias = Time\n\nfn bare_alias_as_parent(value: BareAlias) -> Quantity { value }\n",
"src/phantom_marker_alias_position.rs",
["pub type AppliedAlias = Rc<Box<Time>>", "pub type BareAlias =", "-> Quantity"],
["pub type AppliedAlias = Rc<Box<()>>", "pub type BareAlias = Time", "pub type BareAlias = Memory"]
)
}

// HONESTY WITNESS, NOT A COMPILED-TARGET DISCRIMINATOR. The source model accepts
// a non-applied unit-variant field at its parent Quantity boundary, and the
// positional emitter preserves that authored/value spelling rather than silently
// substituting the distinct phantom marker struct. compile_dag_rust_emit_check
// stops at emitted text; it does not invoke rustc. Consequently this row proves
// the modeled parent relation reaches emission and proves the spelling decision,
// but it MUST NOT be cited as evidence that the emitted crate compiles.
test fn non_applied_unit_variant_keeps_its_modeled_parent_relation_visible() -> Bool {
compile_dag_rust_emit_check(
"module phantom_marker_non_applied_parent_honesty\n\nimport test.fixture.phantom_marker_identity.non_applied_consumer { NonAppliedMarkerField, non_applied_field_as_parent }\n\ntype Subject { value: NonAppliedMarkerField }\n",
"src/test_fixture_phantom_marker_identity_non_applied_consumer.rs",
["pub value: Time", "-> Quantity", "Quantity::\{Time}"],
["pub use crate::std_measure::\{Time}"]
)
}

// CEILING, NOT A RUNG DROP. The meaning-level two-arm discriminator is authorable
// and has been executed: the broad classifier substitutes a marker struct and
// rustc reports E0308 at the parent boundary; the positional classifier exposes
// the unsupported non-applied projection and rustc reports E0573. It is not
// enrollable today. compile_dag_rust_emit_check and compile_dag_multi_module_fixture
// stop at emitted text, while emitted_closure_compile_host is wired to the live
// required-entry roster and offers no fixture-callable operation.
//
// The carrier below makes the enrollment standing, currently enrolled boundary,
// trigger, and routed owner separately readable facts. In particular, an executed
// scratch discriminator cannot be mistaken for enrolled coverage.
type PhantomMarkerMeaningDiscriminatorEnrollmentStanding
= MeaningDiscriminatorExecutedNotEnrolled
| MeaningDiscriminatorEnrolled

type PhantomMarkerMeaningDiscriminatorEnrolledBoundary
= SourceToEmittedTextHonestyOnly
| EmittedCrateMeaningDiscriminator

type PhantomMarkerMeaningDiscriminatorRouteOwner = NonEmptyStr where brand("PhantomMarkerMeaningDiscriminatorRouteOwner")

type PhantomMarkerMeaningDiscriminatorCeiling {
standing: PhantomMarkerMeaningDiscriminatorEnrollmentStanding
enrolled_boundary: PhantomMarkerMeaningDiscriminatorEnrolledBoundary
next_rung_trigger: DissolutionCondition
route_owner: PhantomMarkerMeaningDiscriminatorRouteOwner
}

data non_applied_marker_meaning_discriminator_ceiling: PhantomMarkerMeaningDiscriminatorCeiling = PhantomMarkerMeaningDiscriminatorCeiling {
standing: MeaningDiscriminatorExecutedNotEnrolled,
enrolled_boundary: SourceToEmittedTextHonestyOnly,
next_rung_trigger: unbound_dissolution(description: "dissolve-on: a fixture-callable emitted-crate compilation route runs unconditionally on an acceptance path, executes this meaning discriminator's two arms to rustc termination over their synthetic emitted closure, and compares the reported diagnostics. A callable route or ignored test alone does not fire this trigger."),
route_owner: "node://adhoc-da8c65c0-b3b" as PhantomMarkerMeaningDiscriminatorRouteOwner,
}
8 changes: 8 additions & 0 deletions dag/test/fixture/phantom_marker_identity/marker_consumer.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module test.fixture.phantom_marker_identity.marker_consumer

import std.measure { Measure, One, Time }
import std.nat { Nat }

type MarkerConsumer {
value: Measure<Time, One, Nat>
}
11 changes: 11 additions & 0 deletions dag/test/fixture/phantom_marker_identity/non_applied_consumer.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module test.fixture.phantom_marker_identity.non_applied_consumer

import std.measure { Quantity, Time }

type NonAppliedMarkerField {
value: Time
}

fn non_applied_field_as_parent(subject: NonAppliedMarkerField) -> Quantity {
subject.value
}
7 changes: 7 additions & 0 deletions dag/test/fixture/phantom_marker_identity/plain_consumer.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module test.fixture.phantom_marker_identity.plain_consumer

import test.fixture.phantom_marker_identity.plain_provider { PlainBox, Time }

type PlainConsumer {
value: PlainBox<Time>
}
9 changes: 9 additions & 0 deletions dag/test/fixture/phantom_marker_identity/plain_provider.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module test.fixture.phantom_marker_identity.plain_provider

type Time {
value: Int
}

type PlainBox<T> {
value: Int
}
24 changes: 19 additions & 5 deletions src/v1/04_emit_info.dag
Original file line number Diff line number Diff line change
Expand Up @@ -59,19 +59,33 @@ fn dedupe_nonempty_strings(items: List<String>) -> List<String> {
acc.result
}

fn collect_type_node_import_surface_names(n: Node, source_indices: Map<String, NewlineIndex>) -> List<String> {
// One walk owns both the import-surface name and the position in which that
// occurrence was found. Consumers that only need names project `name`; the
// Rust use-line classifier additionally consumes `applied_argument` rather
// than rebuilding a second walk over the same type tree.
type TypeSurfaceOccurrence {
name: String
applied_argument: Bool
}

fn collect_type_node_import_surface_occurrences(n: Node, applied_argument: Bool, source_indices: Map<String, NewlineIndex>) -> List<TypeSurfaceOccurrence> {
let peeled = normalize_access_type_node(n: n)
let name = authored_name_at(source_indices: source_indices, node: peeled)
let is_tv = if peeled.inferred != none { is_type_variable(inferred: peeled.inferred.value) } else { false }
let own = if name != "" && name != "Dynamic" && !is_tv { [name] } else { [] }
let own = if name != "" && name != "Dynamic" && !is_tv { [TypeSurfaceOccurrence { name: name, applied_argument: applied_argument }] } else { [] }
let children_are_applied_arguments = name != "" && peeled.connective == NoConnective && peeled.children |> count > 0
let child_names = peeled.children |> flat_map(ch =>
collect_type_node_import_surface_names(n: child_type_node(ch: ch), source_indices: source_indices)
collect_type_node_import_surface_occurrences(n: child_type_node(ch: ch), applied_argument: children_are_applied_arguments, source_indices: source_indices)
)
let inferred_names = match peeled.inferred {
Present { value: Resolved { node: rt } } => collect_type_node_import_surface_names(n: rt, source_indices: source_indices)
Present { value: Resolved { node: rt } } => collect_type_node_import_surface_occurrences(n: rt, applied_argument: applied_argument, source_indices: source_indices)
_ => []
}
dedupe_nonempty_strings(items: concat(own, concat(child_names, inferred_names)))
concat(own, concat(child_names, inferred_names))
}

fn collect_type_node_import_surface_names(n: Node, source_indices: Map<String, NewlineIndex>) -> List<String> {
dedupe_nonempty_strings(items: collect_type_node_import_surface_occurrences(n: n, applied_argument: false, source_indices: source_indices) |> map(o => o.name))
}

// fn_decl_items mirrors type_decl_items (dashboard node adhoc-574e999b-39c): a fn-name-keyed lookup
Expand Down
Loading
Loading