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

@@ -254,30 +254,98 @@ fn selfExePath(allocator: std.mem.Allocator) ![]const u8 {
/// A resolved module: the fully-resolved declarations of a single .sx file,
/// with its own scope tracking which names are defined.
///
/// Imports are non-transitive. `scope` is intentionally *narrow*: it
/// contains only the names of decls authored in THIS file (plus namespaced
/// import aliases the file introduces). Visibility for names from
/// flat-imported modules is computed at lookup time by joining the
/// importer's `scope` with each direct flat-import's `scope` via
/// `import_graph` — this lets cyclic imports (e.g. std.sx ↔ allocators.sx)
/// resolve correctly even though one side of the cycle is skipped during
/// `resolveImports` recursion.
///
/// `decls` remains the full transitive flat list so the global lowering
/// pass can resolve a body in B that calls into C even though A never
/// imported C directly.
pub const ResolvedModule = struct {
path: []const u8,
/// Full flat decl list: own decls + every transitively-imported module's
/// own decls (deduped by name). Walked by `lowerRoot`/`scanDecls` so
/// transitive callees stay resolvable when their callers are lowered.
decls: []const *Node,
/// Decls authored in this file. What flat importers of THIS module see
/// (their visibility BFS joins these names in via `import_graph`).
own_decls: []const *Node,
/// Names authored in this file (plus namespace aliases this file
/// introduces). Used as the per-file leaf in the visibility lookup;
/// importers do NOT splice this into their own scope — they walk the
/// import graph at query time instead.
scope: std.StringHashMap(void),
/// Try to add a declaration. Returns true if added, false if name already in scope.
pub fn addDecl(self: *ResolvedModule, allocator: std.mem.Allocator, list: *std.ArrayList(*Node), decl: *Node) !bool {
/// Add a declaration authored in this file. Updates scope + own_decls +
/// the global flat decl list; dedups by name through `seen_list` (which
/// already holds names previously appended via `mergeFlat`, so an
/// authored decl that collides with a transitively-imported one stays
/// out of the global list while still entering `own_decls` for
/// importer-visibility purposes).
pub fn addOwnDecl(
self: *ResolvedModule,
allocator: std.mem.Allocator,
list: *std.ArrayList(*Node),
own_list: *std.ArrayList(*Node),
seen_list: *std.StringHashMap(void),
decl: *Node,
) !bool {
var append_to_global = true;
if (decl.data.declName()) |name| {
if (self.scope.contains(name)) return false;
try self.scope.put(name, {});
if (seen_list.contains(name)) {
append_to_global = false;
} else {
try seen_list.put(name, {});
}
}
try list.append(allocator, decl);
if (append_to_global) try list.append(allocator, decl);
try own_list.append(allocator, decl);
return true;
}
/// Merge another module's decls as flat imports (skipping duplicates).
pub fn mergeFlat(self: *ResolvedModule, allocator: std.mem.Allocator, list: *std.ArrayList(*Node), other: ResolvedModule) !void {
/// Flat-import another module. The imported names are NOT added to
/// `self.scope` — visibility joins per-file scopes at lookup time via
/// `import_graph`. We only need to append `other.decls` (the full
/// transitive list) to the global `list` so the lowering pass can
/// still resolve transitively-imported callees. Deduped by name.
pub fn mergeFlat(
self: *ResolvedModule,
allocator: std.mem.Allocator,
list: *std.ArrayList(*Node),
seen_list: *std.StringHashMap(void),
other: ResolvedModule,
) !void {
_ = self;
for (other.decls) |decl| {
_ = try self.addDecl(allocator, list, decl);
if (decl.data.declName()) |name| {
if (seen_list.contains(name)) continue;
try seen_list.put(name, {});
}
try list.append(allocator, decl);
}
}
/// Add another module as a namespaced import.
pub fn addNamespace(self: *ResolvedModule, allocator: std.mem.Allocator, list: *std.ArrayList(*Node), name: []const u8, other: ResolvedModule, span: ast.Span) !void {
/// Add another module as a namespaced import. The alias `name` becomes
/// part of this module's own decls (so a flat-importer of this module
/// sees the alias one hop out — matching authored names).
pub fn addNamespace(
self: *ResolvedModule,
allocator: std.mem.Allocator,
list: *std.ArrayList(*Node),
own_list: *std.ArrayList(*Node),
seen_list: *std.StringHashMap(void),
name: []const u8,
other: ResolvedModule,
span: ast.Span,
) !void {
const ns_node = try allocator.create(Node);
ns_node.* = .{
.span = span,
@@ -287,11 +355,19 @@ pub const ResolvedModule = struct {
} },
};
try self.scope.put(name, {});
try seen_list.put(name, {});
try list.append(allocator, ns_node);
try own_list.append(allocator, ns_node);
}
pub fn finalize(self: *ResolvedModule, allocator: std.mem.Allocator, list: *std.ArrayList(*Node)) !void {
pub fn finalize(
self: *ResolvedModule,
allocator: std.mem.Allocator,
list: *std.ArrayList(*Node),
own_list: *std.ArrayList(*Node),
) !void {
self.decls = try list.toOwnedSlice(allocator);
self.own_decls = try own_list.toOwnedSlice(allocator);
}
};
@@ -323,6 +399,7 @@ pub fn resolveImports(
var mod = ResolvedModule{
.path = file_path,
.decls = &.{},
.own_decls = &.{},
.scope = std.StringHashMap(void).init(allocator),
};
@@ -338,6 +415,11 @@ pub fn resolveImports(
const flat_decls = try flattenComptimeConditionals(allocator, root.data.root.decls, comptime_ctx);
var decl_list = std.ArrayList(*Node).empty;
var own_decl_list = std.ArrayList(*Node).empty;
// Name set spanning every decl already appended to `decl_list` — used
// by `mergeFlat` to dedupe across diamond imports now that `mod.scope`
// is non-transitive and can no longer serve as the dedup key.
var seen_in_list = std.StringHashMap(void).init(allocator);
for (flat_decls) |decl| {
if (decl.data == .c_import_decl) {
@@ -397,21 +479,23 @@ pub fn resolveImports(
};
ns_node.source_file = file_path;
try mod.scope.put(ns_name, {});
try seen_in_list.put(ns_name, {});
try decl_list.append(allocator, ns_node);
try own_decl_list.append(allocator, ns_node);
} else {
// Flat: add fn_decls directly + keep c_import_decl
for (result.fn_decls) |fd| {
fd.source_file = file_path;
_ = try mod.addDecl(allocator, &decl_list, fd);
_ = try mod.addOwnDecl(allocator, &decl_list, &own_decl_list, &seen_in_list, fd);
}
decl.source_file = file_path;
_ = try mod.addDecl(allocator, &decl_list, decl);
_ = try mod.addOwnDecl(allocator, &decl_list, &own_decl_list, &seen_in_list, decl);
}
continue;
}
if (decl.data != .import_decl) {
decl.source_file = file_path;
_ = try mod.addDecl(allocator, &decl_list, decl);
_ = try mod.addOwnDecl(allocator, &decl_list, &own_decl_list, &seen_in_list, decl);
continue;
}
const imp = decl.data.import_decl;
@@ -473,13 +557,13 @@ pub fn resolveImports(
};
if (imp.name) |ns_name| {
try mod.addNamespace(allocator, &decl_list, ns_name, imported_mod, decl.span);
try mod.addNamespace(allocator, &decl_list, &own_decl_list, &seen_in_list, ns_name, imported_mod, decl.span);
} else {
try mod.mergeFlat(allocator, &decl_list, imported_mod);
try mod.mergeFlat(allocator, &decl_list, &seen_in_list, imported_mod);
}
}
try mod.finalize(allocator, &decl_list);
try mod.finalize(allocator, &decl_list, &own_decl_list);
return mod;
}
@@ -524,13 +608,20 @@ fn resolveDirectoryImport(
try chain.put(dir_path, {});
defer _ = chain.remove(dir_path);
// Merge all files into a combined module
// Merge all files into a combined module. From an importer's perspective
// a directory is one big module: the combined module's `own_decls` is
// the union of every file's `own_decls`, so flat-importing the directory
// exposes everything the files themselves authored — but not what those
// files transitively imported from outside the directory.
var combined = ResolvedModule{
.path = dir_path,
.decls = &.{},
.own_decls = &.{},
.scope = std.StringHashMap(void).init(allocator),
};
var decl_list = std.ArrayList(*Node).empty;
var own_decl_list = std.ArrayList(*Node).empty;
var seen_in_list = std.StringHashMap(void).init(allocator);
for (file_names.items) |file_name| {
const file_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ dir_path, file_name });
@@ -568,9 +659,34 @@ fn resolveDirectoryImport(
break :file_blk result;
};
try combined.mergeFlat(allocator, &decl_list, file_mod);
// Source-order matters: a file's own decls (e.g. `impl Foo` blocks)
// may reference types defined in OTHER files that THIS file imports.
// `file_mod.decls` already lists transitive-imported decls before
// the file's own decls (resolveImports processes `#import` lines in
// source order, and #imports usually come first), so iterating it
// directly preserves the scan order the lowering pass needs to
// register `Event` (a tagged_union) before `handle_event(e: *Event)`
// triggers the placeholder-struct fallback in `resolveTypeName`.
for (file_mod.decls) |decl| {
if (decl.data.declName()) |name| {
if (seen_in_list.contains(name)) continue;
try seen_in_list.put(name, {});
}
try decl_list.append(allocator, decl);
}
// Separately track which decls the directory `re-exports` to its
// flat-importers. Position in `own_decl_list` doesn't matter — it's
// only consumed by the importer-side visibility join (`isNameVisible`
// in lower.zig) which treats it as a set.
for (file_mod.own_decls) |decl| {
if (decl.data.declName()) |name| {
if (combined.scope.contains(name)) continue;
try combined.scope.put(name, {});
}
try own_decl_list.append(allocator, decl);
}
}
try combined.finalize(allocator, &decl_list);
try combined.finalize(allocator, &decl_list, &own_decl_list);
return combined;
}