ffi M5.A.next.4.3: $args[$i] in expression position — source construction

Final slice of the .type_tag activation. Sx code can now
construct Type values through the `$<pack>[<int_literal>]`
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 `$<ident>[<int_literal>]`
  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.
This commit is contained in:
agra
2026-05-27 18:52:41 +03:00
parent 55c72af68a
commit fd03b5812f
5 changed files with 144 additions and 0 deletions

View File

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

View File

@@ -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: `$<pack>[<lit>]` →
// `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)

View File

@@ -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: `$<pack_name>[<int_literal>]`.
// 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_name>' (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);

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,7 @@
s64
string
f64
got s64
got string
got bool
got other