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:
152
src/imports.zig
152
src/imports.zig
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user