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

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