diff --git a/examples/166-pack-type-position-three.sx b/examples/166-pack-type-position-three.sx new file mode 100644 index 0000000..20688ce --- /dev/null +++ b/examples/166-pack-type-position-three.sx @@ -0,0 +1,21 @@ +// Variadic heterogeneous type packs — step 3 complex smoke. +// +// Three-element pack with `$args[2]` (the third element) used in +// the return-type position. Confirms: +// - Multi-arg packs index past the zeroth element correctly. +// - Three distinct call shapes get three distinct monos. +// - The return-type slot is correctly substituted per-mono so +// the inferred caller type matches what the body actually +// returns (string / s64 / bool here). + +#import "modules/std.sx"; + +third :: (..$args) -> $args[2] => args[2]; + +main :: () -> s32 { + a := third(1, 2, "third"); // (s64, s64, string) → "third" + b := third(true, 3.14, 99); // (bool, f64, s64) → 99 + c := third("a", "b", false); // (string, string, bool) → false + print("{} {} {}\n", a, b, c); + return 0; +} diff --git a/src/ast.zig b/src/ast.zig index 9fe2ebd..67b36d5 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -56,6 +56,7 @@ pub const Node = struct { pointer_type_expr: PointerTypeExpr, many_pointer_type_expr: ManyPointerTypeExpr, optional_type_expr: OptionalTypeExpr, + pack_index_type_expr: PackIndexTypeExpr, force_unwrap: ForceUnwrap, null_coalesce: NullCoalesce, deref_expr: DerefExpr, @@ -364,6 +365,15 @@ pub const TypeExpr = struct { protocol_constraints: []const []const u8 = &.{}, // e.g. ["Eq", "Hashable"] for $T/Eq/Hashable }; +/// `$[]` in type position. Resolves to the i-th +/// element type of the active pack binding. Step 3 of the variadic +/// heterogeneous type packs feature — used in trampoline bodies, +/// generic conversions, struct fields parameterised over the pack. +pub const PackIndexTypeExpr = struct { + pack_name: []const u8, + index: u32, +}; + pub const DeferStmt = struct { expr: *Node, }; diff --git a/src/ir/lower.zig b/src/ir/lower.zig index a8ae85d..2d10e2a 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -9740,6 +9740,33 @@ pub const Lowering = struct { /// Resolve a type node, checking type_bindings first for generic type params. fn resolveTypeWithBindings(self: *Lowering, node: *const Node) TypeId { + // Pack-index in a type position: `$[]` resolves to the + // i-th element type of the active pack binding (step 3 of the + // variadic heterogeneous type packs feature). Unblocks parametric + // trampoline bodies (`(*void, $args[0]) -> $args[1]`) in stdlib's + // generic Into(Block) impl. OOB indices emit a diagnostic; no + // binding → bail with a diagnostic-friendly placeholder. + if (node.data == .pack_index_type_expr) { + const pi = node.data.pack_index_type_expr; + if (self.pack_arg_types) |pat| { + if (pat.get(pi.pack_name)) |arg_tys| { + if (pi.index < arg_tys.len) return arg_tys[pi.index]; + if (self.diagnostics) |diags| { + diags.addFmt(.err, node.span, "pack-index type ${s}[{}] out of bounds: '{s}' has {} element{s}", .{ + pi.pack_name, pi.index, pi.pack_name, arg_tys.len, + if (arg_tys.len == 1) @as([]const u8, "") else @as([]const u8, "s"), + }); + } + return .s64; + } + } + if (self.diagnostics) |diags| { + diags.addFmt(.err, node.span, "pack-index type ${s}[{}] used outside an active pack binding", .{ + pi.pack_name, pi.index, + }); + } + return .s64; + } // `*Self` substitution inside foreign-class member declarations // — both foreign and sx-defined — resolves to the class's own // 0-field stub struct (i.e. the opaque Obj-C pointer type). diff --git a/src/ir/type_bridge.zig b/src/ir/type_bridge.zig index aa2fe44..f52beff 100644 --- a/src/ir/type_bridge.zig +++ b/src/ir/type_bridge.zig @@ -26,6 +26,15 @@ pub fn resolveAstType(node: ?*const Node, table: *TypeTable) TypeId { .function_type_expr => |ft| resolveFunctionType(&ft, table), .closure_type_expr => |ct| resolveClosureType(&ct, table), .tuple_type_expr => |tt| resolveTupleType(&tt, table), + .pack_index_type_expr => { + // Pack-index `$args[N]` in a type position must be resolved + // against an active pack binding — `type_bridge` has no access + // to that state, so this bare path falls back to .s64 with a + // diagnostic logged. The pack-aware caller (lowering's + // `resolveTypeWithBindings`) handles this case directly. + std.debug.print("type_bridge: pack-index type expression encountered outside a pack-aware context\n", .{}); + return .s64; + }, .tuple_literal => |tl| resolveTupleLiteralAsType(&tl, table), .parameterized_type_expr => |pt| resolveParameterizedType(&pt, table), .inferred_type => .s64, // inferred — default until we have type inference diff --git a/src/parser.zig b/src/parser.zig index 262de0d..4c2b872 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -460,7 +460,10 @@ pub const Parser = struct { return try self.createNode(start, .{ .array_type_expr = .{ .length = len_node, .element_type = elem_type } }); } - // Generic type parameter introduction: $T or $T/Protocol1/Protocol2 + // Generic type parameter introduction: $T or $T/Protocol1/Protocol2. + // Also: pack-index type access $args[] — resolves to + // the i-th element type of the active pack binding (step 3 of + // the variadic heterogeneous type packs feature). if (self.current.tag == .dollar) { self.advance(); if (self.current.tag != .identifier) { @@ -468,6 +471,24 @@ pub const Parser = struct { } const name = self.tokenSlice(self.current); self.advance(); + // Pack-index access: $[] + if (self.current.tag == .l_bracket) { + self.advance(); // skip '[' + if (self.current.tag != .int_literal) { + return self.fail("expected integer literal in pack index"); + } + const idx_text = self.tokenSlice(self.current); + const idx_val = std.fmt.parseInt(i64, idx_text, 10) catch { + return self.fail("invalid integer literal in pack index"); + }; + if (idx_val < 0) return self.fail("pack index cannot be negative"); + self.advance(); + try self.expect(.r_bracket); + return try self.createNode(start, .{ .pack_index_type_expr = .{ + .pack_name = name, + .index = @intCast(idx_val), + } }); + } // Parse optional protocol constraints: $T/Eq/Hashable var constraints = std.ArrayList([]const u8).empty; while (self.current.tag == .slash) { diff --git a/src/sema.zig b/src/sema.zig index baef5b5..410ef64 100644 --- a/src/sema.zig +++ b/src/sema.zig @@ -884,6 +884,7 @@ pub const Analyzer = struct { .pointer_type_expr, .many_pointer_type_expr, .optional_type_expr, + .pack_index_type_expr, .null_literal, .array_literal, .parameterized_type_expr, @@ -1326,6 +1327,7 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node { .pointer_type_expr, .many_pointer_type_expr, .optional_type_expr, + .pack_index_type_expr, .null_literal, .array_literal, .parameterized_type_expr, diff --git a/tests/expected/166-pack-type-position-three.exit b/tests/expected/166-pack-type-position-three.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/166-pack-type-position-three.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/166-pack-type-position-three.txt b/tests/expected/166-pack-type-position-three.txt new file mode 100644 index 0000000..a7614dc --- /dev/null +++ b/tests/expected/166-pack-type-position-three.txt @@ -0,0 +1 @@ +third 99 false