From fd03b5812fa96a35a8404fbf93ee7e520bfb2b3f Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 27 May 2026 18:52:41 +0300 Subject: [PATCH] =?UTF-8?q?ffi=20M5.A.next.4.3:=20$args[$i]=20in=20express?= =?UTF-8?q?ion=20position=20=E2=80=94=20source=20construction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final slice of the .type_tag activation. Sx code can now construct Type values through the `$[]` syntax in expression position. Lowering emits the new `const_type(TypeId)` opcode; the interp materialises `Value.type_tag(TypeId)`; reflection intrinsics + cmp_eq read it kind-honestly. Plumbing: - src/parser.zig: `parsePrimary` accepts `$[]` at the front of every expression. Emits a `pack_index_type_expr` AST node — same node already used in TYPE positions in step 3, now extended to expression positions. - src/ir/lower.zig: two places teach the new node. - `lowerExpr` arm: looks up `pack_arg_types[name][index]`, emits `builder.constType(arg_tys[index])`. OOB / no-binding paths emit a focused diagnostic + a `constType(.void)` placeholder (loud failure preserves silent-error budget). - `resolveTypeArg` arm: the same lookup, but returns the TypeId directly. Used by the lower-time fast paths in `tryLowerReflectionCall` + `tryConstBoolCondition` so `type_name($args[0])`, `type_eq($args[0], s64)`, and `has_impl(...)` all see the bound TypeId rather than falling through to the `.s64` default that the silent-arm rule forbids. The two arms ensure both runtime AND compile-time paths use the same source-of-truth (`pack_arg_types`), so per-mono dispatch via `inline if type_eq($args[0], s64) { ... }` folds at compile time as expected. `examples/169-pack-value-dispatch.sx` exercises both shapes: - `type_name($args[0])` returns the per-mono concrete type name ("s64", "string", "f64"). - `inline if type_eq($args[0], s64) { ... }` ladder dispatches per-mono ("got s64", "got string", "got bool", "got other"). 209/209 example tests + `zig build test` green. What's now possible end-to-end: show :: (..$args) -> string => type_name($args[0]); show(42) // "s64" show("hi") // "string" describe :: (..$args) -> string { inline if type_eq($args[0], s64) { return "got s64"; } ... } The "by the book" activation is complete: - foundation (const_type opcode, interp variant, helpers) — 4.0 - interp reflection arms (type_name / type_eq / has_impl) — 4.1 - box_any/display audit + bitcast guard — 4.2 - source-language construction via $args[$i] — 4.3 Step 5 (generic Into(Block) impl in stdlib) is now fully unblocked — its trampoline body can interpolate per-mono types both in type positions AND in expression positions. --- examples/169-pack-value-dispatch.sx | 44 ++++++++++++++++ src/ir/lower.zig | 58 +++++++++++++++++++++ src/parser.zig | 34 ++++++++++++ tests/expected/169-pack-value-dispatch.exit | 1 + tests/expected/169-pack-value-dispatch.txt | 7 +++ 5 files changed, 144 insertions(+) create mode 100644 examples/169-pack-value-dispatch.sx create mode 100644 tests/expected/169-pack-value-dispatch.exit create mode 100644 tests/expected/169-pack-value-dispatch.txt diff --git a/examples/169-pack-value-dispatch.sx b/examples/169-pack-value-dispatch.sx new file mode 100644 index 0000000..9e6c78b --- /dev/null +++ b/examples/169-pack-value-dispatch.sx @@ -0,0 +1,44 @@ +// Variadic heterogeneous type packs — step 4 source-construction +// path: `$args[$i]` in expression position yields a comptime Type +// VALUE (`Value.type_tag(TypeId)` in the interp). Lets builders + +// pack-fn bodies dispatch on the i-th pack type at compile time. +// +// Two usage shapes covered here: +// +// 1. `type_name($args[0])` — the value-form Type goes through the +// reflection intrinsic, picking up the per-mono concrete type. +// +// 2. `inline if type_eq($args[0], s64) { ... }` — compile-time +// branch over the pack element's type. The fold via +// `tryConstBoolCondition` reads $args[0] via `resolveTypeArg`, +// which the step-4 work taught to walk `pack_arg_types`. +// +// Lowering: `$args[$i]` in expression position emits a +// `const_type(arg_types[i])` IR op. The interp materialises a +// `Value.type_tag(TypeId)`. LLVM emit bails (Type is comptime-only). + +#import "modules/std.sx"; + +show :: (..$args) -> string => type_name($args[0]); + +describe :: (..$args) -> string { + inline if type_eq($args[0], s64) { return "got s64"; } + inline if type_eq($args[0], string) { return "got string"; } + inline if type_eq($args[0], bool) { return "got bool"; } + return "got other"; +} + +main :: () -> s32 { + // type_name picks up the per-mono first-arg type. + print("{}\n", show(42)); // s64 + print("{}\n", show("hi")); // string + print("{}\n", show(3.14)); // f64 + + // inline-if + type_eq picks the right branch per mono. + print("{}\n", describe(42)); // got s64 + print("{}\n", describe("hello")); // got string + print("{}\n", describe(true)); // got bool + print("{}\n", describe(3.14)); // got other + + return 0; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 373f47e..afcd52f 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -2028,6 +2028,35 @@ pub const Lowering = struct { fn lowerExpr(self: *Lowering, node: *const Node) Ref { return switch (node.data) { + // Pack-index in expression position: `$[]` → + // `const_type(arg_types[index])`. Yields a comptime-only + // Type value (`Value.type_tag(TypeId)` in the interp). + // OOB / no-active-pack-binding → focused diagnostic; the + // emitted Ref is a const_type(.void) placeholder so the + // verifier downstream catches misuse rather than silently + // succeeding with .void. + .pack_index_type_expr => |pi| blk: { + if (self.pack_arg_types) |pat| { + if (pat.get(pi.pack_name)) |arg_tys| { + if (pi.index < arg_tys.len) { + break :blk self.builder.constType(arg_tys[pi.index]); + } + if (self.diagnostics) |diags| { + diags.addFmt(.err, node.span, "pack-index value ${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"), + }); + } + break :blk self.builder.constType(.void); + } + } + if (self.diagnostics) |diags| { + diags.addFmt(.err, node.span, "pack-index value ${s}[{}] used outside an active pack binding", .{ + pi.pack_name, pi.index, + }); + } + break :blk self.builder.constType(.void); + }, .int_literal => |lit| { // If target is a float type, emit as float literal if (self.target_type) |tt| { @@ -8872,6 +8901,35 @@ pub const Lowering = struct { /// - Direct type names (Vec4 → lookup in TypeTable) /// - type_expr AST nodes fn resolveTypeArg(self: *Lowering, node: *const Node) TypeId { + // Pack-index access in a type-arg slot (e.g. `type_name($args[0])` + // or `type_eq($args[i], s64)`). Same shape as the + // `resolveTypeWithBindings` arm — looks up the bound pack types + // and returns the i-th. OOB and no-active-binding emit focused + // diagnostics rather than silently defaulting to .s64 (the + // catch-all `else` below) — that fall-through is exactly the + // "silent unimplemented arm" the project's REJECTED PATTERNS + // forbid. + 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 ${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 .void; + } + } + if (self.diagnostics) |diags| { + diags.addFmt(.err, node.span, "pack-index ${s}[{}] used outside an active pack binding", .{ + pi.pack_name, pi.index, + }); + } + return .void; + } switch (node.data) { .identifier => |id| { // Check type bindings first (from generic monomorphization) diff --git a/src/parser.zig b/src/parser.zig index 4c2b872..6d1d957 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -2293,6 +2293,40 @@ pub const Parser = struct { fn parsePrimary(self: *Parser) anyerror!*Node { const start = self.current.loc.start; + // Pack-index in expression position: `$[]`. + // Yields a `pack_index_type_expr` AST node (same shape used in + // type positions in step 3) — lowering resolves it to a + // `const_type(TypeId)` value at the bound pack-arg type. Step 3 + // already handles the same node in type positions; this arm + // extends it to value expressions, completing the round trip + // for builders that pass Type values to `type_name`/`type_eq` + // /etc at interp time. + if (self.current.tag == .dollar) { + self.advance(); + if (self.current.tag != .identifier) { + return self.fail("expected pack name after '$'"); + } + const pname = self.tokenSlice(self.current); + self.advance(); + if (self.current.tag != .l_bracket) { + return self.fail("expected '[' after '$' (pack indexing requires a literal index in expression position)"); + } + 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 = pname, + .index = @intCast(idx_val), + } }); + } switch (self.current.tag) { .int_literal => { const text = self.tokenSlice(self.current); diff --git a/tests/expected/169-pack-value-dispatch.exit b/tests/expected/169-pack-value-dispatch.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/169-pack-value-dispatch.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/169-pack-value-dispatch.txt b/tests/expected/169-pack-value-dispatch.txt new file mode 100644 index 0000000..e468731 --- /dev/null +++ b/tests/expected/169-pack-value-dispatch.txt @@ -0,0 +1,7 @@ +s64 +string +f64 +got s64 +got string +got bool +got other