mem: implicit-context foundation + many compiler fixes

The session-long set of changes that lay the groundwork for the
Jai-literal implicit-Context-parameter refactor. Lots of accumulated
work; the new arrival is the implicit-ctx foundation (steps 1+2 of
the plan in current/CHECKPOINT-MEM.md):

  Step 1 — `CAllocator :: struct {}` stateless allocator in
    library/modules/allocators.sx, delegating directly to
    libc_malloc/libc_free. `ConstantValue` in src/ir/inst.zig gains a
    `func_ref: FuncId` leaf so nested aggregates can carry function
    pointers (the inline Allocator value's fn-ptr fields). Switch
    sites updated in emit_llvm.zig, print.zig, interp.zig.

  Step 2 — `emitDefaultContextGlobal` in src/ir/lower.zig synthesises
    a static `__sx_default_context` global with a nested-aggregate
    init_val pointing at the CAllocator → Allocator thunks. The
    second-pass `initVtableGlobals` in emit_llvm.zig is generalised
    to handle `.aggregate` init_vals (re-emits after func_map is
    populated so func_ref leaves resolve to real symbols).

Also folded in from earlier work this session:

  - Phase 1.1: `xx value` heap-copy in `buildProtocolValue` routes
    through `context.allocator` via the new `allocViaContext` helper.
  - interp.zig: `marshalForeignArg` double-offset bug fixed —
    `heapSlice` already adds `hp.offset` to the slice ptr, so the
    extra `+ hp.offset` was scribbling memcpy/memset into adjacent
    heap state, corrupting `heap.items[0]`. Symptom: `build_format`
    at comptime produced zero bytes, all `print` calls failed.
  - Lazy lowering: `lazyLowerFunction` now declares foreign-body
    functions as extern stubs in the local (comptime) module so
    cross-module foreign calls resolve.
  - Allocator API: all stdlib allocators on one-line `init() -> *T`
    (CAllocator/GPA: libc-backed; Arena/TrackingAllocator: parent-
    backed; BufAlloc: embeds state at head of user buffer).
  - issues 0038 (transitive #import), 0039 (chess + stdlib migration
    fallout), 0040 (generic struct method dot-dispatch), 0041
    (pointer types as type-arg), 0042 (alias name resolution) — all
    fixed; regression tests in examples/.
  - Diagnostic: `emitError` now embeds the lowering's
    `current_source_file` and enclosing function in the literal
    message; SX_TRACE_UNRESOLVED=1 dumps a Zig stack trace at the
    emit site so misattributed spans can't hide where the failure
    is.
  - tools/verify-step.sh (all-platforms gate) and tools/scratch.sh
    (interp/codegen parity tester) added.

Test suite: 152 example tests pass; chess builds + screenshots on
macOS / iOS sim / Android.
This commit is contained in:
agra
2026-05-24 22:59:20 +03:00
parent 0ba41b2980
commit 29784c22a8
63 changed files with 3448 additions and 1207 deletions

View File

@@ -681,32 +681,40 @@ pub const LLVMEmitter = struct {
}
}
/// Initialize vtable globals with function pointer constants.
/// Must run after Pass 1 (function declarations) so func_map is populated.
/// Initialize vtable + aggregate-with-func_ref globals with function
/// pointer constants. Must run after Pass 1 (function declarations) so
/// func_map is populated — that's why these globals get a placeholder
/// initializer in `emitGlobals` and we fix them up here.
fn initVtableGlobals(self: *LLVMEmitter) void {
for (self.ir_mod.globals.items, 0..) |global, i| {
const iv = global.init_val orelse continue;
const func_ids = switch (iv) {
.vtable => |ids| ids,
else => continue,
};
const llvm_global = self.global_map.get(@intCast(i)) orelse continue;
const llvm_ty = self.toLLVMType(global.ty);
// Build constant struct of function pointers
var field_vals = std.ArrayList(c.LLVMValueRef).empty;
defer field_vals.deinit(self.alloc);
for (func_ids) |fid| {
const llvm_func = self.func_map.get(fid.index()) orelse {
field_vals.append(self.alloc, c.LLVMConstNull(self.cached_ptr)) catch unreachable;
continue;
};
field_vals.append(self.alloc, llvm_func) catch unreachable;
switch (iv) {
.vtable => |func_ids| {
var field_vals = std.ArrayList(c.LLVMValueRef).empty;
defer field_vals.deinit(self.alloc);
for (func_ids) |fid| {
const llvm_func = self.func_map.get(fid.index()) orelse {
field_vals.append(self.alloc, c.LLVMConstNull(self.cached_ptr)) catch unreachable;
continue;
};
field_vals.append(self.alloc, llvm_func) catch unreachable;
}
const init_val = c.LLVMConstNamedStruct(llvm_ty, field_vals.items.ptr, @intCast(field_vals.items.len));
c.LLVMSetInitializer(llvm_global, init_val);
c.LLVMSetGlobalConstant(llvm_global, 1);
},
.aggregate => |agg| {
// Re-emit. The first pass in `emitGlobals` already ran,
// but func_ref leaves resolved to null then (func_map
// wasn't populated yet). Now they resolve properly.
const init_val = self.emitConstAggregate(agg, llvm_ty);
c.LLVMSetInitializer(llvm_global, init_val);
},
else => continue,
}
const init_val = c.LLVMConstNamedStruct(llvm_ty, field_vals.items.ptr, @intCast(field_vals.items.len));
c.LLVMSetInitializer(llvm_global, init_val);
c.LLVMSetGlobalConstant(llvm_global, 1);
}
}
@@ -2241,39 +2249,6 @@ pub const LLVMEmitter = struct {
.call_builtin => |bi| {
// Builtins that map to libc functions or LLVM intrinsics
switch (bi.builtin) {
.malloc => {
const size = self.coerceArg(self.resolveRef(bi.args[0]), self.sizeType());
const malloc_fn = self.getOrDeclareMalloc();
var args = [_]c.LLVMValueRef{size};
self.mapRef(c.LLVMBuildCall2(self.builder, self.getMallocType(), malloc_fn, &args, 1, "malloc"));
},
.free => {
const ptr = self.resolveRef(bi.args[0]);
const free_fn = self.getOrDeclareFree();
var args = [_]c.LLVMValueRef{ptr};
_ = c.LLVMBuildCall2(self.builder, self.getFreeType(), free_fn, &args, 1, "");
self.advanceRefCounter();
},
.memcpy => {
const dst = self.resolveRef(bi.args[0]);
const src = self.resolveRef(bi.args[1]);
const len = self.coerceArg(self.resolveRef(bi.args[2]), self.sizeType());
const memcpy_fn = self.getOrDeclareMemcpy();
var args = [_]c.LLVMValueRef{ dst, src, len };
_ = c.LLVMBuildCall2(self.builder, self.getMemcpyType(), memcpy_fn, &args, 3, "");
self.advanceRefCounter();
},
.memset => {
const dst = self.resolveRef(bi.args[0]);
var val = self.resolveRef(bi.args[1]);
const len = self.coerceArg(self.resolveRef(bi.args[2]), self.sizeType());
// memset expects i32 for byte value — coerce width
val = self.coerceArg(val, self.cached_i32);
const memset_fn = self.getOrDeclareMemset();
var args = [_]c.LLVMValueRef{ dst, val, len };
_ = c.LLVMBuildCall2(self.builder, self.getMemsetType(), memset_fn, &args, 3, "");
self.advanceRefCounter();
},
.sqrt, .sin, .cos, .floor => {
const val = self.resolveRef(bi.args[0]);
const val_ty = c.LLVMTypeOf(val);
@@ -3699,6 +3674,7 @@ pub const LLVMEmitter = struct {
.boolean => |v| c.LLVMConstInt(elem_ty, @intFromBool(v), 0),
.string => |sid| self.emitConstStringGlobal(self.ir_mod.types.getString(sid)),
.aggregate => |inner| self.emitConstAggregate(inner, elem_ty),
.func_ref => |fid| self.func_map.get(fid.index()) orelse c.LLVMConstNull(elem_ty),
else => c.LLVMConstNull(elem_ty),
};
}