diff --git a/src/v1/04_infer.dag b/src/v1/04_infer.dag index 19d91715131..9095c06ca80 100644 --- a/src/v1/04_infer.dag +++ b/src/v1/04_infer.dag @@ -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` -> 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 }, diff --git a/src/v1/05_emit_rust.dag b/src/v1/05_emit_rust.dag index bbbc9e285b5..4a6956b4fd5 100644 --- a/src/v1/05_emit_rust.dag +++ b/src/v1/05_emit_rust.dag @@ -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, @@ -3163,6 +3164,46 @@ fn emit_type_def_from_connective(item: Node, recursive_types: Set, 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) -> 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` (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, children: List, source_indices: Map) -> List { + 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`), several as a tuple +// (`PhantomData<(Q, S)>`) so one field "uses" them all. +fn rust_phantom_marker_inner(unused: List) -> String { + // Single unused param -> bare `PhantomData`; 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, children: List, recursive_types: Set, shared_types: Set, 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 @@ -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`'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}") } } @@ -4592,6 +4642,23 @@ fn emit_typed_expr_base(texpr: Node, registry: Map, 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`). Reading +// such a field as a plain `base.field.clone()` is an E0308 (`Box` 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, 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, scope: InferScope, depth: Int, shared_types: Set, 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 { @@ -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 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))) @@ -4868,15 +4943,49 @@ fn emit_rust_expr_record_lit(expr: Node, registry: Map, 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`) leaves `resolved_type` a bare alias leaf, + // so the emitter would name the alias (E0560: `Rc` 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` 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>`, 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` 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) { @@ -6242,7 +6351,16 @@ fn emit_typed_record_lit(type_name: String?, fields: List, 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 @@ -6341,7 +6459,25 @@ fn emit_typed_record_lit(type_name: String?, fields: List, 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