mem: Steps 5-7 — context-identifier rebind + interp ctx bootstrap
Step 5 — `context` resolves through `current_ctx_ref`. The compile-time
emit of the default GPA into the `context` global is gone; entry points
already bind `current_ctx_ref` to `&__sx_default_context` and every
sx-to-sx call forwards it. `allocViaContext` sources from
`current_ctx_ref` too. `matchContextAllocCall` is kept as a comptime
escape hatch: the ct_module spun up by `evalComptimeString` doesn't get
the full Allocator/CAllocator/Context type registration so the protocol-
dispatch chain wouldn't run in the interp; codegen also wins from the
direct libc malloc/free.
Step 6 — `push Context.{...}` stack-discipline rewrite. Allocates a
fresh `Context` slot, binds `current_ctx_ref` to it for the body's
lexical scope, restores on exit. No global, no walk.
Step 7 — interp parity. `defaultContextValue()` builds the Context
aggregate (CAllocator thunks for alloc/dealloc, null data) on demand.
`interp.call` bootstraps slot_ptr(0) when an entry function with
implicit ctx is called sans args; `materializeCtxArg` dereferences the
caller's slot_ptr into the aggregate at every sx-to-sx call boundary so
the callee's `load(ref_0)` lands on the value; `load` of an aggregate
is a passthrough. `.global_addr` of `__sx_default_context` returns the
aggregate directly so exported entries' first-line `global_addr(...)`
runs cleanly in `#run`.
`ct_lowering` inherits `implicit_ctx_enabled` + `has_implicit_ctx` so
functions lowered into the ct module carry ctx like their main-module
twins.
152/152 example tests pass. Snapshots regen.
This commit is contained in:
128
src/ir/lower.zig
128
src/ir/lower.zig
@@ -1014,11 +1014,6 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-initialize context with default GPA at the start of main()
|
||||
if (std.mem.eql(u8, name, "main")) {
|
||||
self.emitDefaultContextInit();
|
||||
}
|
||||
|
||||
// Lower the function body (set target_type to return type for implicit returns)
|
||||
const saved_target = self.target_type;
|
||||
self.target_type = if (ret_ty != .void) ret_ty else null;
|
||||
@@ -1907,6 +1902,18 @@ pub const Lowering = struct {
|
||||
.enum_tag => |et| break :blk self.builder.constInt(@intCast(et.tag), et.ty),
|
||||
}
|
||||
}
|
||||
// `context` resolves to a load through the lowering's
|
||||
// current `__sx_ctx` pointer. Every sx function (and
|
||||
// every `push Context.{...}` body) sets `current_ctx_ref`
|
||||
// to a `*Context` it owns, so this is one indirection.
|
||||
if (self.implicit_ctx_enabled and std.mem.eql(u8, id.name, "context")) {
|
||||
if (self.current_ctx_ref != Ref.none) {
|
||||
const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse {
|
||||
break :blk self.emitError("context", node.span);
|
||||
};
|
||||
break :blk self.builder.load(self.current_ctx_ref, ctx_ty);
|
||||
}
|
||||
}
|
||||
// Check globals (#run constants)
|
||||
if (self.global_names.get(id.name)) |gi| {
|
||||
break :blk self.builder.emit(.{ .global_get = gi.id }, gi.ty);
|
||||
@@ -5013,7 +5020,11 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern-match context.allocator.alloc/dealloc → heap_alloc/heap_free
|
||||
// Pattern-match context.allocator.alloc/dealloc → heap_alloc/heap_free.
|
||||
// The comptime interp doesn't register the full Allocator
|
||||
// protocol in ct_module, so the protocol-dispatch chain it
|
||||
// would otherwise emit can't run. Codegen also benefits —
|
||||
// direct libc malloc/free, no thunk indirection.
|
||||
if (self.matchContextAllocCall(fa, args.items)) |ref| return ref;
|
||||
|
||||
// Type constructor call: Sx(f32).user(0.5) — obj is a call that returns a type
|
||||
@@ -5459,15 +5470,18 @@ pub const Lowering = struct {
|
||||
/// allocators.sx, so the fallback exists strictly for the bootstrapping
|
||||
/// edge case.
|
||||
fn allocViaContext(self: *Lowering, size_ref: Ref, void_ptr_ty: TypeId) Ref {
|
||||
const ctx_gi = self.global_names.get("context") orelse {
|
||||
if (!self.implicit_ctx_enabled or self.current_ctx_ref == Ref.none) {
|
||||
return self.builder.emit(.{ .heap_alloc = .{ .operand = size_ref } }, void_ptr_ty);
|
||||
}
|
||||
const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse {
|
||||
return self.builder.emit(.{ .heap_alloc = .{ .operand = size_ref } }, void_ptr_ty);
|
||||
};
|
||||
const ctx_ty_info = self.module.types.get(ctx_gi.ty);
|
||||
const ctx_ty_info = self.module.types.get(ctx_ty);
|
||||
if (ctx_ty_info != .@"struct" or ctx_ty_info.@"struct".fields.len < 1) {
|
||||
return self.builder.emit(.{ .heap_alloc = .{ .operand = size_ref } }, void_ptr_ty);
|
||||
}
|
||||
const allocator_ty = ctx_ty_info.@"struct".fields[0].ty;
|
||||
const ctx = self.builder.emit(.{ .global_get = ctx_gi.id }, ctx_gi.ty);
|
||||
const ctx = self.builder.load(self.current_ctx_ref, ctx_ty);
|
||||
const allocator = self.builder.structGet(ctx, 0, allocator_ty);
|
||||
// #inline Allocator protocol layout: { ctx, alloc_fn_ptr, dealloc_fn_ptr }.
|
||||
// field 0 = receiver ctx, field 1 = alloc fn-ptr.
|
||||
@@ -5512,16 +5526,18 @@ pub const Lowering = struct {
|
||||
return new_args;
|
||||
}
|
||||
|
||||
/// Pattern-match `context.allocator.alloc(size)` → heap_alloc,
|
||||
/// `context.allocator.dealloc(ptr)` → heap_free.
|
||||
/// Pattern-match `context.allocator.alloc(size)` → heap_alloc and
|
||||
/// `context.allocator.dealloc(ptr)` → heap_free. Required because the
|
||||
/// comptime interpreter doesn't get a full type/protocol registration
|
||||
/// of the Allocator chain; the protocol-dispatch lowering would
|
||||
/// produce IR that the interp can't execute. Codegen wins from this
|
||||
/// short-circuit too (libc malloc/free direct, no thunk indirection
|
||||
/// for the trivial default-context case).
|
||||
fn matchContextAllocCall(self: *Lowering, fa: ast.FieldAccess, call_args: []const Ref) ?Ref {
|
||||
// fa is the callee field_access: expecting .alloc or .dealloc
|
||||
if (!std.mem.eql(u8, fa.field, "alloc") and !std.mem.eql(u8, fa.field, "dealloc")) return null;
|
||||
// fa.object should be `context.allocator` — another field_access
|
||||
if (fa.object.data != .field_access) return null;
|
||||
const inner = fa.object.data.field_access;
|
||||
if (!std.mem.eql(u8, inner.field, "allocator")) return null;
|
||||
// inner.object should be `context` — an identifier
|
||||
if (inner.object.data != .identifier) return null;
|
||||
if (!std.mem.eql(u8, inner.object.data.identifier.name, "context")) return null;
|
||||
|
||||
@@ -5530,7 +5546,6 @@ pub const Lowering = struct {
|
||||
const ptr_void = self.module.types.ptrTo(.void);
|
||||
return self.builder.emit(.{ .heap_alloc = .{ .operand = call_args[0] } }, ptr_void);
|
||||
} else {
|
||||
// dealloc
|
||||
if (call_args.len < 1) return null;
|
||||
return self.builder.emit(.{ .heap_free = .{ .operand = call_args[0] } }, .void);
|
||||
}
|
||||
@@ -6171,23 +6186,30 @@ pub const Lowering = struct {
|
||||
}
|
||||
|
||||
fn lowerPush(self: *Lowering, ps: *const ast.PushStmt) void {
|
||||
// push context_expr { body }
|
||||
// → save = global_get(context), global_set(context, new_val), body, global_set(context, save)
|
||||
const gi = self.global_names.get("context") orelse {
|
||||
// No context global — just lower the body without push/pop
|
||||
// push Context.{...} { body } — allocates a fresh Context on the
|
||||
// stack frame, rebinds the lowering's `current_ctx_ref` to it for
|
||||
// the body's lexical scope, then restores. No global, no walk.
|
||||
if (!self.implicit_ctx_enabled) {
|
||||
self.lowerBlock(ps.body);
|
||||
return;
|
||||
}
|
||||
const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse {
|
||||
self.lowerBlock(ps.body);
|
||||
return;
|
||||
};
|
||||
// Save current context
|
||||
const save = self.builder.emit(.{ .global_get = gi.id }, gi.ty);
|
||||
// Lower the new context value
|
||||
const saved_ctx_ref = self.current_ctx_ref;
|
||||
defer self.current_ctx_ref = saved_ctx_ref;
|
||||
|
||||
const saved_target = self.target_type;
|
||||
self.target_type = ctx_ty;
|
||||
const ctx_val = self.lowerExpr(ps.context_expr);
|
||||
// Store into context global
|
||||
self.builder.emitVoid(.{ .global_set = .{ .global = gi.id, .value = ctx_val } }, .void);
|
||||
// Lower the body
|
||||
self.target_type = saved_target;
|
||||
|
||||
const slot = self.builder.alloca(ctx_ty);
|
||||
self.builder.store(slot, ctx_val);
|
||||
self.current_ctx_ref = slot;
|
||||
|
||||
self.lowerBlock(ps.body);
|
||||
// Restore saved context
|
||||
self.builder.emitVoid(.{ .global_set = .{ .global = gi.id, .value = save } }, .void);
|
||||
}
|
||||
|
||||
fn lowerMultiAssign(self: *Lowering, ma: *const ast.MultiAssign) void {
|
||||
@@ -6419,6 +6441,12 @@ pub const Lowering = struct {
|
||||
ct_lowering.main_file = null; // no main file filtering
|
||||
ct_lowering.comptime_param_nodes = self.comptime_param_nodes;
|
||||
ct_lowering.fn_ast_map = self.fn_ast_map; // share AST map for lazy resolution
|
||||
// Inherit the implicit-ctx switch: the parent program uses
|
||||
// Context, so functions lowered into ct_module must carry
|
||||
// __sx_ctx too (otherwise the inserted code's `context.X`
|
||||
// reads can't resolve through current_ctx_ref).
|
||||
ct_lowering.implicit_ctx_enabled = self.implicit_ctx_enabled;
|
||||
ct_module.has_implicit_ctx = self.implicit_ctx_enabled;
|
||||
|
||||
// Lower only the functions reachable from this expression.
|
||||
// For a call like build_format(fmt), we need build_format's AST.
|
||||
@@ -10419,52 +10447,6 @@ pub const Lowering = struct {
|
||||
};
|
||||
}
|
||||
|
||||
/// Auto-initialize the global `context` with a default GPA allocator at the start of main().
|
||||
/// Emits IR instructions equivalent to:
|
||||
/// __default_gpa : GPA = .{ alloc_count = 0 };
|
||||
/// context = Context.{ allocator = GPA.create(@__default_gpa), data = null };
|
||||
fn emitDefaultContextInit(self: *Lowering) void {
|
||||
// Look up the context global
|
||||
const ctx_gi = self.global_names.get("context") orelse return;
|
||||
const ctx_ty = ctx_gi.ty;
|
||||
|
||||
// Look up GPA type
|
||||
const gpa_ty = self.module.types.findByName(self.module.types.internString("GPA")) orelse return;
|
||||
// Look up Allocator type
|
||||
const alloc_ty = self.module.types.findByName(self.module.types.internString("Allocator")) orelse return;
|
||||
|
||||
// Get GPA→Allocator thunks
|
||||
const thunks = self.getOrCreateThunks("Allocator", "GPA");
|
||||
if (thunks.len < 2) return;
|
||||
|
||||
// 1. Stack-allocate GPA with alloc_count = 0
|
||||
const gpa_slot = self.builder.alloca(gpa_ty);
|
||||
const zero = self.builder.constInt(0, .s64);
|
||||
const gpa_val = self.builder.emit(.{ .struct_init = .{
|
||||
.fields = self.alloc.dupe(Ref, &.{zero}) catch return,
|
||||
} }, gpa_ty);
|
||||
self.builder.store(gpa_slot, gpa_val);
|
||||
|
||||
// 2. Build Allocator inline protocol value: { ctx: *void, alloc_fn, dealloc_fn }
|
||||
const void_ptr_ty = self.module.types.ptrTo(.void);
|
||||
const gpa_ptr = gpa_slot; // alloca already gives us *GPA, all pointers are compatible
|
||||
const alloc_fn = self.builder.emit(.{ .func_ref = thunks[0] }, void_ptr_ty);
|
||||
const dealloc_fn = self.builder.emit(.{ .func_ref = thunks[1] }, void_ptr_ty);
|
||||
|
||||
const alloc_val = self.builder.emit(.{ .struct_init = .{
|
||||
.fields = self.alloc.dupe(Ref, &.{ gpa_ptr, alloc_fn, dealloc_fn }) catch return,
|
||||
} }, alloc_ty);
|
||||
|
||||
// 3. Build Context struct: { allocator, data: null }
|
||||
const null_ptr = self.builder.constNull(void_ptr_ty);
|
||||
const ctx_val = self.builder.emit(.{ .struct_init = .{
|
||||
.fields = self.alloc.dupe(Ref, &.{ alloc_val, null_ptr }) catch return,
|
||||
} }, ctx_ty);
|
||||
|
||||
// 4. Store into context global
|
||||
self.builder.emitVoid(.{ .global_set = .{ .global = ctx_gi.id, .value = ctx_val } }, .void);
|
||||
}
|
||||
|
||||
fn emitModuleConst(self: *Lowering, ci: ModuleConstInfo) Ref {
|
||||
switch (ci.value.data) {
|
||||
.int_literal => |lit| {
|
||||
|
||||
Reference in New Issue
Block a user