diff --git a/src/ir/inst.zig b/src/ir/inst.zig index b3c8c14c..407a964e 100644 --- a/src/ir/inst.zig +++ b/src/ir/inst.zig @@ -323,6 +323,11 @@ pub const Subslice = struct { base: Ref, lo: 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 { diff --git a/src/ir/interp.zig b/src/ir/interp.zig index e236d557..9a6f61ed 100644 --- a/src/ir/interp.zig +++ b/src/ir/interp.zig @@ -1234,15 +1234,28 @@ pub const Interpreter = struct { }, .subslice => |sub| { const base = frame.getRef(sub.base); - const lo_val = frame.getRef(sub.lo); - const hi_val = frame.getRef(sub.hi); - const lo: usize = @intCast(lo_val.asInt() orelse return error.TypeError); - const hi: usize = @intCast(hi_val.asInt() orelse return error.TypeError); + const lo: usize = @intCast(frame.getRef(sub.lo).asInt() orelse return bailDetail("comptime subslice: lo index is not an integer")); + const hi: usize = @intCast(frame.getRef(sub.hi).asInt() orelse return bailDetail("comptime subslice: hi index is not an integer")); + if (hi < lo) return error.OutOfBounds; if (base.asString(self)) |s| { if (hi > s.len) return error.OutOfBounds; 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 ───────────────────────────────────── @@ -1753,6 +1766,33 @@ pub const Interpreter = struct { 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 ───────────────────────────── fn constToValue(self: *Interpreter, cv: inst_mod.ConstantValue) Value { diff --git a/src/ir/lower/expr.zig b/src/ir/lower/expr.zig index 1fef4e92..94aa021b 100644 --- a/src/ir/lower/expr.zig +++ b/src/ir/lower/expr.zig @@ -1397,19 +1397,29 @@ pub fn lowerIndexExpr(self: *Lowering, ie: *const ast.IndexExpr) Ref { pub fn lowerSliceExpr(self: *Lowering, se: *const ast.SliceExpr) Ref { 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); 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); - // 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) 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 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 {