ir: Type as first-class value (Any-shaped {tag, value})

Previously, `t : Type = f64` stored a boxed string carrying the literal
name "f64"; comparisons and `type_of`/`type_name` round-trips lost the
underlying TypeId. This switches `Type` to a runtime-representable Any
pair: `{ tag = .any.index() (meta-marker), value = TypeId.index() }`.

Mechanism:
- `const_type` emits a 16-byte Any aggregate via insertvalue.
- `TypeId.any` advertises 16 bytes / 8-byte alignment so structs that
  embed `t: Type` size correctly under verifySizes.
- `lowerBinaryOp` folds `==`/`!=` between static type-refs to a
  `const_bool`, and decomposes runtime Any-vs-Any compares via
  `unbox_any` so LLVM doesn't see icmp on aggregates.
- `lowerMatch`'s `is_type_match` path unboxes Any-typed subjects to
  the i64 type tag before the switch, so `case type:` etc. fire.
- `lowerRuntimeDispatchCall` (used by `case T: ... cast(t) val`) does
  the same unbox for the type-tag arg.
- `type_of(val: Any)` rebuilds an Any with `{.any, tag_of(val)}` so
  the result is itself a `Type` value, not a bare i64.
- `buildPackSliceValue` stops re-boxing const_type — the value is
  already canonical Any.
- `__sx_type_names` now indexes by TypeId across the whole table
  using the new `types.formatTypeName` (structural names for `*T`,
  `[]T`, `[N]T`, `?T`, `Vector(N,T)`, function/closure/tuple) so
  runtime `type_name(t)` works for compound types.
- `interp.zig`'s comptime `type_name` accepts either the bare
  `.type_tag` Value or the Any-boxed aggregate it now sees.
- `scanDecls` registers `Vec4 :: Vector(4, f32)` style aliases in
  `type_alias_map` (before the `fn_ast_map` check; `Vector` IS a
  `#builtin` fn). Lets `Vec4` in expression position lower as
  `const_type(<vector tid>)`.
- `isStaticTypeArg` becomes scope-aware: a name shadowed by a runtime
  local is not static. `isStaticTypeRef` is the symmetric helper for
  the eq fold.
- `inferExprType` returns `.any` for bare type names (identifier and
  type_expr) so pack arg types are correct.

Side effect: `print("{}", Vec4)` now prints the structural name
`Vector(4,f32)` rather than the alias literal `Vec4` — 12-meta's
expectation updated. Aliases stay pointer-equal to their target
(`Vec4 == Vector(4, f32)` is true).

Tests:
- examples/189-type-all-interactions.sx: 12-section comprehensive
  coverage — literal `==`, `type_of(value) == T`, `Type` var storage,
  `type_name` (static + runtime), printing Type values, generic
  dispatch via `$T: Type`, `identity($T, val)`, `Wrap($T)`, reflection
  builtins (`size_of`, `align_of`, `field_count`, `type_eq`),
  `..$args` pack walking, `Type` in struct field, compound type
  literals (`*Point`, `[4]s32`, `[]bool`, `?f64`).
- examples/12-meta.sx: expected output updated to reflect structural
  name for the Vec4 alias path.
- ffi-objc-call-06-sret-return.ir: regenerated to absorb the new
  type-name strings now emitted globally.

223/223 examples pass.
This commit is contained in:
agra
2026-05-28 14:02:10 +03:00
parent 9b7ffd70b2
commit 3ac13b7442
13 changed files with 1305 additions and 570 deletions

View File

