comptime VM: dedicated Type builtin TypeId (8B), distinct from .any — foundation (dead)

Add TypeId.type_value (slot 19) + matching TypeInfo.type_value variant: an
8-byte type handle, distinct from the 16-byte boxed .any. All types.zig layout
handlers wired (size/align 8, display "Type", hash/eql); toLLVMTypeInfo -> i64.

Reserve builtin headroom: first_user 19 -> 100 (slots 20-99 padded with the
unresolved tripwire) so future builtins don't renumber user TypeIds / churn
sx ir snapshots. 22 IR snapshots regenerated (pure renumber to 100-base).

type_resolver still returns .any for "Type" — nothing produces .type_value
yet, so no behavior change. 697/0 both gates.
This commit is contained in:
agra
2026-06-18 13:03:21 +03:00
parent 7d59b5eeb6
commit 6844fb90e7
25 changed files with 39319 additions and 37485 deletions

View File

@@ -335,6 +335,23 @@ when reached (sentinels or accessor fns; see the design doc Risks).
`List` growth; orthogonal, see `current/CHECKPOINT-METATYPE.md`.)
## Log
- **Phase 3 P3.4 step 2 (VM plan) — dedicated `Type` builtin TypeId: FOUNDATION landed (dead/additive) (2026-06-18).**
Added `TypeId.type_value` (slot 19) + a matching `TypeInfo.type_value` variant + the builtins
init entry — an **8-byte type handle distinct from the 16-byte boxed `.any`** (THE WALL). All
`types.zig` layout handlers wired: `sizeOf`/`typeSizeBytes` → 8, `typeAlignBytes` → 8,
`typeName` → "Type", `hashTypeInfo`/`typeInfoEql` no-payload arms. Only ONE exhaustive switch
needed a new arm (`backend/llvm/types.zig` `toLLVMTypeInfo` → `cached_i64`); every other
`switch(TypeInfo)` site has an `else` (audited when the resolver flips). **`first_user` 19 → 100**
(per the user): slots 2099 are RESERVED builtin headroom (infos padded with the `unresolved`
tripwire), so future builtins don't renumber user TypeIds / churn `sx ir` snapshots. Cost:
~80 default entries in each binary's per-type reflection arrays (user opted in). **Still dead:**
`type_resolver.zig:64` STILL returns `.any` for "Type" — nothing produces `.type_value` yet, so
NO behavior change. Regenerated 22 IR snapshots (pure TypeId renumber to 100-base; `git diff
--name-only` confirmed ONLY `.ir` files + the 2 source files changed — no stdout/stderr/exit).
**697/0 both gates** (OFF and `-Dcomptime-flat`). **Next:** flip `type_resolver:64` →
`.type_value`, then migrate the `.any` refs that mean "a Type value" (const_type result /
reflection returns / metatype `Type` params / `.type_tag` checks) — leave the real boxed-Any
refs — file-by-file with a build after each.
- **Phase 3 P3.4 step 1 (VM plan) — lowering-time default context; first blocker cleared (2026-06-18).**
`materializeDefaultContext` now falls back to a ZEROED `Context` (found by name) when the
`__sx_default_context` global is absent — i.e. at LOWERING time, where the global isn't

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -97,6 +97,10 @@ pub const TypeLowering = struct {
return c.LLVMVectorType(elem, vec.length);
},
.any => self.e.getAnyStructType(),
// A comptime `Type` value is an 8-byte type handle (a `TypeId` in a
// word), distinct from the 16-byte boxed Any. It is comptime-only, but
// the type still lowers (dead comptime-body code / a global slot) as i64.
.type_value => self.e.cached_i64,
.noreturn => self.e.cached_void,
.@"struct" => |s| {
// Build LLVM struct type from fields

View File

@@ -34,9 +34,23 @@ pub const TypeId = enum(u32) {
usize = 16,
void = 17,
cstring = 18, // thin null-terminated char* (see TypeInfo.cstring)
_, // user-defined types start at 19
/// A comptime `Type` VALUE — an 8-byte handle (a `TypeId` stored in a word),
/// DISTINCT from `.any`. A `Type` value is the reified type itself
/// (`reflect`/`const_type`/the comptime compiler-API), not a boxed Any. It used
/// to share `.any`'s slot, but `.any` is a 16-byte `{tag,value}` box (variadic
/// any), so a `Type` stored in an aggregate was sized 16B while the value is 8B
/// — which blocked the flat-memory comptime VM. Its own slot fixes the size and
/// keeps every downstream `== .any`/`switch` check from conflating the two.
type_value = 19,
_, // user-defined types start at `first_user` (slots 2099 reserved for future builtins)
pub const first_user: u32 = 19;
/// User-defined types start here. Builtins occupy 018 (plus `type_value` at
/// 19); slots 2099 are RESERVED headroom so adding a new builtin doesn't
/// renumber every user type (which would churn every `sx ir` snapshot in the
/// corpus). The `TypeTable` pads its `infos` array out to this index with the
/// `unresolved` tripwire so an accidental reference to a reserved slot panics
/// rather than silently aliasing a real type.
pub const first_user: u32 = 100;
pub fn index(self: TypeId) u32 {
return @intFromEnum(self);
@@ -86,6 +100,9 @@ pub const TypeInfo = union(enum) {
noreturn,
usize,
isize,
/// A comptime `Type` VALUE (see `TypeId.type_value`): an 8-byte type handle,
/// distinct from the 16-byte boxed `any`.
type_value,
/// Resolution-failure sentinel (see `TypeId.unresolved`).
unresolved,
@@ -385,10 +402,20 @@ pub const TypeTable = struct {
.usize, // 16: usize (pointer-sized unsigned)
.void, // 17
.cstring, // 18: thin null-terminated char*
.type_value, // 19: comptime `Type` value (8-byte handle, distinct from any)
};
for (&builtins) |info| {
table.infos.append(alloc, info) catch unreachable;
}
// Pad the reserved builtin headroom (slots after the real builtins, up to
// `first_user`) with the `unresolved` tripwire: these slots are never a
// legitimate type, so any reference panics in `sizeOf`/`get` rather than
// silently aliasing a user type. Reserving the range keeps user TypeIds
// stable as new builtins are added (no snapshot churn).
std.debug.assert(table.infos.items.len <= TypeId.first_user);
while (table.infos.items.len < TypeId.first_user) {
table.infos.append(alloc, .unresolved) catch unreachable;
}
return table;
}
@@ -721,6 +748,7 @@ pub const TypeTable = struct {
.array => |arr| arr.length * self.sizeOf(arr.element),
.vector => |vec| vec.length * self.sizeOf(vec.element),
.any => 16, // {type_tag, data_ptr}
.type_value => 8, // an 8-byte type handle (a `TypeId` in a word), NOT the 16-byte any box
.@"struct" => |s| {
var total: u32 = 0;
for (s.fields) |f| total += @max(self.sizeOf(f.ty), 8);
@@ -803,6 +831,7 @@ pub const TypeTable = struct {
if (ty == .cstring) 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 == .type_value) return 8; // 8-byte type handle (a `TypeId` in a word)
if (ty.isBuiltin()) return ptr_size; // default for unknown builtins
const info = self.get(ty);
return switch (info) {
@@ -904,6 +933,7 @@ pub const TypeTable = struct {
if (ty == .string) return 8; // i64 drives alignment
if (ty == .cstring) return ptr_align;
if (ty == .any) return 8; // {i64, i64} aligns to 8
if (ty == .type_value) return 8; // 8-byte type handle
if (ty.isBuiltin()) return ptr_align;
const info = self.get(ty);
return switch (info) {
@@ -975,6 +1005,7 @@ pub const TypeTable = struct {
.string => "string",
.cstring => "cstring",
.any => "Any",
.type_value => "Type",
.noreturn => "noreturn",
.isize => "isize",
.usize => "usize",
@@ -1120,7 +1151,7 @@ fn hashTypeInfo(h: *std.hash.Wyhash, info: TypeInfo) void {
switch (info) {
.signed => |w| h.update(&.{w}),
.unsigned => |w| h.update(&.{w}),
.f32, .f64, .void, .bool, .string, .cstring, .any, .noreturn, .usize, .isize, .unresolved => {},
.f32, .f64, .void, .bool, .string, .cstring, .any, .type_value, .noreturn, .usize, .isize, .unresolved => {},
.pointer => |p| h.update(std.mem.asBytes(&p.pointee)),
.many_pointer => |p| h.update(std.mem.asBytes(&p.element)),
.slice => |s| h.update(std.mem.asBytes(&s.element)),
@@ -1191,7 +1222,7 @@ fn typeInfoEql(a: TypeInfo, b: TypeInfo) bool {
return switch (a) {
.signed => |w| w == b.signed,
.unsigned => |w| w == b.unsigned,
.f32, .f64, .void, .bool, .string, .cstring, .any, .noreturn, .usize, .isize, .unresolved => true,
.f32, .f64, .void, .bool, .string, .cstring, .any, .type_value, .noreturn, .usize, .isize, .unresolved => true,
.pointer => |p| p.pointee == b.pointer.pointee,
.many_pointer => |p| p.element == b.many_pointer.element,
.slice => |s| s.element == b.slice.element,