ffi M5.A.next.2b.fu2.C: heterogeneous pack ret + OOB diagnostic

Two follow-on fixes for follow-up #2 (generic pack-fn return).

(1) `pack_arg_types` — a new type-only pack binding consulted by
`inferExprType` for `<pack_name>[<int_literal>]`. The earlier
`pack_arg_nodes`-via-synthesized-idents path lost the type
during return-type inference because the synthesized idents
("__pack_args_0" etc.) only resolve once the mono scope is set
up — but the inference runs BEFORE scope setup. Now
`monomorphizePackFn` installs `pack_arg_types[<pack>] =
arg_types` alongside the existing nodes/count maps, and
`inferExprType` consults it directly.

`foo(..$args) -> $R => args[2]` called as `foo(42, 3.2, "hello")`
now correctly returns "hello" (string) — the third element-
typed pick threads through inference to the mono ret_ty.

(2) `diagPackIndexOOB` — focused diagnostic for `args[<lit>]`
where the literal exceeds the pack arity. Pre-fix the
substitution returned null and the standard slice-indexing
fall-through emitted "unresolved args" — burying the real
cause. Now: "pack index 2 out of bounds: 'args' has 1
element" at the index span.

Tests:
- `examples/160-pack-hetero-ret.sx` — generic `$R` with non-
  zeroth heterogeneous pick (returns "hello").
- `examples/161-pack-index-oob.sx` — call passes 1 arg but
  body indexes args[2]; locks in the OOB diagnostic shape.

200/200 example tests + `zig build test` green.
This commit is contained in:
agra
2026-05-27 16:34:26 +03:00
parent c917f92509
commit 2e0b97aaa5
7 changed files with 108 additions and 0 deletions

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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[<lit>]` (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 `<pack_name>[<int_literal>]` 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
/// `<pack_name>[<comptime_int_literal>]` 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: `<pack_name>[<int_literal>]`.
// 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);
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
hello

View File

@@ -0,0 +1 @@
1

View File

@@ -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