Files
sx/src/ir/conversions.zig
agra 12552e125d fix(ir): resolve named-const array dims (0083) + materialize literal slice args (0084)
Two silent-miscompile codegen fixes:

0083 — named-const array dimension. `TypeResolver.resolveCompound`'s array
arm resolved the dimension with `if int_literal ... else 0`, so a named const
(`N :: 16; [N]T`) hit the silent `else 0`: the array became 0-length / 0-byte
and element access ran out of bounds (garbage for scalars, bus error for
slice/pointer/struct elements). The arm now delegates the dimension to
`inner.resolveArrayLen` (symmetric with `inner.resolveInner` for the element).
The stateful `Lowering.resolveArrayLen` evaluates it as a compile-time integer
across the comptime-constant / generic-value / module-global const tables and
emits a diagnostic — no fabricated length — when it isn't one.

0084 — `.[...]` literal passed directly as a call arg. `lowerArrayLiteral`
always yields an aggregate array value; the array→slice conversion is the
caller's job. The local-bound var-decl path did it, but the call-arg coercion
path had no array→slice arm, so `classify([N]T, []T)` returned `.none` and the
raw array was passed where a slice was expected (callee read its {ptr,len}
header off the wrong bytes → 0 / garbage / segfault). `classify` now returns a
new `.array_to_slice` plan for same-element `[N]T → []T`, and `coerceToType`
emits the existing `array_to_slice` op — identical to the local-bound path.

Regressions (fail-before/pass-after demonstrated on the pre-fix compiler):
  examples/0140-types-named-const-array-dim.sx (s64 + string + struct elems)
  examples/0141-types-slice-literal-direct-call-arg.sx (string + []s64)

Gate: zig build, zig build test, bash tests/run_examples.sh (387 passed).
Issues 0083 and 0084 marked RESOLVED.
2026-06-04 08:22:45 +03:00

152 lines
6.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)
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 == .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;
}
}
// 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;
const dst_ptr = !dst_ty.isBuiltin() and self.l.module.types.get(dst_ty) == .pointer;
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;
}
};