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:
242
src/ir/lower.zig
242
src/ir/lower.zig
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user