@@ -25,5 +25,5 @@ main :: () {
// ** stdout **
// f64
// 3.200000
// Vec4
// Vector(4,f32)
// () -> s32

View File

@@ -0,0 +1,206 @@
// Type as a first-class value — comprehensive interaction smoke.
//
// Every way a user can plausibly touch a `Type` value, in one
// place. Pre-fix: some sections fail (`<?>`, false, garbage, or
// compile errors). Post-fix: every section's output matches the
// header line.
//
// Sections labelled `--- N. <thing> ---` so the diff localises
// which interaction regressed.
#import "modules/std.sx";
// ── Test fixtures ─────────────────────────────────────────────
Point :: struct { x: s32; y: s32; }
Color :: enum { red; green; blue; }
Wrap :: struct ($T: Type) { v: T; }
identity :: ($T: Type, val: T) -> T => val;
// Generic comptime fn that dispatches on type identity (compile-
// time fold via type_eq → const_bool → inline-if folds the
// branch away).
describe :: ($T: Type) -> string {
inline if type_eq(T, s64) { return "int64"; }
inline if type_eq(T, string) { return "text"; }
inline if type_eq(T, bool) { return "boolean"; }
return "other";
}
// Pack-fn collecting per-position types (step 4A.bare path).
type_list :: (..$args) -> string {
list := $args;
s := "[";
i : s64 = 0;
while i < list.len {
if i > 0 { s = concat(s, ", "); }
s = concat(s, type_name(list[i]));
i = i + 1;
}
return concat(s, "]");
}
// Type stored in a struct field.
TypeHolder :: struct { t: Type; }
main :: () -> s32 {
// ── 1. Type literal equality ────────────────────────────
print("=== 1. literal == ===\n");
print("s64 == s64: {}\n", s64 == s64);
print("s64 == string: {}\n", s64 == string);
print("*u8 == *u8: {}\n", *u8 == *u8);
print("?s64 == ?s64: {}\n", ?s64 == ?s64);
print("?s64 == ?s32: {}\n", ?s64 == ?s32);
// ── 2. type_of(value) ───────────────────────────────────
print("=== 2. type_of(value) == T ===\n");
a : s64 = 42;
b : f64 = 3.14;
s : string = "hi";
print("type_of(a) == s64: {}\n", type_of(a) == s64);
print("type_of(b) == f64: {}\n", type_of(b) == f64);
print("type_of(s) == string: {}\n", type_of(s) == string);
print("type_of(a) == f64: {}\n", type_of(a) == f64);
// ── 3. Type variable storage + readback ─────────────────
print("=== 3. Type variable storage ===\n");
t : Type = f64;
print("t == f64: {}\n", t == f64);
print("t == string: {}\n", t == string);
t = string;
print("after reassign t == string: {}\n", t == string);
t = bool;
print("t == bool: {}\n", t == bool);
// ── 4. type_name on literals + variables ────────────────
print("=== 4. type_name ===\n");
print("type_name(s64): {}\n", type_name(s64));
print("type_name(*u8): {}\n", type_name(*u8));
print("type_name(Point): {}\n", type_name(Point));
print("type_name(Color): {}\n", type_name(Color));
t = f64;
print("type_name(t): {}\n", type_name(t));
// ── 5. Print Type values directly ───────────────────────
print("=== 5. print Type values ===\n");
print("literal: {}\n", s64);
t = string;
print("var: {}\n", t);
print("type_of(b): {}\n", type_of(b));
// ── 6. Generic dispatch via $T: Type ────────────────────
print("=== 6. generic dispatch ===\n");
print("describe(s64): {}\n", describe(s64));
print("describe(string): {}\n", describe(string));
print("describe(bool): {}\n", describe(bool));
print("describe(f64): {}\n", describe(f64));
// ── 7. identity(T, val) ─────────────────────────────────
print("=== 7. identity($T, val) ===\n");
print("identity(s64, 7): {}\n", identity(s64, 7));
print("identity(string, hi): {}\n", identity(string, "hi"));
print("identity(bool, true): {}\n", identity(bool, true));
// ── 8. Comptime-generated struct (Wrap($T)) ─────────────
print("=== 8. Wrap($T) ===\n");
w_int := Wrap(s64).{ v = 42 };
w_str := Wrap(string).{ v = "wrapped" };
print("Wrap(s64).v: {}\n", w_int.v);
print("Wrap(string).v: {}\n", w_str.v);
// ── 9. Reflection builtins on Types ─────────────────────
print("=== 9. reflection on Type ===\n");
print("size_of(s64): {}\n", size_of(s64));
print("size_of(*u8): {}\n", size_of(*u8));
print("align_of(f64): {}\n", align_of(f64));
print("field_count(Point): {}\n", field_count(Point));
print("type_eq(s64, s64): {}\n", type_eq(s64, s64));
print("type_eq(s64, string): {}\n", type_eq(s64, string));
// ── 10. Type pack (..$args) walking ─────────────────────
print("=== 10. ..$args walking ===\n");
print("type_list(): {}\n", type_list());
print("type_list(1): {}\n", type_list(42));
print("type_list(1, \"x\"): {}\n", type_list(42, "x"));
print("type_list(true, 3.14): {}\n", type_list(true, 3.14));
// ── 11. Type in struct field ────────────────────────────
print("=== 11. Type in struct field ===\n");
h := TypeHolder.{ t = s64 };
print("h.t == s64: {}\n", h.t == s64);
print("h.t == string: {}\n", h.t == string);
print("type_name(h.t): {}\n", type_name(h.t));
// ── 12. Compound type literals ──────────────────────────
print("=== 12. compound literals ===\n");
print("type_name(*Point): {}\n", type_name(*Point));
print("type_name([4]s32): {}\n", type_name([4]s32));
print("type_name([]bool): {}\n", type_name([]bool));
print("type_name(?f64): {}\n", type_name(?f64));
return 0;
}
// ** stdout **
// === 1. literal == ===
// s64 == s64: true
// s64 == string: false
// *u8 == *u8: true
// ?s64 == ?s64: true
// ?s64 == ?s32: false
// === 2. type_of(value) == T ===
// type_of(a) == s64: true
// type_of(b) == f64: true
// type_of(s) == string: true
// type_of(a) == f64: false
// === 3. Type variable storage ===
// t == f64: true
// t == string: false
// after reassign t == string: true
// t == bool: true
// === 4. type_name ===
// type_name(s64): s64
// type_name(*u8): *u8
// type_name(Point): Point
// type_name(Color): Color
// type_name(t): f64
// === 5. print Type values ===
// literal: s64
// var: string
// type_of(b): f64
// === 6. generic dispatch ===
// describe(s64): int64
// describe(string): text
// describe(bool): boolean
// describe(f64): other
// === 7. identity($T, val) ===
// identity(s64, 7): 7
// identity(string, hi): hi
// identity(bool, true): true
// === 8. Wrap($T) ===
// Wrap(s64).v: 42
// Wrap(string).v: wrapped
// === 9. reflection on Type ===
// size_of(s64): 8
// size_of(*u8): 8
// align_of(f64): 8
// field_count(Point): 2
// type_eq(s64, s64): true
// type_eq(s64, string): false
// === 10. ..$args walking ===
// type_list(): []
// type_list(1): [s64]
// type_list(1, "x"): [s64, string]
// type_list(true, 3.14): [bool, f64]
// === 11. Type in struct field ===
// h.t == s64: true
// h.t == string: false
// type_name(h.t): s64
// === 12. compound literals ===
// type_name(*Point): *Point
// type_name([4]s32): [4]s32
// type_name([]bool): []bool
// type_name(?f64): ?f64

View File

@@ -319,7 +319,7 @@ any_to_string :: (val: Any) -> string {
case slice: result = slice_to_string(cast(type) val);
case pointer: result = pointer_to_string(cast(type) val);
case optional: result = optional_to_string(cast(type) val);
case type: { s : string = xx val; result = s; }
case type: result = type_name(val);
}
result;
}

