The 0141 repro relied on a silent-wrong coercion: passing List.items (a
[*]T many-pointer, no length) to a []T parameter passed the bare 8-byte
pointer into a 16-byte {ptr,len} slot — garbage .len, at comptime a segfault
in the VM slice decoder (decodeMemberSlice), at runtime an LLVM verify failure.
Fix (root cause): classify [*]T -> []T as many_to_slice_reject in
conversions.zig and emit a build-gating diagnostic in coerce.zig telling the
user to slice with a length (ptr[0..len]). Guard runComptimeTypeFunc to skip
VM eval once diagnostics.hasErrors() — a type-fn body that failed coercion
holds malformed comptime data (a real host Addr) that would fault the VM's
Ref-level guards.
Land the corrected feature as examples/0640 (List-grown comptime enum via
vs.items[0..vs.len] -> green=7) and the rejection as
examples/1183-diagnostics-many-pointer-to-slice-rejected. Mark issue 0141
RESOLVED.
708/0 corpus + 476/476 unit.
166 lines
7.9 KiB
Zig
166 lines
7.9 KiB
Zig
const std = @import("std");
|
|
const ast = @import("../ast.zig");
|
|
const types = @import("types.zig");
|
|
const lower = @import("lower.zig");
|
|
|
|
const Node = ast.Node;
|
|
const TypeId = types.TypeId;
|
|
const Lowering = lower.Lowering;
|
|
|
|
/// Coercion planning (architecture phase A4.3): classify HOW a value of one
|
|
/// type converts to another, before `Lowering` emits the IR for it. The
|
|
/// classifier is pure (reads the type table + protocol/impl registries); all
|
|
/// actual IR emission — `unbox_any`/`optional_wrap`/`int_to_float`/protocol
|
|
/// erasure/the `Into` call — stays in `Lowering`.
|
|
///
|
|
/// A `*Lowering` facade (Principle 5, like `CallResolver`/`GenericResolver`/
|
|
/// `ProtocolResolver`). Two entry points:
|
|
/// - `classify(src, dst)` — the built-in coercion ladder consumed by
|
|
/// `coerceToType` (the shared, recursive value-conversion path).
|
|
/// - `classifyXX(src, dst)` — the `xx`-operator head consumed by `lowerXX`
|
|
/// (Any unbox, no-op, protocol erasure, protocol→pointer, else the ladder
|
|
/// + the user-`Into` fallback).
|
|
pub const CoercionResolver = struct {
|
|
l: *Lowering,
|
|
|
|
/// The built-in coercion the `coerceToType` ladder will emit for `src → dst`.
|
|
/// `.none` means no built-in applies (the value passes through unchanged;
|
|
/// `lowerXX` then tries a user `Into`). Branch order mirrors `coerceToType`
|
|
/// exactly — the emitter switches on this and reproduces each arm.
|
|
pub const CoercionPlan = enum {
|
|
no_op, // src == dst
|
|
unbox_any, // any → concrete
|
|
box_any, // concrete → any
|
|
closure_to_fn_reject, // closure value → bare fn-ptr (diagnostic, returns operand)
|
|
tuple_elementwise, // (A,B) → (C,D), same arity
|
|
optional_unwrap, // ?T → concrete (narrowing)
|
|
void_to_optional, // void (null literal) → ?T
|
|
optional_wrap, // concrete → ?T
|
|
erase_protocol, // concrete → protocol value
|
|
int_to_float,
|
|
float_to_int,
|
|
ptr_int_bitcast, // ptr ↔ int
|
|
widen, // same kind, dst wider
|
|
narrow, // same kind, dst narrower
|
|
array_to_slice, // [N]T → []T (materialize backing storage + header)
|
|
many_to_slice_reject, // [*]T → []T (no length — needs ptr[0..len]; diagnostic)
|
|
string_to_cstring, // literal-only implicit; other strings need to_cstring
|
|
cstring_to_string_reject, // explicit from_cstring required (diagnostic)
|
|
none, // nothing applies — pass the value through
|
|
};
|
|
|
|
pub fn classify(self: CoercionResolver, src_ty: TypeId, dst_ty: TypeId) CoercionPlan {
|
|
if (src_ty == dst_ty) return .no_op;
|
|
if (src_ty == .string and dst_ty == .cstring) return .string_to_cstring;
|
|
if (src_ty == .cstring and dst_ty == .string) return .cstring_to_string_reject;
|
|
if (src_ty == .any and dst_ty != .any) return .unbox_any;
|
|
if (dst_ty == .any and src_ty != .any) return .box_any;
|
|
|
|
if (!src_ty.isBuiltin() and !dst_ty.isBuiltin()) {
|
|
if (self.l.module.types.get(src_ty) == .closure and self.l.module.types.get(dst_ty) == .function) {
|
|
return .closure_to_fn_reject;
|
|
}
|
|
}
|
|
|
|
// Tuple → Tuple, same arity.
|
|
if (!src_ty.isBuiltin() and !dst_ty.isBuiltin()) {
|
|
const si = self.l.module.types.get(src_ty);
|
|
const di = self.l.module.types.get(dst_ty);
|
|
if (si == .tuple and di == .tuple and si.tuple.fields.len == di.tuple.fields.len) {
|
|
return .tuple_elementwise;
|
|
}
|
|
}
|
|
|
|
// Fixed array → slice of the same element: an aggregate array value
|
|
// (e.g. a `.[...]` literal passed directly as a call arg) needs to be
|
|
// materialized into addressable storage and wrapped in a {ptr,len}
|
|
// header. Without this the array value is passed where a slice is
|
|
// expected — the callee reads the header off the wrong bytes (issue
|
|
// 0084). The local-bound path already does this conversion on its own.
|
|
if (!src_ty.isBuiltin() and !dst_ty.isBuiltin()) {
|
|
const si = self.l.module.types.get(src_ty);
|
|
const di = self.l.module.types.get(dst_ty);
|
|
if (si == .array and di == .slice and si.array.element == di.slice.element) {
|
|
return .array_to_slice;
|
|
}
|
|
// `[*]T → []T`: a many-pointer carries NO length, so it cannot form a
|
|
// `{ptr,len}` slice header implicitly. Silently passing the bare 8-byte
|
|
// pointer where a 16-byte fat pointer is expected corrupts the callee's
|
|
// view (garbage `.len`, mis-aligned reads) — at comptime it segfaults
|
|
// (issue 0141), at runtime it fails LLVM verification. Reject loudly so
|
|
// the user supplies the length via `ptr[0..len]`.
|
|
if (si == .many_pointer and di == .slice) {
|
|
return .many_to_slice_reject;
|
|
}
|
|
}
|
|
|
|
// Optional → Concrete unwrap (narrowing).
|
|
if (!src_ty.isBuiltin()) {
|
|
const src_info = self.l.module.types.get(src_ty);
|
|
if (src_info == .optional) {
|
|
const child_ty = src_info.optional.child;
|
|
if (child_ty == dst_ty or (dst_ty.isBuiltin() and child_ty.isBuiltin())) {
|
|
return .optional_unwrap;
|
|
}
|
|
}
|
|
}
|
|
|
|
// void (null literal) → Optional.
|
|
if (src_ty == .void and !dst_ty.isBuiltin()) {
|
|
if (self.l.module.types.get(dst_ty) == .optional) return .void_to_optional;
|
|
}
|
|
|
|
// Concrete → Optional wrap.
|
|
if (!dst_ty.isBuiltin()) {
|
|
if (self.l.module.types.get(dst_ty) == .optional) return .optional_wrap;
|
|
}
|
|
|
|
// Concrete → Protocol (auto type erasure) — only when the source has a
|
|
// resolvable concrete type name; otherwise fall through to the numeric
|
|
// ladder (matching `coerceToType`, which leaves the erase block).
|
|
if (self.l.getProtocolInfo(dst_ty) != null) {
|
|
if (self.l.resolveConcreteTypeName(src_ty) != null) return .erase_protocol;
|
|
}
|
|
|
|
// Numeric / pointer ladder.
|
|
const src_float = Lowering.isFloat(src_ty);
|
|
const dst_float = Lowering.isFloat(dst_ty);
|
|
const src_int = self.l.isIntEx(src_ty);
|
|
const dst_int = self.l.isIntEx(dst_ty);
|
|
const src_ptr = (!src_ty.isBuiltin() and self.l.module.types.get(src_ty) == .pointer) or src_ty == .cstring;
|
|
const dst_ptr = (!dst_ty.isBuiltin() and self.l.module.types.get(dst_ty) == .pointer) or dst_ty == .cstring;
|
|
|
|
if (src_int and dst_float) return .int_to_float;
|
|
if (src_float and dst_int) return .float_to_int;
|
|
if ((src_ptr and dst_int) or (src_int and dst_ptr)) return .ptr_int_bitcast;
|
|
|
|
const src_bits = self.l.typeBitsEx(src_ty);
|
|
const dst_bits = self.l.typeBitsEx(dst_ty);
|
|
if (src_bits > 0 and dst_bits > 0) {
|
|
if (dst_bits < src_bits) return .narrow;
|
|
if (dst_bits > src_bits) return .widen;
|
|
}
|
|
return .none;
|
|
}
|
|
|
|
/// The `xx`-operator head decision for `lowerXX`. `.coerce` defers to the
|
|
/// built-in ladder (`coerceToType` / `classify`) + the user-`Into` fallback.
|
|
/// Branch order mirrors `lowerXX` exactly.
|
|
pub const XXPlan = enum {
|
|
unbox_any, // src is Any → unbox (lowerXX adds the f32/f64 match dispatch)
|
|
no_op, // src == dst
|
|
erase_protocol, // dst is a protocol → buildProtocolErasure
|
|
protocol_to_pointer, // src is a protocol, dst is a pointer → recover ctx
|
|
coerce, // built-in ladder + user `Into` fallback
|
|
};
|
|
|
|
pub fn classifyXX(self: CoercionResolver, src_ty: TypeId, dst_ty: TypeId) XXPlan {
|
|
if (src_ty == .any) return .unbox_any;
|
|
if (src_ty == dst_ty) return .no_op;
|
|
if (self.l.getProtocolInfo(dst_ty) != null) return .erase_protocol;
|
|
if (self.l.getProtocolInfo(src_ty) != null and !dst_ty.isBuiltin() and
|
|
self.l.module.types.get(dst_ty) == .pointer) return .protocol_to_pointer;
|
|
return .coerce;
|
|
}
|
|
};
|