From d99c0fdb2b1fd7a2822050235921fc332c93333b Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 27 May 2026 19:19:32 +0300 Subject: [PATCH] ffi M5.A.next.4A.bare.4.B: tryLowerReflectionCall splits static vs dynamic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix for the silent .s64 fall-through in `type_name()`. `tryLowerReflectionCall` now splits on `isStaticTypeArg(node)`: - Static (type_expr / identifier / pack_index_type_expr / pointer / array / slice / optional / many_pointer / function_type_expr / tuple_literal / call) → fold to const_string at lower time (today's fast path). - Dynamic (index_expr, field_access, runtime locals, anything else) → emit `callBuiltin(.type_name, [arg_ref])`. The interp's arm (commit 9600ba5) reads the runtime `.type_tag` Value and returns the per-position name. `isStaticTypeArg(node)` is a new helper mirroring the explicit arms of `resolveTypeArg`. Lives alongside resolveTypeArg in lower.zig; documented to track shape changes together. emit_llvm: the comptime reflection builtins (`type_name`, `type_eq`, `has_impl`) now emit a silent undef-i64 placeholder. Same reasoning as 4A.bare.1.B's relaxation of const_type's emit_llvm arm: the JIT compiles the containing fn module-wide even if main never calls it, so emit-time noise here is just dead-from-main's-perspective code. Real misuse — passing a non- Type value to one of these — is caught by the interp arm's `asTypeId orelse bailDetail`. `examples/171-pack-dynamic-type-name.sx` flips from "s64s64" (silent .s64 fold per element) to "s64string" (per-position correct via interp arm). Test runs `walk(42, "hi")` at `#run` time so the dynamic path executes in the interp. 211/211 example tests + zig build test green. --- examples/171-pack-dynamic-type-name.sx | 30 ++++++---- src/ir/emit_llvm.zig | 20 ++++--- src/ir/lower.zig | 60 +++++++++++++++++-- tests/expected/171-pack-dynamic-type-name.txt | 2 + 4 files changed, 85 insertions(+), 27 deletions(-) diff --git a/examples/171-pack-dynamic-type-name.sx b/examples/171-pack-dynamic-type-name.sx index 174ef4a..dde0e8d 100644 --- a/examples/171-pack-dynamic-type-name.sx +++ b/examples/171-pack-dynamic-type-name.sx @@ -1,21 +1,23 @@ // Variadic heterogeneous type packs — step 4A final-slice // follow-up. `type_name()` where the argument is // NOT a static type expression (e.g. `list[i]` indexing into -// a `$args`-derived `[]Type` slice) silently folds to "s64" -// today because `resolveTypeArg`'s index_expr fall-through -// returns `.s64`. That's exactly the kind of silent unimplemented -// arm the CLAUDE.md REJECTED PATTERNS section forbids. +// a `$args`-derived `[]Type` slice) silently folded to "s64" +// because `resolveTypeArg`'s catch-all `else => .s64` lied — +// the kind of silent unimplemented arm the project's REJECTED +// PATTERNS forbid. // -// Next commit teaches `tryLowerReflectionCall` to detect "arg -// not statically resolvable" and emit a `builtin_call` to the -// new `.type_name` builtin. The interp's arm (already wired in -// commit 9600ba5) reads the runtime `.type_tag` Value and -// returns the per-position concrete type name. +// The fix: `tryLowerReflectionCall` now splits static vs +// dynamic args via `isStaticTypeArg(node)`. Static → fold to +// const_string at lower time (today's fast path). Dynamic → +// emit `callBuiltin(.type_name, [arg_ref])` for the interp's +// runtime arm to handle. // -// Expected output after fix: -// s64string +// Type values are comptime-only — the dynamic path only works +// inside a comptime context (`#run` / `#insert`). The test +// runs `walk(42, "hi")` at `#run` time and prints the result. #import "modules/std.sx"; +#import "modules/compiler.sx"; walk :: (..$args) -> string { list := $args; @@ -28,7 +30,9 @@ walk :: (..$args) -> string { return s; } -main :: () -> s32 { +show :: () { print("{}\n", walk(42, "hi")); - return 0; } +#run show(); + +main :: () { print("rt\n"); } diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 3dada61..64bc568 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -2968,15 +2968,17 @@ pub const LLVMEmitter = struct { self.advanceRefCounter(); }, .type_name, .type_eq, .has_impl => { - // Comptime-only reflection builtins. Reaching - // LLVM emit means lowering DIDN'T fold the call - // (static-arg fast path) AND lowering emitted a - // real `builtin_call` — but the resulting IR - // shouldn't survive past the comptime interp - // that's supposed to consume it. Loud failure: - // log + undef placeholder so the LLVM verifier - // catches downstream use. - std.debug.print("emit_llvm: comptime reflection builtin '{s}' reached runtime emit — Type values are interp-only.\n", .{@tagName(bi.builtin)}); + // Comptime-only reflection builtins. When the + // lowering split between static-fold and + // dynamic-builtin-call routes a call to one of + // these, the call site is intended for the + // interp's arm. LLVM still compiles the + // containing fn (the JIT module-wide) even if + // main never calls it — so this op DOES reach + // emit, just in dead-from-main's-perspective + // code. Silent undef-i64 placeholder is the + // right answer; the interp's arm catches real + // misuse via `asTypeId orelse bailDetail`. self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty))); }, else => { diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 3f25c96..6eb1b2e 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -8838,11 +8838,28 @@ pub const Lowering = struct { return self.builder.constInt(count, .s64); } if (std.mem.eql(u8, name, "type_name")) { - // type_name(T) → const_string("TypeName") - const ty = self.resolveTypeArg(c.args[0]); - const tn_str = self.formatTypeName(ty); - const sid = self.module.types.internString(tn_str); - return self.builder.constString(sid); + // type_name(T): + // - Statically resolvable arg (type expression, pack + // index, generic binding, etc.) → fold to const_string + // at lower time. + // - Dynamic arg (e.g. `list[i]` indexing into a + // `$args`-derived []Type slice) → emit a + // `callBuiltin(.type_name, [arg_ref])`. The interp's + // arm (commit 9600ba5) reads the runtime `.type_tag` + // and returns the per-position name. Without this + // split, the catch-all `else => .s64` in + // `resolveTypeArg` silently returns "s64" for every + // dynamic call — exactly the silent-arm pattern the + // project's REJECTED PATTERNS forbid. + if (isStaticTypeArg(c.args[0])) { + const ty = self.resolveTypeArg(c.args[0]); + const tn_str = self.formatTypeName(ty); + const sid = self.module.types.internString(tn_str); + return self.builder.constString(sid); + } + const arg_ref = self.lowerExpr(c.args[0]); + const args_owned = self.alloc.dupe(Ref, &.{arg_ref}) catch return self.builder.constString(self.module.types.internString("")); + return self.builder.callBuiltin(.type_name, args_owned, .string); } if (std.mem.eql(u8, name, "type_eq")) { // type_eq(T1, T2) → const_bool — comptime TypeId equality. @@ -8965,6 +8982,39 @@ pub const Lowering = struct { /// - Type param bindings ($T → concrete type via type_bindings) /// - Direct type names (Vec4 → lookup in TypeTable) /// - type_expr AST nodes + /// True iff `node` matches an AST shape that `resolveTypeArg` + /// can resolve to a concrete TypeId without falling through to + /// the silent `.s64` default. Used by `tryLowerReflectionCall` + /// to split static-fold from dynamic-builtin-call paths. + /// + /// Static-arg shapes mirror the explicit arms of `resolveTypeArg`: + /// - type_expr / identifier (type name or bound generic) + /// - pack_index_type_expr (`$pack[]`) + /// - compound type literals (pointer, array, slice, optional, + /// many_pointer, function_type_expr) + /// - parameterised type-constructor `call` (Vector, List, etc.) + /// - tuple_literal as a tuple TYPE + /// + /// Dynamic shapes (index_expr, field_access, runtime locals, + /// etc.) fall to the alternative path that emits a builtin_call. + fn isStaticTypeArg(node: *const Node) bool { + return switch (node.data) { + .type_expr, + .identifier, + .pack_index_type_expr, + .pointer_type_expr, + .many_pointer_type_expr, + .array_type_expr, + .slice_type_expr, + .optional_type_expr, + .function_type_expr, + .tuple_literal, + .call, + => true, + else => false, + }; + } + 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 diff --git a/tests/expected/171-pack-dynamic-type-name.txt b/tests/expected/171-pack-dynamic-type-name.txt index d3a2664..1a7ebe4 100644 --- a/tests/expected/171-pack-dynamic-type-name.txt +++ b/tests/expected/171-pack-dynamic-type-name.txt @@ -1 +1,3 @@ s64string +--- build done --- +rt