diff --git a/examples/160-pack-hetero-ret.sx b/examples/160-pack-hetero-ret.sx new file mode 100644 index 0000000..d1d6883 --- /dev/null +++ b/examples/160-pack-hetero-ret.sx @@ -0,0 +1,25 @@ +// Variadic heterogeneous type packs — generic `$R` with +// heterogeneous element pick. `foo(..$args) -> $R => args[2]` +// returns the THIRD arg's value; the ret type is inferred from +// the third arg's concrete type per call shape. +// +// foo(42, 3.2, "hello") → returns "hello" (string). +// +// Exercises: +// - generic `$R` inference for non-zeroth pack indices. +// - heterogeneous mixed-type call args binding into distinct +// types per position (s64, f64, string). +// - `pack_arg_types` type-only binding for `inferExprType` +// pre-mono-scope: without it, the synthesized-ident detour +// loses the type because the scope isn't set up yet during +// return-type inference. + +#import "modules/std.sx"; + +foo :: (..$args) -> $R => args[2]; + +main :: () -> s32 { + a := foo(42, 3.2, "hello"); + print("{}\n", a); + return 0; +} diff --git a/examples/161-pack-index-oob.sx b/examples/161-pack-index-oob.sx new file mode 100644 index 0000000..043268f --- /dev/null +++ b/examples/161-pack-index-oob.sx @@ -0,0 +1,20 @@ +// Variadic heterogeneous type packs — out-of-bounds pack index +// is a compile-time error. +// +// `foo(..$args) -> $R => args[2]` accesses the third pack +// element. When called with fewer than 3 args, the literal index +// 2 is out of bounds for the pack's actual arity. The compiler +// detects this in `diagPackIndexOOB` and emits a focused +// diagnostic at the index span — pre-fix, the fall-through hit +// the standard slice-indexing path and produced "unresolved +// 'args'" which buried the real cause. + +#import "modules/std.sx"; + +foo :: (..$args) -> $R => args[2]; + +main :: () -> s32 { + n : s64 = foo(99); + print("{}\n", n); + return 0; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 9ff828a..1764827 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -170,6 +170,14 @@ pub const Lowering = struct { /// when no `args` slice is in scope (the mono path doesn't /// materialise the slice). pack_param_count: ?std.StringHashMap(u32) = null, + /// Type-only pack binding consulted by `inferExprType` for + /// `args[]` (parallel to `pack_arg_nodes` which carries the + /// AST substitution used at lowering time). Holds the concrete + /// call-site arg types in declaration order — same data the + /// mono's pack-param signature uses. Lets generic-`$R` return + /// inference resolve `args[i]` to the correct concrete type even + /// before the mono's scope is set up. + pack_arg_types: ?std.StringHashMap([]const TypeId) = null, struct_const_map: std.StringHashMap(StructConstInfo) = std.StringHashMap(StructConstInfo).init(std.heap.page_allocator), // "Struct.CONST" → value info module_const_map: std.StringHashMap(ModuleConstInfo) = std.StringHashMap(ModuleConstInfo).init(std.heap.page_allocator), // module-level value constants (e.g. AF_INET :s32: 2) foreign_name_map: std.StringHashMap([]const u8) = std.StringHashMap([]const u8).init(std.heap.page_allocator), // sx name → C name for #foreign renames @@ -4238,6 +4246,14 @@ pub const Lowering = struct { if (self.packArgNodeAt(ie)) |arg_node| { return self.lowerExpr(arg_node); } + // Out-of-bounds pack indexing: object IS a pack name + index + // IS a comptime int literal but exceeds the pack arity. Emit + // a focused diagnostic so the user gets "pack index 2 out of + // bounds" instead of the generic "unresolved 'args'" that the + // fall-through scope-lookup would produce. + if (self.diagPackIndexOOB(ie)) { + return self.builder.constInt(0, .s64); + } const obj = self.lowerExpr(ie.object); const idx = self.lowerExpr(ie.index); // Infer element type from the object's slice/array type @@ -4246,6 +4262,27 @@ pub const Lowering = struct { return self.builder.emit(.{ .index_get = .{ .lhs = obj, .rhs = idx } }, elem_ty); } + /// Detect `[]` where the literal exceeds + /// the pack arity (or is negative). Emits a diagnostic and + /// returns true; caller skips the standard indexing path and + /// returns a placeholder Ref. Returns false for non-pack bases, + /// non-literal indices, or in-range indices. + fn diagPackIndexOOB(self: *Lowering, ie: *const ast.IndexExpr) bool { + const ppc = self.pack_param_count orelse return false; + if (ie.object.data != .identifier) return false; + const pack_name = ie.object.data.identifier.name; + const n = ppc.get(pack_name) orelse return false; + if (ie.index.data != .int_literal) return false; + const raw: i64 = ie.index.data.int_literal.value; + if (raw >= 0 and @as(u32, @intCast(raw)) < n) return false; + if (self.diagnostics) |diags| { + diags.addFmt(.err, ie.index.span, "pack index {} out of bounds: '{s}' has {} element{s}", .{ + raw, pack_name, n, if (n == 1) @as([]const u8, "") else @as([]const u8, "s"), + }); + } + return true; + } + /// Returns the call-site arg AST node when `ie` matches /// `[]` with the pack name bound /// in the active `pack_arg_nodes` map and the index in range. @@ -8171,6 +8208,7 @@ pub const Lowering = struct { const saved_target = self.target_type; const saved_pan = self.pack_arg_nodes; const saved_ppc = self.pack_param_count; + const saved_pat = self.pack_arg_types; const saved_iri = self.inline_return_target; const saved_ctx_ref = self.current_ctx_ref; self.func_defer_base = self.defer_stack.items.len; @@ -8183,6 +8221,7 @@ pub const Lowering = struct { self.target_type = saved_target; self.pack_arg_nodes = saved_pan; self.pack_param_count = saved_ppc; + self.pack_arg_types = saved_pat; self.inline_return_target = saved_iri; self.current_ctx_ref = saved_ctx_ref; self.builder.func = saved_func; @@ -8223,8 +8262,12 @@ pub const Lowering = struct { var pre_ppc = std.StringHashMap(u32).init(self.alloc); defer pre_ppc.deinit(); pre_ppc.put(pack_name, @intCast(arg_types.len)) catch return; + var pre_pat = std.StringHashMap([]const TypeId).init(self.alloc); + defer pre_pat.deinit(); + pre_pat.put(pack_name, arg_types) catch return; self.pack_arg_nodes = pre_pan; self.pack_param_count = pre_ppc; + self.pack_arg_types = pre_pat; const declared_is_generic_ret = blk: { const rt = fd.return_type orelse break :blk false; @@ -11462,6 +11505,22 @@ pub const Lowering = struct { } }); }, .index_expr => |ie| { + // Pack-arg type lookup: `[]`. + // Read directly from `pack_arg_types` — bypasses the + // synthesized-ident detour in `pack_arg_nodes` which + // would otherwise lose the type when the mono's + // scope isn't set up yet (generic-`$R` pre-inference). + if (self.pack_arg_types) |pat| { + if (ie.object.data == .identifier and ie.index.data == .int_literal) { + if (pat.get(ie.object.data.identifier.name)) |arg_tys| { + const raw: i64 = ie.index.data.int_literal.value; + if (raw >= 0) { + const i: usize = @intCast(raw); + if (i < arg_tys.len) return arg_tys[i]; + } + } + } + } if (self.packArgNodeAt(&ie)) |arg_node| { return self.inferExprType(arg_node); } diff --git a/tests/expected/160-pack-hetero-ret.exit b/tests/expected/160-pack-hetero-ret.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/160-pack-hetero-ret.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/160-pack-hetero-ret.txt b/tests/expected/160-pack-hetero-ret.txt new file mode 100644 index 0000000..ce01362 --- /dev/null +++ b/tests/expected/160-pack-hetero-ret.txt @@ -0,0 +1 @@ +hello diff --git a/tests/expected/161-pack-index-oob.exit b/tests/expected/161-pack-index-oob.exit new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/tests/expected/161-pack-index-oob.exit @@ -0,0 +1 @@ +1 diff --git a/tests/expected/161-pack-index-oob.txt b/tests/expected/161-pack-index-oob.txt new file mode 100644 index 0000000..b0a0e58 --- /dev/null +++ b/tests/expected/161-pack-index-oob.txt @@ -0,0 +1 @@ +/Users/agra/projects/sx/examples/161-pack-index-oob.sx:14:32: error: pack index 2 out of bounds: 'args' has 1 element