diff --git a/current/CHECKPOINT-COMPILER-API.md b/current/CHECKPOINT-COMPILER-API.md index 383cf4e7..1c9bba05 100644 --- a/current/CHECKPOINT-COMPILER-API.md +++ b/current/CHECKPOINT-COMPILER-API.md @@ -325,6 +325,22 @@ when reached (sentinels or accessor fns; see the design doc Risks). `List` growth; orthogonal, see `current/CHECKPOINT-METATYPE.md`.) ## Log +- **Phase 3 P3.4 (VM plan) — wire the VM at the LOWERING-time site + measure (2026-06-18).** + Routed `runComptimeTypeFunc` (the type-fn fold — the THIRD comptime call site) through + `comptime_vm.tryEval` behind `-Dcomptime-flat`/`SX_COMPTIME_FLAT` with legacy fallback, + mirroring the two emit-time folds. Extracted the shared post-check (`checkComptimeTypeResult` + — the declared-but-never-defined zero-field guard) so both paths use it. **Measurement + (SX_COMPTIME_FLAT_TRACE):** every metatype/compiler-API type-fn currently bails CLEANLY + with `no __sx_default_context global to materialize the implicit context` — at lowering + time the default-context global doesn't exist yet (it's built at emit time), so the VM bails + at context materialization, BEFORE running the body (no partial mint, no crash → legacy + mints). The hardening holds: **no crashes** across the corpus on the VM lowering-time path. + Both gates **697/0**. **So the FIRST lowering-time blocker is the implicit context, not + `Type` modeling** — the VM needs a way to materialize/skip the default context at lowering + time (most type-fns get an implicit ctx for potential `List`-growth alloc; many don't use + it). Next: materialize a lowering-time default context for the VM (or pass a null ctx + + bail only if the allocator is actually used), THEN model `Type` values + the VM-native write + side. This is near-pure fallback today — permanent scaffolding that lights up as those land. - **Phase 3 P3.4-prep (VM plan) — harden the VM against malformed lowering-time IR (2026-06-18).** Prerequisite for wiring the VM at the LOWERING-time comptime site (`runComptimeTypeFunc`), where IR can be malformed (an unresolved name lowers to a dangling / `Ref.none` operand — diff --git a/src/ir/lower/comptime.zig b/src/ir/lower/comptime.zig index 60ae8136..ce8f56bc 100644 --- a/src/ir/lower/comptime.zig +++ b/src/ir/lower/comptime.zig @@ -7,6 +7,8 @@ const inst_mod = @import("../inst.zig"); const unescape = @import("../../unescape.zig"); const parser_mod = @import("../../parser.zig"); const interp_mod = @import("../interp.zig"); +const comptime_vm = @import("../comptime_vm.zig"); +const build_opts = @import("build_opts"); const program_index_mod = @import("../program_index.zig"); const resolver_mod = @import("../resolver.zig"); const ModuleConstInfo = program_index_mod.ModuleConstInfo; @@ -489,6 +491,33 @@ pub fn runComptimeTypeFunc(self: *Lowering, func_id: FuncId, span: ast.Span) ?Ty // Clear the interp's last-bail channel so a bail HERE is attributable to // THIS construction (not a stale message from an earlier comptime eval). interp_mod.Interpreter.last_bail_detail = null; + + // Flat-memory VM fast path (gated by `-Dcomptime-flat` / `SX_COMPTIME_FLAT`), + // the THIRD comptime call site after the two emit-time folds. A type-fn runs + // on the VM; `null` (any bail) falls through to the legacy interpreter below, + // which mints identically. The VM bails BEFORE any table mutation — its + // compiler-WRITE fns (declare_type/register_type/pointer_to) aren't ported to + // `callCompilerFn`, and it can't yet model a `Type` result — so a minting + // type-fn bails at the first write call (no partial mint → no double-mint). + // The VM is hardened against malformed lowering-time IR (it BAILS, never + // panics; see `comptime_vm.refTy`/`badRef`). Today this is near-pure fallback; + // it lights up as `Type` modeling + the VM-native write side land. + const comptime_flat = build_opts.comptime_flat or std.c.getenv("SX_COMPTIME_FLAT") != null; + const vm_result: ?interp_mod.Value = if (comptime_flat) + comptime_vm.tryEval(self.alloc, self.module, func_id) + else + null; + if (comptime_flat and std.c.getenv("SX_COMPTIME_FLAT_TRACE") != null) { + if (vm_result != null) + std.debug.print("[comptime-vm] HANDLED type-fn\n", .{}) + else + std.debug.print("[comptime-vm] fallback type-fn: {s}\n", .{comptime_vm.last_bail_reason orelse ""}); + } + if (vm_result) |v| { + const tid_vm = v.asTypeId() orelse return null; + return checkComptimeTypeResult(self, tid_vm, span); + } + const result = interp.call(func_id, &.{}) catch |err| { // A comptime type construction (declare/define, reflection) that bails // must surface a build-gating diagnostic naming the reason — NOT poison @@ -506,11 +535,16 @@ pub fn runComptimeTypeFunc(self: *Lowering, func_id: FuncId, span: ast.Span) ?Ty return null; }; const tid = result.asTypeId() orelse return null; - // A bare `declare("X")` that is never completed by a `define(handle, …)` - // leaves a zero-FIELD nominal slot (an undefined enum). Sizing / constructing - // / emitting it panics at codegen (`verifySizes`: llvm_size != ir_size). - // Reject it loudly here — a zero-variant enum is never a legitimate result - // (`defineEnum` rejects an empty variant list too). + return checkComptimeTypeResult(self, tid, span); +} + +/// Post-check a comptime type-construction result (shared by the VM and legacy +/// paths). A bare `declare("X")` never completed by a `define(handle, …)` leaves +/// a zero-FIELD nominal slot (an undefined enum); sizing / constructing / emitting +/// it panics at codegen (`verifySizes`: llvm_size != ir_size). Reject it loudly +/// here — a zero-variant enum is never a legitimate result (`defineEnum` rejects +/// an empty variant list too). Returns the type, or null after gating the build. +fn checkComptimeTypeResult(self: *Lowering, tid: TypeId, span: ast.Span) ?TypeId { if (!tid.isBuiltin()) { const info = self.module.types.get(tid); if (info == .tagged_union and info.tagged_union.fields.len == 0) {