fix(interp): comptime subslice over non-string aggregates

`arr[lo..hi]` at comptime bailed for any non-string base — the interp's
.subslice op only handled string-backed values. Worse, the open-ended
`hi` came from a .length op that misread a 2-element array as a {ptr,len}
fat pointer (returning the 2nd element, not the count), so even lo/hi
weren't valid ints.

Fix, interp-only (runtime already handles arrays via LLVMTypeOf):
- Thread the base operand's IR type onto the Subslice op (base_ty); the
  interp uses it to tell a bare array (elements = aggregate fields) from a
  {data,len} slice (elements in the data field) — indistinguishable by
  Value shape alone.
- Fold an open-ended slice's hi to the array's static length for fixed
  arrays at lower time (runtime emitLength folds the same constant, so the
  IR result is unchanged — no snapshot churn — but the comptime interp no
  longer hits the ambiguous .length op).
- subsliceElements() resolves the element list (array/slice, inline or
  slot_ptr-backed) and subslice returns a proper {data,len} slice value.

Suite green (678), no .ir changes.
This commit is contained in:
agra
2026-06-17 05:11:33 +03:00
parent 4e8075491d
commit d22037c4a7
3 changed files with 65 additions and 10 deletions

View File

@@ -323,6 +323,11 @@ pub const Subslice = struct {
base: Ref, base: Ref,
lo: Ref, lo: Ref,
hi: Ref, hi: Ref,
/// The base operand's IR type (array vs slice vs string). The runtime
/// backend reads array/slice-ness off `LLVMTypeOf`, but the comptime
/// interp can't tell a 2-element array from a `{ptr,len}` fat pointer by
/// Value shape alone, so it consults this. `.void` for old call sites.
base_ty: TypeId = .void,
}; };
pub const Call = struct { pub const Call = struct {

View File

@@ -1234,15 +1234,28 @@ pub const Interpreter = struct {
}, },
.subslice => |sub| { .subslice => |sub| {
const base = frame.getRef(sub.base); const base = frame.getRef(sub.base);
const lo_val = frame.getRef(sub.lo); const lo: usize = @intCast(frame.getRef(sub.lo).asInt() orelse return bailDetail("comptime subslice: lo index is not an integer"));
const hi_val = frame.getRef(sub.hi); const hi: usize = @intCast(frame.getRef(sub.hi).asInt() orelse return bailDetail("comptime subslice: hi index is not an integer"));
const lo: usize = @intCast(lo_val.asInt() orelse return error.TypeError); if (hi < lo) return error.OutOfBounds;
const hi: usize = @intCast(hi_val.asInt() orelse return error.TypeError);
if (base.asString(self)) |s| { if (base.asString(self)) |s| {
if (hi > s.len) return error.OutOfBounds; if (hi > s.len) return error.OutOfBounds;
return .{ .value = .{ .string = s[lo..hi] } }; return .{ .value = .{ .string = s[lo..hi] } };
} }
return bailDetail("comptime subslice: base is not a string-backed value (slice over non-string aggregates not yet supported)"); // Non-string aggregate (array or `{data,len}` slice). The
// underlying element list comes from the aggregate directly (an
// array) or its data field (a slice) — `sub.base_ty` picks which,
// since a 2-element array and a `{ptr,len}` pair are
// indistinguishable by Value shape alone.
const elems = self.subsliceElements(frame, base, sub.base_ty) orelse
return bailDetail("comptime subslice: base is not a sliceable array/slice value");
if (hi > elems.len) return error.OutOfBounds;
const sub_elems = elems[lo..hi];
// Return a proper slice VALUE `{data, len}`: data is the element
// aggregate, len the (int) count. The int len is what lets
// downstream `.length` / `index_get` / `decodeVariantElements`
// read this as a slice and not a bare array.
const pair = self.alloc.dupe(Value, &.{ .{ .aggregate = sub_elems }, .{ .int = @intCast(sub_elems.len) } }) catch return error.CannotEvalComptime;
return .{ .value = .{ .aggregate = pair } };
}, },
// ── Addr/deref ───────────────────────────────────── // ── Addr/deref ─────────────────────────────────────
@@ -1753,6 +1766,33 @@ pub const Interpreter = struct {
return current; return current;
} }
/// The element list backing a comptime array/slice VALUE, for `subslice`.
/// `base_ty` (threaded onto the op at lower time) disambiguates the two
/// shapes that look identical as Values: an ARRAY's aggregate holds its
/// elements directly, while a SLICE is a `{data, len}` fat pointer whose
/// `data` field holds them. Returns null for any other shape (caller bails).
fn subsliceElements(self: *Interpreter, frame: *Frame, base: Value, base_ty: TypeId) ?[]const Value {
var b = base;
if (b == .slot_ptr) b = self.resolveSlotChain(frame, b);
const fields = switch (b) {
.aggregate => |f| f,
else => return null,
};
const is_slice = !base_ty.isBuiltin() and self.module.types.get(base_ty) == .slice;
if (is_slice) {
if (fields.len != 2) return null;
const len: usize = @intCast(fields[1].asInt() orelse return null);
var data = fields[0];
if (data == .slot_ptr) data = self.resolveSlotChain(frame, data);
return switch (data) {
.aggregate => |arr| if (len <= arr.len) arr[0..len] else null,
else => null,
};
}
// Array (or unknown base_ty fallback): the fields ARE the elements.
return fields;
}
// ── Constant → Value conversion ───────────────────────────── // ── Constant → Value conversion ─────────────────────────────
fn constToValue(self: *Interpreter, cv: inst_mod.ConstantValue) Value { fn constToValue(self: *Interpreter, cv: inst_mod.ConstantValue) Value {

View File

@@ -1397,19 +1397,29 @@ pub fn lowerIndexExpr(self: *Lowering, ie: *const ast.IndexExpr) Ref {
pub fn lowerSliceExpr(self: *Lowering, se: *const ast.SliceExpr) Ref { pub fn lowerSliceExpr(self: *Lowering, se: *const ast.SliceExpr) Ref {
const obj = self.lowerExpr(se.object); const obj = self.lowerExpr(se.object);
const obj_ty = self.inferExprType(se.object);
var lo = if (se.start) |s| self.lowerExpr(s) else self.builder.constInt(0, .i64); var lo = if (se.start) |s| self.lowerExpr(s) else self.builder.constInt(0, .i64);
if (se.start_exclusive) lo = self.builder.add(lo, self.builder.constInt(1, .i64), .i64); if (se.start_exclusive) lo = self.builder.add(lo, self.builder.constInt(1, .i64), .i64);
var hi = if (se.end) |e| self.lowerExpr(e) else self.builder.emit(.{ .length = .{ .operand = obj } }, .i64); // Open-ended `hi`: for a fixed-size array the length is a compile-time
// constant — emit it directly rather than a runtime `.length` op. Runtime
// codegen folds the identical constant for an array (`emitLength`), so the
// result is unchanged; the win is the comptime interp, which can't
// disambiguate a 2-element array from a `{ptr,len}` fat pointer by Value
// shape and so would misread a `.length` op on an array.
var hi = if (se.end) |e|
self.lowerExpr(e)
else if (!obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .array)
self.builder.constInt(@intCast(self.module.types.get(obj_ty).array.length), .i64)
else
self.builder.emit(.{ .length = .{ .operand = obj } }, .i64);
if (se.end_inclusive) hi = self.builder.add(hi, self.builder.constInt(1, .i64), .i64); if (se.end_inclusive) hi = self.builder.add(hi, self.builder.constInt(1, .i64), .i64);
// Infer result slice type from the object
const obj_ty = self.inferExprType(se.object);
// Subslice of string stays string (same {ptr, i64} layout, correct type category) // Subslice of string stays string (same {ptr, i64} layout, correct type category)
if (obj_ty == .string) { if (obj_ty == .string) {
return self.builder.emit(.{ .subslice = .{ .base = obj, .lo = lo, .hi = hi } }, .string); return self.builder.emit(.{ .subslice = .{ .base = obj, .lo = lo, .hi = hi, .base_ty = obj_ty } }, .string);
} }
const elem_ty = self.getElementType(obj_ty); const elem_ty = self.getElementType(obj_ty);
const slice_ty = if (elem_ty != .void) self.module.types.sliceOf(elem_ty) else self.module.types.sliceOf(.u8); const slice_ty = if (elem_ty != .void) self.module.types.sliceOf(elem_ty) else self.module.types.sliceOf(.u8);
return self.builder.emit(.{ .subslice = .{ .base = obj, .lo = lo, .hi = hi } }, slice_ty); return self.builder.emit(.{ .subslice = .{ .base = obj, .lo = lo, .hi = hi, .base_ty = obj_ty } }, slice_ty);
} }
pub fn lowerTupleLiteral(self: *Lowering, tl: *const ast.TupleLiteral) Ref { pub fn lowerTupleLiteral(self: *Lowering, tl: *const ast.TupleLiteral) Ref {