Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion src/v1/04_infer.dag
Original file line number Diff line number Diff line change
Expand Up @@ -1474,7 +1474,21 @@ fn infer_expr(texpr: Node, scope: InferScope, expected: Node?) -> InferResult {
let field_type_lookup = lookup_field_type_node(n: resolved_base, field_name: field_name, source_indices: scope.type_env.source_indices)
match field_type_lookup {
Present { value: field_type } =>
let field_summary = field_summary_for_type(base_type: base_rt, env: scope.type_env, field: field_name)
// The field TYPE was found via `resolved_base` (the alias-chain-
// expanded structural record, G3). Derive the FieldSummary from the
// SAME expansion so the two stay consistent: `field_summary_for_type`
// peels with `resolve_scrutinee_type_node` alone, which does not
// chase the structural-name hop a cross-module / multi-hop alias
// needs (e.g. `Gibibyte` -> `Measure<Memory, Gibi, Nat>` -> the
// `Measure { count }` struct). Try the authored type first (keeps the
// optional `.value` fast-path unchanged) and fall back to the
// expanded record only when the authored type yields no summary --
// otherwise emission fails closed to `compile_error!("field access
// missing reconcile summary ...")`.
let field_summary = match field_summary_for_type(base_type: base_rt, env: scope.type_env, field: field_name) {
Present { value: s } => Present { value: s }
Absent => field_summary_for_type(base_type: resolved_base, env: scope.type_env, field: field_name)
}
let fa_texpr = make_named_expr_node(
name: field_name,
expr_data: ExprFieldAccess { summary: field_summary },
Expand Down
152 changes: 144 additions & 8 deletions src/v1/05_emit_rust.dag
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ import v1.compiler.infer {
build_params_scope, extend_scope,
build_emit_graph_info,
expr_span,
resolved_type_name
resolved_type_name,
expand_type_for_field_access
}
import v1.compiler.infer_emit_info {
EmitGraphInfo, TypeSummary, StructRepr, EnumRepr,
Expand Down Expand Up @@ -3163,6 +3164,46 @@ fn emit_type_def_from_connective(item: Node, recursive_types: Set<String>, share
}


// True iff the type node `n` (or any nested child) names `target` -- the
// structural "does this field type mention this generic param" test.
fn type_node_mentions_name(n: Node, target: String, source_indices: Map<String, NewlineIndex>) -> Bool {
if authored_name_at(source_indices: source_indices, node: n) == target {
true
} else {
n.children |> any(c => type_node_mentions_name(n: c, target: target, source_indices: source_indices))
}
}

// A struct generic param that no field's type references is UNUSED -- Rust
// rejects it (E0392 "parameter is never used"). The phantom dimensional tags on
// `Measure<Q, S, M>` (only `M` reaches the `count` field; `Q`/`S` tag the
// dimension) are the canonical case. Such params must be carried in a
// `PhantomData` field for the emitted struct to be well-formed, and every
// construction must then populate that field (see emit_typed_record_lit).
fn struct_unused_param_names(generic_param_names: List<String>, children: List<Node>, source_indices: Map<String, NewlineIndex>) -> List<String> {
generic_param_names |> filter(p =>
!(children |> any(child => type_node_mentions_name(n: field_node_type_expr(n: child), target: p, source_indices: source_indices)))
)
}

// The `<...>` arg of the synthetic `PhantomData` carrier for a struct's unused
// params: a single param bare (`PhantomData<Q>`), several as a tuple
// (`PhantomData<(Q, S)>`) so one field "uses" them all.
fn rust_phantom_marker_inner(unused: List<String>) -> String {
// Single unused param -> bare `PhantomData<Q>`; several -> `PhantomData<(Q, S)>`.
// `join` of a one-element list is that element, so the single case needs no
// Optional peel (avoids a `match` whose `Absent` arm is unreachable here).
if unused |> count == 1 {
unused |> join(separator: ", ")
} else {
concat("(", unused |> join(separator: ", "), ")")
}
}

// The unused-param `PhantomData` field name -- single authority shared by the
// struct definition and every construction so they always agree.
fn rust_phantom_field_name() -> String { "_phantom" }

fn emit_struct_from_children(name: String, type_params: String, generic_param_names: List<String>, children: List<Node>, recursive_types: Set<String>, shared_types: Set<String>, env: TypeEnv, emit_info: EmitGraphInfo) -> String {
let has_fn_fields = match map_get(emit_info.type_summaries, name) {
Present { value: ts } => ts.has_fn_fields
Expand All @@ -3181,7 +3222,16 @@ fn emit_struct_from_children(name: String, type_params: String, generic_param_na
concat(derives, "\n", rust_visibility_prefix(), rust_items().struct_keyword, " ", name, type_params, ";")
} else {
let field_lines = children |> map(child => emit_struct_field_from_child(struct_name: name, child: child, generic_param_names: generic_param_names, recursive_types: recursive_types, shared_types: shared_types, env: env, emit_info: emit_info))
let fields_str = field_lines |> join(separator: "\n")
// E0392: carry any generic param unused by the fields in a PhantomData
// field (`Measure<Q, S, M>`'s phantom `Q`/`S`). The `#[derive]` on the
// struct adds the standard `Q: Trait` bounds, satisfied by the concrete
// marker tags at every instantiation; serde special-cases PhantomData and
// adds no Serialize/Deserialize bound.
let unused_params = struct_unused_param_names(generic_param_names: generic_param_names, children: children, source_indices: env.source_indices)
let all_field_lines = if unused_params |> count > 0 {
concat(field_lines, [concat(" ", rust_visibility_prefix(), rust_phantom_field_name(), ": std::marker::PhantomData<", rust_phantom_marker_inner(unused: unused_params), ">,")])
} else { field_lines }
let fields_str = all_field_lines |> join(separator: "\n")
concat(derives, "\n", rust_visibility_prefix(), rust_items().struct_keyword, " ", name, type_params, " {\n", fields_str, "\n}")
}
}
Expand Down Expand Up @@ -4592,6 +4642,23 @@ fn emit_typed_expr_base(texpr: Node, registry: Map<String, ItemInfo>, scope: Inf
}
}

// True iff the dot-accessed `field` on `base` is stored as `Box<..>` -- the
// struct-def emitter boxes a field whose type `needs_box_wrapping` (recursive
// types, and generic-param magnitudes like `Measure.count: Box<M>`). Reading
// such a field as a plain `base.field.clone()` is an E0308 (`Box<M>` vs `M`);
// it must deref through the Box. Reuses the SAME `needs_box_wrapping` the def
// uses, so the read always agrees with how the field was stored. The base type
// is alias-chain-expanded first so a cross-module alias (`Gibibyte` ->
// `Measure { count }`) resolves to the struct carrying the field.
fn field_access_field_is_boxed(base: Node, field: String, scope: InferScope, shared_types: Set<String>, emit_info: EmitGraphInfo) -> Bool {
let base_struct = expand_type_for_field_access(n: resolved_type(n: base), env: scope.type_env, module_name: scope.module_name)
match find_child_named(n: base_struct, name: field, source_indices: scope.type_env.source_indices) {
Present { value: field_child } =>
needs_box_wrapping(n: resolved_type(n: field_child), recursive_types: emit_info.recursive_type_set, shared_types: shared_types, source_indices: scope.type_env.source_indices)
Absent => false
}
}

fn emit_typed_field_access(base: Node, field: String, summary: FieldSummary?, registry: Map<String, ItemInfo>, scope: InferScope, depth: Int, shared_types: Set<String>, emit_info: EmitGraphInfo) -> String {
let base_str = emit_typed_expr_base(texpr: base, registry: registry, scope: scope, depth: depth, shared_types: shared_types, emit_info: emit_info)
let base_is_anon_record = match base.inferred {
Expand Down Expand Up @@ -4629,7 +4696,15 @@ fn emit_typed_field_access(base: Node, field: String, summary: FieldSummary?, re
set_contains(emit_info.owned_bindings, base_name)
_ => false
}
if base_is_owned {
let field_is_boxed = field_access_field_is_boxed(base: base, field: field, scope: scope, shared_types: shared_types, emit_info: emit_info)
if field_is_boxed {
// Box<M> stored field: deref through the Box to the magnitude, then
// clone. The parens group the deref before `.clone()` (unary `*`
// binds looser than `.`), so they are NOT redundant and stay
// warning-clean under -D warnings in sole-return, chained, and
// receiver positions alike.
concat("(*", base_str, ".", emit_ident(name: field, target: Rust), ").clone()")
} else if base_is_owned {
concat(base_str, ".", emit_ident(name: field, target: Rust))
} else {
apply_type_template1(template: sharing.clone_value, arg0: concat(base_str, ".", emit_ident(name: field, target: Rust)))
Expand Down Expand Up @@ -4868,15 +4943,49 @@ fn emit_rust_expr_record_lit(expr: Node, registry: Map<String, ItemInfo>, scope:
ExprRecordLit { parent_enum: parent_enum } =>
let tn = record_lit_type_name_at(texpr: expr, source_indices: scope.type_env.source_indices)
let fs = expr.children
let raw = emit_typed_record_lit(type_name: tn, fields: fs, parent_enum: parent_enum, resolved_type: resolved_type(n: expr), registry: registry, scope: scope, depth: depth, shared_types: shared_types, emit_info: emit_info)
// Peel a struct-alias literal to its underlying concrete struct before
// emission. `Gibibyte { count: x }` (where `type Gibibyte =
// Measure<Memory, Gibi, Nat>`) leaves `resolved_type` a bare alias leaf,
// so the emitter would name the alias (E0560: `Rc<Measure>` has no field
// `count`), skip `Box`-wrapping the field, and skip the `Rc::new` share
// wrap. Expanding the alias chain to the `Measure { count }` struct
// (G3, same machinery the field-access TYPE lookup uses) makes the struct
// NAME, the field `Box<M>` boxing, and the shared-type `Rc::new` all
// resolve exactly as the direct `Measure { .. }` literal already does.
// For a non-alias literal `resolved_type` is already the concrete record,
// and the expansion returns it unchanged.
let si = scope.type_env.source_indices
let resolved_rt = resolved_type(n: expr)
let expanded_rt = expand_type_for_field_access(n: resolved_rt, env: scope.type_env, module_name: scope.module_name)
let variant_name = match tn { Present { value: n } => n Absent => "" }
// The struct-literal NAME passed to emit_typed_record_lit must be the
// CONCRETE struct (`Measure`), not the authored alias (`Gibibyte` =
// `Rc<Measure<..>>`, not brace-constructible). `record_lit_type_name_at`
// gives the alias; `expanded_rt` is the peeled struct decl. Pass its name
// so the literal name, the `Box<M>` field boxing (keyed on the type_name),
// and the `PhantomData` population (E0392) all resolve on the real struct
// — every one of these keys on this name, so they must agree (cursor
// review 31502). Only override for a plain struct-alias literal (peeled
// type is a named product, not an enum variant); variants / non-aliases
// keep their authored name.
let peeled_type_name = if parent_enum == none && expanded_rt.connective == Conj && expanded_rt.ident_span != none {
let concrete = authored_name_at(source_indices: si, node: expanded_rt)
if concrete != "" && concrete != variant_name { Present { value: concrete } } else { tn }
} else { tn }
let raw = emit_typed_record_lit(type_name: peeled_type_name, fields: fs, parent_enum: parent_enum, resolved_type: expanded_rt, registry: registry, scope: scope, depth: depth, shared_types: shared_types, emit_info: emit_info)
let rc_name = match parent_enum {
Present { value: en } => en
Absent =>
match contextual_variant_parent(variant_name: variant_name, parent_enum: parent_enum, resolved_type: resolved_type(n: expr), emit_info: emit_info, source_indices: si) {
match contextual_variant_parent(variant_name: variant_name, parent_enum: parent_enum, resolved_type: expanded_rt, emit_info: emit_info, source_indices: si) {
Present { value: p } => p
Absent => variant_name
Absent =>
// Share-wrap on the peeled concrete struct (`Measure`) when the
// literal was written through an alias whose own name is not a
// shared type; falls back to the written name otherwise.
let expanded_name = authored_name_at(source_indices: si, node: expanded_rt)
if expanded_rt.ident_span != none && expanded_name != "" && expanded_name != variant_name && set_contains(shared_types, expanded_name) {
expanded_name
} else { variant_name }
}
}
if rc_name != "" && set_contains(shared_types, rc_name) {
Expand Down Expand Up @@ -6242,7 +6351,16 @@ fn emit_typed_record_lit(type_name: String?, fields: List<Node>, parent_enum: St
Absent =>
let rt_is_type_var = if resolved_type.inferred != none { is_type_variable(inferred: resolved_type.inferred.value) } else { false }
let rt_name = authored_name_at(source_indices: si, node: resolved_type)
if resolved_type.ident_span != none && rt_name != tn && !rt_is_type_var && rt_name != "Error" {
// Only treat `rt_name` as an enum PARENT (prefixing `display_tn` as
// `Enum::Variant`) when `tn` is genuinely a variant of it -- the
// guard the sibling sites already use. Without it, a struct-alias
// literal whose expanded `resolved_type` names a different concrete
// struct than the written name would wrongly emit `Measure::Gibibyte`
// (cursor review 31502). For a plain struct it is not a variant, so
// the fallback yields `parent_enum` and `display_tn` stays the bare
// struct name.
if resolved_type.ident_span != none && rt_name != tn && !rt_is_type_var && rt_name != "Error"
&& variant_belongs_to_enum(type_summaries: emit_info.type_summaries, variant_name: tn, enum_name: rt_name) {
Present { value: rt_name }
} else {
parent_enum
Expand Down Expand Up @@ -6341,7 +6459,25 @@ fn emit_typed_record_lit(type_name: String?, fields: List<Node>, parent_enum: St
} else { [] }
Absent => []
}
let all_field_strs = concat(field_strs, default_strs)
// Populate the synthetic PhantomData field (E0392) whenever the target
// struct carries unused generic params -- the construction counterpart
// of the phantom field appended in emit_struct_from_children. `tn` here
// is the CONCRETE struct name (the caller passes the peeled
// `expanded_rt` name as type_name, not the alias), so this lookup hits
// the `Measure` decl (connective==Conj) and fires for Measure/its
// aliases, staying silent for every fully-used-param struct and variant.
let phantom_strs = match lookup_type_by_name(env: scope.type_env, name: tn) {
Present { value: struct_decl } =>
let is_struct = struct_decl.connective == Conj
let pnames = struct_decl.params |> map(p => generic_param_name_at(n: p, source_indices: si))
let unused = struct_unused_param_names(generic_param_names: pnames, children: struct_decl.children, source_indices: si)
let has_unused = (unused |> count) > 0
if is_struct && has_unused {
[concat(" ", rust_phantom_field_name(), ": std::marker::PhantomData,")]
} else { [] }
Absent => []
}
let all_field_strs = concat(concat(field_strs, default_strs), phantom_strs)
let fields_str = all_field_strs |> join(separator: "\n")
let raw = concat(display_tn, " {\n", fields_str, "\n}")
raw
Expand Down
Loading