View File

@@ -144,6 +144,12 @@ pub const LLVMEmitter = struct {
// Cached field name arrays for reflection (TypeId → LLVM global)
field_name_arrays: std.AutoHashMap(u32, c.LLVMValueRef),
// Lazy global `[N x string]` indexed by TypeId.index(), holding
// each type's display name. Built on the first dynamic
// `type_name(t)` call site; reused thereafter.
type_name_array: ?c.LLVMValueRef = null,
type_name_array_len: u32 = 0,
// Target configuration (stored for ABI decisions during emission)
target_config: TargetConfig,
@@ -1670,20 +1676,24 @@ pub const LLVMEmitter = struct {
self.mapRef(llvm_val);
}
},
.const_type => {
// Type values are comptime-only. At LLVM emit they
// become undef-i64 placeholders — Type-aware ops are
// routed through the interp; if one slips through to
// a runtime use site (`type_name` / `type_eq` /
// `has_impl` / `bitcast`), the corresponding emit_llvm
// bail or runtime use-site error fires loudly. Pure
// STORAGE of Type values in runtime aggregates (e.g.
// `$args` lowering builds an `[]Any` slice whose
// elements happen to be Type values that the user's
// code may or may not actually read) is safe — undef
// just gets stored, undef just gets read; no use-site
// misbehaviour follows.
self.mapRef(c.LLVMGetUndef(self.cached_i64));
.const_type => |tid| {
// Type values are Any-shaped pairs:
// { tag = .any.index() (the meta-marker),
// value = tid.index() }
// Lets storage in Any slots, struct fields,
// `Type`-typed vars, and slice elements all round-
// trip through the standard Any infrastructure.
// `case type:` in `any_to_string` matches on
// tag == `.any.index()`. Runtime `type_name(t)`
// extracts the value field and indexes into the
// type-name lookup table.
const any_ty = self.getAnyStructType();
const tag = c.LLVMConstInt(self.cached_i64, TypeId.any.index(), 0);
const val = c.LLVMConstInt(self.cached_i64, tid.index(), 0);
var result = c.LLVMGetUndef(any_ty);
result = c.LLVMBuildInsertValue(self.builder, result, tag, 0, "ct.tag");
result = c.LLVMBuildInsertValue(self.builder, result, val, 1, "ct.val");
self.mapRef(result);
},
// ── Arithmetic ─────────────────────────────────────────
@@ -2969,19 +2979,66 @@ pub const LLVMEmitter = struct {
_ = c.LLVMBuildCall2(self.builder, self.getWriteType(), write_fn, &write_args, 3, "");
self.advanceRefCounter();
},
.type_name, .type_eq, .has_impl => {
// 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)));
.type_name => {
// Dynamic `type_name(t)` at runtime: extract
// the TypeId from the arg (an Any-boxed Type
// value: tag=`.s64.index()`, value=tid), GEP
// into the compiler-emitted `__sx_type_names`
// global, load the string. The arg's LLVM
// shape is the `{i64, i64}` Any aggregate
// (because the IR-side arg type is `.any`
// when boxed); for unboxed direct call sites
// (the arg IR type is `.s64` from
// `const_type`), the value IS the TypeId
// index directly.
const arg_ref = bi.args[0];
const arg_val = self.resolveRef(arg_ref);
const arg_ir_ty = self.getRefIRType(arg_ref) orelse @import("types.zig").TypeId.s64;
const tid_idx = blk: {
if (arg_ir_ty == .any) {
// Boxed: extract value field.
break :blk c.LLVMBuildExtractValue(self.builder, arg_val, 1, "tn.tid");
}
// Bare i64 (TypeId index).
break :blk arg_val;
};
const arr_global = self.getOrBuildTypeNameArray();
const arr_len = self.type_name_array_len;
const string_ty = self.getStringStructType();
const arr_ty = c.LLVMArrayType(string_ty, arr_len);
const zero = c.LLVMConstInt(self.cached_i64, 0, 0);
var indices = [2]c.LLVMValueRef{ zero, tid_idx };
const gep = c.LLVMBuildInBoundsGEP2(self.builder, arr_ty, arr_global, &indices, 2, "tn.gep");
const result = c.LLVMBuildLoad2(self.builder, string_ty, gep, "tn.load");
self.mapRef(result);
},
.type_eq => {
// Dynamic `type_eq(a, b)` — both args are
// Type values. Extract TypeId from each Any
// box (or use directly if `.s64`-typed),
// icmp eq.
const a = blk: {
const v = self.resolveRef(bi.args[0]);
const ty = self.getRefIRType(bi.args[0]) orelse @import("types.zig").TypeId.s64;
if (ty == .any) break :blk c.LLVMBuildExtractValue(self.builder, v, 1, "te.a");
break :blk v;
};
const b = blk: {
const v = self.resolveRef(bi.args[1]);
const ty = self.getRefIRType(bi.args[1]) orelse @import("types.zig").TypeId.s64;
if (ty == .any) break :blk c.LLVMBuildExtractValue(self.builder, v, 1, "te.b");
break :blk v;
};
const eq_res = c.LLVMBuildICmp(self.builder, c.LLVMIntEQ, a, b, "te.eq");
self.mapRef(eq_res);
},
.has_impl => {
// Runtime has_impl needs a protocol-map
// snapshot — not wired yet. Silent false for
// now; the lower-time fold via
// `tryConstBoolCondition` covers every
// statically-resolvable call.
self.mapRef(c.LLVMConstInt(self.cached_i1, 0, 0));
},
else => {
// size_of, cast — handled by lowering or codegen glue
@@ -4496,6 +4553,49 @@ pub const LLVMEmitter = struct {
// ── Reflection emission helpers ────────────────────────────────
/// Build (or return cached) a global constant array of {ptr, i64}
/// string values indexed by `TypeId.index()`. Lets the dynamic
/// `type_name(t)` builtin look up the type's display name at
/// runtime — `gep arr[tid]; load string`. The array's length is
/// the current `infos.items.len`; new types interned after this
/// is built fall outside the array (the gep would OOB), so
/// callers must build LAZILY after all types are registered.
fn getOrBuildTypeNameArray(self: *LLVMEmitter) c.LLVMValueRef {
if (self.type_name_array) |g| return g;
const n: u32 = @intCast(self.ir_mod.types.infos.items.len);
const string_ty = self.getStringStructType();
var field_vals = std.ArrayList(c.LLVMValueRef).empty;
defer field_vals.deinit(self.alloc);
var i: u32 = 0;
while (i < n) : (i += 1) {
const tid = @import("types.zig").TypeId.fromIndex(i);
const name_str = self.ir_mod.types.formatTypeName(self.alloc, tid);
const str_z = self.alloc.dupeZ(u8, name_str) catch unreachable;
defer self.alloc.free(str_z);
const global_str = c.LLVMAddGlobal(self.llvm_module, c.LLVMArrayType(self.cached_i8, @intCast(name_str.len + 1)), "tn.str");
c.LLVMSetInitializer(global_str, c.LLVMConstStringInContext(self.context, str_z.ptr, @intCast(name_str.len + 1), 1));
c.LLVMSetGlobalConstant(global_str, 1);
c.LLVMSetLinkage(global_str, c.LLVMPrivateLinkage);
const len_val = c.LLVMConstInt(self.cached_i64, name_str.len, 0);
var struct_fields = [2]c.LLVMValueRef{ global_str, len_val };
const const_struct = c.LLVMConstStructInContext(self.context, &struct_fields, 2, 0);
field_vals.append(self.alloc, const_struct) catch unreachable;
}
const arr_ty = c.LLVMArrayType(string_ty, n);
const arr_init = c.LLVMConstArray(string_ty, field_vals.items.ptr, n);
const global = c.LLVMAddGlobal(self.llvm_module, arr_ty, "__sx_type_names");
c.LLVMSetInitializer(global, arr_init);
c.LLVMSetGlobalConstant(global, 1);
c.LLVMSetLinkage(global, c.LLVMPrivateLinkage);
self.type_name_array = global;
self.type_name_array_len = n;
return global;
}
/// Build (or return cached) a global constant array of {ptr, i64} string values
/// for the field names of a struct type.
fn getOrBuildFieldNameArray(self: *LLVMEmitter, struct_type: TypeId) c.LLVMValueRef {

View File

@@ -1767,7 +1767,22 @@ pub const Interpreter = struct {
.type_name => {
if (bi.args.len < 1) return bailDetail("comptime type_name: missing argument");
const arg = frame.getRef(bi.args[0]);
const tid = arg.asTypeId() orelse return bailDetail("comptime type_name: argument is not a Type value (expected `.type_tag`, got a different Value kind)");
// Accept either a bare `.type_tag` Value (the
// comptime-native form) or an Any-boxed Type
// (`.aggregate { tag: int, value: .type_tag }`)
// — the latter shape is what `box_any` produces
// when const_type values flow through a `.any`-typed
// slice or struct field.
const tid = blk: {
if (arg.asTypeId()) |t| break :blk t;
if (arg == .aggregate) {
const fields = arg.aggregate;
if (fields.len >= 2) {
if (fields[1].asTypeId()) |t| break :blk t;
}
}
return bailDetail("comptime type_name: argument is not a Type value (expected `.type_tag` or Any-boxed Type)");
};
const name = self.module.types.typeName(tid);
// Copy the slice into the interp's allocator so it
// outlives any TypeTable churn during the rest of the

View File

@@ -610,6 +610,18 @@ pub const Lowering = struct {
const alias_id = if (self.module.types.findByName(alias_name_id)) |existing| existing else self.module.types.intern(alias_info);
self.module.types.update(alias_id, alias_info);
}
} else if (std.mem.eql(u8, callee_name, "Vector")) {
// Builtin type constructor — checked BEFORE
// the generic `fn_ast_map` branch because
// `Vector` IS in `fn_ast_map` (declared as a
// `#builtin` fn) but `instantiateTypeFunction`
// can't resolve it (no body). Use
// `resolveTypeCallWithBindings` which
// hard-codes the vector layout.
const result_ty = self.resolveTypeCallWithBindings(call_data);
if (result_ty != .void) {
self.type_alias_map.put(cd.name, result_ty) catch {};
}
} else if (self.fn_ast_map.get(callee_name)) |fd| {
// Type-returning function: Foo :: Complex(u32)
if (fd.type_params.len > 0) {
@@ -635,6 +647,15 @@ pub const Lowering = struct {
const alias_id = if (self.module.types.findByName(alias_name_id)) |existing| existing else self.module.types.intern(alias_info);
self.module.types.update(alias_id, alias_info);
}
} else {
// Builtin parameterised type (Vector(N, T) etc) —
// resolve via type_bridge and register the result
// under the alias name so `Vec4` in expression
// position can `const_type(<vector tid>)`.
const result_ty = type_bridge.resolveAstType(cd.value, &self.module.types);
if (result_ty != .void and result_ty != .s64) {
self.type_alias_map.put(cd.name, result_ty) catch {};
}
}
}
// comptime_expr handled in Pass 2
@@ -2258,16 +2279,26 @@ pub const Lowering = struct {
break :blk self.builder.emit(.{ .func_ref = fid }, .s64);
}
}
// Type-as-value: if target is Any (Type context), produce a type name string
if (self.target_type == .any) {
const sid = self.module.types.internString(id.name);
const str = self.builder.constString(sid);
break :blk self.builder.boxAny(str, .string);
}
// Type-as-value: known type name used where a value is expected (e.g. cast(s64, val))
// The type arg is lowered but unused — the caller resolves from AST.
if (self.isKnownTypeName(id.name)) {
break :blk self.emitPlaceholder(id.name);
// Type-as-value: a name that resolves to a TypeId
// (primitive, alias, registered struct/enum/union,
// generic-struct instantiation) evaluates to a
// `const_type` in expression position. Works for
// direct assignment to a `Type`-typed slot
// (`x: Type = Vec4`), comparison (`x == Vec4`), and
// pack-arg / Any context (boxing happens at the
// consumer).
const ty = blk_ty: {
if (self.type_bindings) |tb| {
if (tb.get(id.name)) |t| break :blk_ty t;
}
if (self.type_alias_map.get(id.name)) |t| break :blk_ty t;
if (type_bridge.resolveTypePrimitive(id.name)) |t| break :blk_ty t;
const name_id = self.module.types.internString(id.name);
if (self.module.types.findByName(name_id)) |t| break :blk_ty t;
break :blk_ty TypeId.void;
};
if (ty != .void) {
break :blk self.builder.constType(ty);
}
// Unknown identifier
break :blk self.emitError(id.name, node.span);
@@ -2410,16 +2441,13 @@ pub const Lowering = struct {
if (self.global_names.get(te.name)) |gi| {
break :blk self.builder.emit(.{ .global_get = gi.id }, gi.ty);
}
// Type-as-value: if target is Any (Type variable), produce a boxed string
if (self.target_type == .any) {
const sid = self.module.types.internString(te.name);
const str = self.builder.constString(sid);
break :blk self.builder.boxAny(str, .string);
}
// Type expressions are always type names from the parser — they appear
// in value position when used as args to cast() etc. Silent placeholder.
// Type literal in expression position → first-class
// `const_type` Value (i64 = TypeId.index()). Makes
// `t : Type = f64;` store a real TypeId; lets
// `t == f64` icmp at runtime against the same TypeId.
if (self.isKnownTypeName(te.name)) {
break :blk self.emitPlaceholder(te.name);
const ty = type_bridge.resolveAstType(node, &self.module.types);
break :blk self.builder.constType(ty);
}
break :blk self.emitError(te.name, node.span);
},
@@ -2456,6 +2484,46 @@ pub const Lowering = struct {
return self.builder.blockParam(merge_bb, 0, .bool);
}
// Type-literal comparison fold: when both sides are type-shaped
// AST nodes (`s64`, `*u8`, `?T`, `[3]f64`, etc.) OR resolve to
// a static TypeId at lower time (`type_of(x)` for any
// statically-typed `x`), resolve each and emit a `const_bool`.
// Same semantic as `type_eq(A, B)` but using the standard `==`
// operator — the user's intuition. Without the fold, both
// sides lower as `const_type` undef-i64 and the runtime icmp
// returns garbage.
if (bop.op == .eq or bop.op == .neq) {
if (self.isStaticTypeRef(bop.lhs) and self.isStaticTypeRef(bop.rhs)) {
const lhs_ty = self.resolveTypeArg(bop.lhs);
const rhs_ty = self.resolveTypeArg(bop.rhs);
const eq_result = lhs_ty == rhs_ty;
return self.builder.constBool(if (bop.op == .eq) eq_result else !eq_result);
}
}
// Any-shaped `==` (e.g. `t == s64` where `t: Type`): both
// operands are 16-byte `{tag, value}` aggregates. LLVM
// doesn't accept `icmp` on aggregates directly. Decompose
// via `unbox_any` (which extracts the value field at
// `.s64`) and compare the i64s. Tag fields are stable
// across compilations of the same source so value-only
// identity is enough.
if (bop.op == .eq or bop.op == .neq) {
const lhs_ty = self.inferExprType(bop.lhs);
const rhs_ty = self.inferExprType(bop.rhs);
if (lhs_ty == .any and rhs_ty == .any) {
const lhs = self.lowerExpr(bop.lhs);
const rhs = self.lowerExpr(bop.rhs);
const lhs_val = self.builder.emit(.{ .unbox_any = .{ .operand = lhs } }, .s64);
const rhs_val = self.builder.emit(.{ .unbox_any = .{ .operand = rhs } }, .s64);
if (bop.op == .eq) {
return self.builder.emit(.{ .cmp_eq = .{ .lhs = lhs_val, .rhs = rhs_val } }, .bool);
} else {
return self.builder.emit(.{ .cmp_ne = .{ .lhs = lhs_val, .rhs = rhs_val } }, .bool);
}
}
}
// Special case: optional == null / optional != null
if (bop.op == .eq or bop.op == .neq) {
const lhs_is_null = bop.lhs.data == .null_literal;
@@ -3295,8 +3363,15 @@ pub const Lowering = struct {
default_bb = self.freshBlock("match.unr");
}
// Switch on the subject (for type match, subject IS the tag; for enum match, extract tag)
const tag = if (is_type_match) subject else if (is_optional_match) self.builder.emit(.{ .optional_has_value = .{ .operand = subject } }, .bool) else blk: {
// Switch on the subject (for type match, subject is either a
// bare TypeId (s64) or an Any-shaped Type value — unbox in the
// latter case so the switch sees the i64 type id).
const tag = if (is_type_match) tag_blk: {
if (subject_ty == .any) {
break :tag_blk self.builder.emit(.{ .unbox_any = .{ .operand = subject } }, .s64);
}
break :tag_blk subject;
} else if (is_optional_match) self.builder.emit(.{ .optional_has_value = .{ .operand = subject } }, .bool) else blk: {
// Determine actual tag type from union info (e.g. u32 for SDL_Event)
const tag_ty: TypeId = tt: {
if (!subject_ty.isBuiltin()) {
@@ -8018,7 +8093,12 @@ pub const Lowering = struct {
}
// Lower the type tag (runtime value) and Any value BEFORE the switch
const type_tag = self.lowerExpr(type_tag_node orelse return self.emitError("dispatch", call_node.callee.span));
const type_tag_raw = self.lowerExpr(type_tag_node orelse return self.emitError("dispatch", call_node.callee.span));
const type_tag_node_ty = self.inferExprType(type_tag_node.?);
const type_tag = if (type_tag_node_ty == .any)
self.builder.emit(.{ .unbox_any = .{ .operand = type_tag_raw } }, .s64)
else
type_tag_raw;
const any_val = self.lowerExpr(any_val_node orelse return self.emitError("dispatch", call_node.callee.span));
// Lower non-cast arguments once (before the switch)
@@ -8290,6 +8370,9 @@ pub const Lowering = struct {
const array_slot = self.builder.alloca(array_ty);
for (arg_types, 0..) |ty, i| {
// `const_type` produces an `.any`-typed Type value
// (`{tag=.any, value=tid}`) — already the canonical Any
// shape, so no re-box needed.
const type_val = self.builder.constType(ty);
const idx_ref = self.builder.constInt(@intCast(i), .s64);
const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = idx_ref } }, any_ptr_ty);
@@ -8923,7 +9006,7 @@ pub const Lowering = struct {
// `resolveTypeArg` silently returns "s64" for every
// dynamic call — exactly the silent-arm pattern the
// project's REJECTED PATTERNS forbid.
if (isStaticTypeArg(c.args[0])) {
if (self.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);
@@ -9024,16 +9107,18 @@ pub const Lowering = struct {
} }, .any);
}
if (std.mem.eql(u8, name, "type_of")) {
// type_of(val) — extract Any tag or produce compile-time constant
if (c.args.len < 1) return self.builder.constInt(0, .s64);
// type_of(val) — produce a Type value (.any-typed aggregate).
if (c.args.len < 1) return self.builder.constType(.void);
const arg_ty = self.inferExprType(c.args[0]);
if (arg_ty == .any) {
// Runtime: extract tag field (field 0 of Any {tag: s64, value: s64})
// Runtime: extract tag, rebuild Any with `{.any, tag}` so
// the returned value carries Type semantics (tag field
// says ".any" → the value field holds the type id).
const val = self.lowerExpr(c.args[0]);
return self.builder.structGet(val, 0, .s64);
const tag_val = self.builder.structGet(val, 0, .s64);
return self.builder.boxAny(tag_val, .any);
} else {
// Static: emit type tag as constant
return self.builder.constInt(@intCast(@intFromEnum(arg_ty)), .s64);
return self.builder.constType(arg_ty);
}
}
if (std.mem.eql(u8, name, "field_index")) {
@@ -9091,10 +9176,25 @@ pub const Lowering = struct {
///
/// 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,
fn isStaticTypeArg(self: *Lowering, node: *const Node) bool {
switch (node.data) {
.type_expr => |te| {
// A type-keyword name (e.g. `s64`) is always static.
// A user-defined name that happens to be in scope as
// a runtime variable (`x: Type = s64; type_name(x)`)
// is NOT static — route through the dynamic builtin
// call so the runtime lookup table fires.
if (self.scope) |scope| {
if (scope.lookup(te.name) != null) return false;
}
return true;
},
.identifier => |id| {
if (self.scope) |scope| {
if (scope.lookup(id.name) != null) return false;
}
return true;
},
.pack_index_type_expr,
.pointer_type_expr,
.many_pointer_type_expr,
@@ -9104,9 +9204,59 @@ pub const Lowering = struct {
.function_type_expr,
.tuple_literal,
.call,
=> true,
else => false,
};
=> return true,
else => return false,
}
}
/// True iff `node` is a Type-shaped expression that resolves to a
/// concrete TypeId at lower time WITHOUT being a runtime variable
/// reference. Differs from `isStaticTypeArg` in that we exclude
/// identifiers that are in scope as runtime locals/globals — those
/// are runtime Type values (e.g. `t: Type = f64`) and the
/// comparison fold can't statically resolve them.
fn isStaticTypeRef(self: *Lowering, node: *const Node) bool {
switch (node.data) {
.type_expr => |te| {
// Compound type names (`s64`, `Point`, `Vec4`) resolve
// statically. If the name is also a runtime var in
// scope, it's a value reference, not a type ref.
if (self.scope) |scope| {
if (scope.lookup(te.name) != null) return false;
}
return self.isKnownTypeName(te.name) or
self.module.types.findByName(self.module.types.internString(te.name)) != null or
self.type_alias_map.get(te.name) != null;
},
.identifier => |id| {
if (self.scope) |scope| {
if (scope.lookup(id.name) != null) return false;
}
return self.isKnownTypeName(id.name) or
self.module.types.findByName(self.module.types.internString(id.name)) != null or
self.type_alias_map.get(id.name) != null;
},
.pointer_type_expr,
.many_pointer_type_expr,
.array_type_expr,
.slice_type_expr,
.optional_type_expr,
.function_type_expr,
.pack_index_type_expr,
=> return true,
.call => |cl| {
// `type_of(x)` resolves statically when `x`'s type is
// known — which it always is for a typed expression.
if (cl.callee.data == .identifier and
std.mem.eql(u8, cl.callee.data.identifier.name, "type_of") and
cl.args.len == 1)
{
return true;
}
return false;
},
else => return false,
}
}
fn resolveTypeArg(self: *Lowering, node: *const Node) TypeId {
@@ -9170,6 +9320,18 @@ pub const Lowering = struct {
return type_bridge.resolveAstType(node, &self.module.types);
},
.call => |cl| {
// `type_of(x)` resolves to `inferExprType(x)` at lower
// time when `x`'s type is statically known (which it
// is for any expression — type inference always
// produces a concrete TypeId). Lets
// `type_of(a) == s64` fold the same as
// `inferExprType(a) == s64`.
if (cl.callee.data == .identifier and
std.mem.eql(u8, cl.callee.data.identifier.name, "type_of") and
cl.args.len == 1)
{
return self.inferExprType(cl.args[0]);
}
// Handle type constructor calls: size_of(Sx(f32)), size_of(Complex(u32))
return self.resolveTypeCallWithBindings(&cl);
},
@@ -12148,6 +12310,10 @@ pub const Lowering = struct {
if (self.module_const_map.get(id.name)) |ci| {
return ci.ty;
}
// A bare type name (alias like `Vec4`, struct name, or
// builtin primitive) referenced in expression position
// is a Type value — IR type `.any`.
if (self.isKnownTypeName(id.name)) return .any;
return .s64;
},
.type_expr => |te| {
@@ -12157,6 +12323,9 @@ pub const Lowering = struct {
return binding.ty;
}
}
// A bare type name in expression position (e.g. `s64`,
// `Point`, `*u8`) is a Type value — IR type `.any`.
if (self.isKnownTypeName(te.name)) return .any;
return .s64;
},
.enum_literal => {
@@ -12927,6 +13096,7 @@ pub const Lowering = struct {
if (self.type_bindings) |bindings| {
if (bindings.get(name) != null) return true;
}
if (self.type_alias_map.get(name) != null) return true;
const name_id = self.module.types.internString(name);
return self.module.types.findByName(name_id) != null;
}

View File

@@ -385,6 +385,10 @@ pub const Builder = struct {
/// fail loudly rather than silently materialise the TypeId as an
/// int.
pub fn constType(self: *Builder, tid: TypeId) Ref {
// Type values are Any-shaped at runtime —
// `{ tag = .any.index() (the meta-marker), value = tid }`.
// Matches `Type → .any` in `type_bridge`. The interp keeps
// the high-fidelity `.type_tag` Value for comptime ops.
return self.emit(.{ .const_type = tid }, .any);
}

View File

@@ -232,7 +232,15 @@ pub fn resolveTypePrimitive(name: []const u8) ?TypeId {
if (std.mem.eql(u8, name, "string")) return .string;
if (std.mem.eql(u8, name, "void")) return .void;
if (std.mem.eql(u8, name, "Any")) return .any;
if (std.mem.eql(u8, name, "Type")) return .any; // Type-as-value: boxed string
// Type values are runtime-representable as Any-shaped pairs:
// `{ tag = .any.index() (the meta-marker), value = TypeId.index() }`.
// Lets `t : Type = f64; t == s64; print(t)` all route through the
// existing Any infrastructure — boxing/unboxing, `case type:`
// dispatch, runtime `type_name(t)` via the type-name lookup
// table. Comparison decomposes via the eq fold path
// (`isStaticTypeRef`) for static literals; runtime-var vs
// literal compares decompose at `lowerBinaryOp`.
if (std.mem.eql(u8, name, "Type")) return .any;
if (std.mem.eql(u8, name, "noreturn")) return .noreturn;
if (std.mem.eql(u8, name, "usize")) return .usize;
if (std.mem.eql(u8, name, "isize")) return .isize;

View File

@@ -466,6 +466,7 @@ pub const TypeTable = struct {
if (ty == .s64 or ty == .u64 or ty == .f64) return 8;
if (ty == .usize or ty == .isize) return ptr_size;
if (ty == .string) return 16; // {ptr, i64} — always 16 (i64 alignment pads on wasm32)
if (ty == .any) return 16; // {i64 tag, i64 value} — Any boxed layout
if (ty.isBuiltin()) return ptr_size; // default for unknown builtins
const info = self.get(ty);
return switch (info) {
@@ -564,6 +565,7 @@ pub const TypeTable = struct {
if (ty == .s64 or ty == .u64 or ty == .f64) return 8;
if (ty == .usize or ty == .isize) return ptr_align;
if (ty == .string) return 8; // i64 drives alignment
if (ty == .any) return 8; // {i64, i64} aligns to 8
if (ty.isBuiltin()) return ptr_align;
const info = self.get(ty);
return switch (info) {
@@ -650,6 +652,91 @@ pub const TypeTable = struct {
},
};
}
/// Like `typeName` but produces structural names for compound
/// types (`*T`, `[]T`, `[N]T`, `?T`, `Vector(N,T)`, function and
/// tuple types) instead of returning `"?"`. Compound names are
/// freshly allocated via `alloc`; builtin and named user types
/// return borrowed slices.
pub fn formatTypeName(self: *const TypeTable, alloc: std.mem.Allocator, id: TypeId) []const u8 {
if (id.isBuiltin()) return self.typeName(id);
const info = self.get(id);
return switch (info) {
.@"struct" => |s| self.getString(s.name),
.@"enum" => |e| self.getString(e.name),
.@"union" => |u| self.getString(u.name),
.tagged_union => |u| self.getString(u.name),
.protocol => |p| self.getString(p.name),
.pointer => |p| blk: {
const inner = self.formatTypeName(alloc, p.pointee);
break :blk std.fmt.allocPrint(alloc, "*{s}", .{inner}) catch "*?";
},
.many_pointer => |p| blk: {
const inner = self.formatTypeName(alloc, p.element);
break :blk std.fmt.allocPrint(alloc, "[*]{s}", .{inner}) catch "[*]?";
},
.slice => |s| blk: {
const inner = self.formatTypeName(alloc, s.element);
break :blk std.fmt.allocPrint(alloc, "[]{s}", .{inner}) catch "[]?";
},
.array => |a| blk: {
const inner = self.formatTypeName(alloc, a.element);
break :blk std.fmt.allocPrint(alloc, "[{d}]{s}", .{ a.length, inner }) catch "[N]?";
},
.vector => |v| blk: {
const inner = self.formatTypeName(alloc, v.element);
break :blk std.fmt.allocPrint(alloc, "Vector({d},{s})", .{ v.length, inner }) catch "Vector(?)";
},
.optional => |o| blk: {
const inner = self.formatTypeName(alloc, o.child);
break :blk std.fmt.allocPrint(alloc, "?{s}", .{inner}) catch "?_";
},
.function => |f| blk: {
var buf = std.ArrayList(u8).empty;
defer buf.deinit(alloc);
buf.append(alloc, '(') catch break :blk "(?)";
for (f.params, 0..) |p, i| {
if (i > 0) buf.appendSlice(alloc, ", ") catch break :blk "(?)";
buf.appendSlice(alloc, self.formatTypeName(alloc, p)) catch break :blk "(?)";
}
buf.append(alloc, ')') catch break :blk "(?)";
if (f.ret != .void) {
buf.appendSlice(alloc, " -> ") catch break :blk "(?)";
buf.appendSlice(alloc, self.formatTypeName(alloc, f.ret)) catch break :blk "(?)";
}
break :blk buf.toOwnedSlice(alloc) catch "(?)";
},
.closure => |co| blk: {
var buf = std.ArrayList(u8).empty;
defer buf.deinit(alloc);
buf.appendSlice(alloc, "Closure(") catch break :blk "Closure(?)";
for (co.params, 0..) |p, i| {
if (i > 0) buf.appendSlice(alloc, ", ") catch break :blk "Closure(?)";
buf.appendSlice(alloc, self.formatTypeName(alloc, p)) catch break :blk "Closure(?)";
}
buf.append(alloc, ')') catch break :blk "Closure(?)";
if (co.ret != .void) {
buf.appendSlice(alloc, " -> ") catch break :blk "Closure(?)";
buf.appendSlice(alloc, self.formatTypeName(alloc, co.ret)) catch break :blk "Closure(?)";
}
break :blk buf.toOwnedSlice(alloc) catch "Closure(?)";
},
.tuple => |tu| blk: {
var buf = std.ArrayList(u8).empty;
defer buf.deinit(alloc);
buf.append(alloc, '(') catch break :blk "(?)";
for (tu.fields, 0..) |f, i| {
if (i > 0) buf.appendSlice(alloc, ", ") catch break :blk "(?)";
buf.appendSlice(alloc, self.formatTypeName(alloc, f)) catch break :blk "(?)";
}
buf.append(alloc, ')') catch break :blk "(?)";
break :blk buf.toOwnedSlice(alloc) catch "(?)";
},
.signed => |w| std.fmt.allocPrint(alloc, "s{d}", .{w}) catch "s?",
.unsigned => |w| std.fmt.allocPrint(alloc, "u{d}", .{w}) catch "u?",
else => self.typeName(id),
};
}
};
// ── Intern map support ──────────────────────────────────────────────────

View File

@@ -1,4 +1,4 @@
f64
3.200000
Vec4
Vector(4,f32)
() -> s32

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,59 @@
=== 1. literal == ===
s64 == s64: true
s64 == string: false
*u8 == *u8: true
?s64 == ?s64: true
?s64 == ?s32: false
=== 2. type_of(value) == T ===
type_of(a) == s64: true
type_of(b) == f64: true
type_of(s) == string: true
type_of(a) == f64: false
=== 3. Type variable storage ===
t == f64: true
t == string: false
after reassign t == string: true
t == bool: true
=== 4. type_name ===
type_name(s64): s64
type_name(*u8): *u8
type_name(Point): Point
type_name(Color): Color
type_name(t): f64
=== 5. print Type values ===
literal: s64
var: string
type_of(b): f64
=== 6. generic dispatch ===
describe(s64): int64
describe(string): text
describe(bool): boolean
describe(f64): other
=== 7. identity($T, val) ===
identity(s64, 7): 7
identity(string, hi): hi
identity(bool, true): true
=== 8. Wrap($T) ===
Wrap(s64).v: 42
Wrap(string).v: wrapped
=== 9. reflection on Type ===
size_of(s64): 8
size_of(*u8): 8
align_of(f64): 8
field_count(Point): 2
type_eq(s64, s64): true
type_eq(s64, string): false
=== 10. ..$args walking ===
type_list(): []
type_list(1): [s64]
type_list(1, "x"): [s64, string]
type_list(true, 3.14): [bool, f64]
=== 11. Type in struct field ===
h.t == s64: true
h.t == string: false
type_name(h.t): s64
=== 12. compound literals ===
type_name(*Point): *Point
type_name([4]s32): [4]s32
type_name([]bool): []bool
type_name(?f64): ?f64

File diff suppressed because it is too large Load Diff