Files
sx/src/imports.zig
agra 29784c22a8 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.
2026-05-24 22:59:20 +03:00

693 lines
29 KiB
Zig

const std = @import("std");
const ast = @import("ast.zig");
const parser = @import("parser.zig");
const errors = @import("errors.zig");
const c_import = @import("c_import.zig");
const Node = ast.Node;
/// Comptime evaluation context for the inline-if hoisting pass below.
/// Mirrors the values `injectComptimeConstants` will later push into the
/// lowering's `comptime_constants` map (OS / ARCH / POINTER_SIZE), but
/// derived directly from the build target so we can resolve top-level
/// `inline if OS == .X { ... }` arms before imports + lowering run.
pub const ComptimeContext = struct {
/// Lowercase OS name matching the OperatingSystem enum tag
/// (macos / linux / windows / wasm / ios / android / unknown).
os: []const u8 = "unknown",
/// Lowercase architecture name matching the Architecture enum tag
/// (aarch64 / x86_64 / wasm32 / wasm64 / unknown).
arch: []const u8 = "unknown",
/// 4 for wasm32, 8 for every other target.
pointer_size: i64 = 8,
};
/// Top-level `inline if OS == .X { decls }` blocks are parsed as
/// `if_expr` / `match_expr` nodes in `root.decls`, but the lowering
/// pass only knows how to dispatch on `.fn_decl` / `.const_decl` /
/// `.var_decl` / etc. at decl positions — an `if_expr` at the top
/// level is silently dropped. Same story for `#import` decls inside an
/// `inline if` body: they need to be surfaced to the top so import
/// resolution sees them.
///
/// This pass walks `decls`, replaces every comptime conditional with
/// the body of its taken arm (recursively flattened), and drops the
/// rest. A condition we can't resolve at this stage is also dropped —
/// the caller may want to surface that as a diagnostic later, but for
/// the OS / ARCH / POINTER_SIZE patterns we cover here it shouldn't
/// happen in practice.
pub fn flattenComptimeConditionals(allocator: std.mem.Allocator, decls: []const *Node, ctx: ComptimeContext) std.mem.Allocator.Error![]const *Node {
var out = std.ArrayList(*Node).empty;
for (decls) |decl| {
switch (decl.data) {
.if_expr => |ie| {
if (ie.is_comptime) {
if (evalComptimeCondition(ie.condition, ctx)) |is_true| {
const taken: ?*const Node = if (is_true) ie.then_branch else ie.else_branch;
if (taken) |b| try appendBranchDecls(allocator, &out, b, ctx);
continue;
}
// Couldn't evaluate — drop the whole conditional. This is
// a conservative choice; future work may surface it as a
// diagnostic. For OS / ARCH / POINTER_SIZE comparisons
// the eval is total, so this shouldn't fire in practice.
continue;
}
try out.append(allocator, decl);
},
.match_expr => |me| {
if (me.is_comptime) {
if (evalComptimeMatch(&me, ctx)) |body| {
try appendBranchDecls(allocator, &out, body, ctx);
}
continue;
}
try out.append(allocator, decl);
},
else => try out.append(allocator, decl),
}
}
return try out.toOwnedSlice(allocator);
}
fn appendBranchDecls(allocator: std.mem.Allocator, out: *std.ArrayList(*Node), branch: *const Node, ctx: ComptimeContext) std.mem.Allocator.Error!void {
const stmts: []const *Node = if (branch.data == .block)
branch.data.block.stmts
else
&[_]*Node{@constCast(branch)};
const recursed = try flattenComptimeConditionals(allocator, stmts, ctx);
try out.appendSlice(allocator, recursed);
}
fn evalComptimeCondition(node: *const Node, ctx: ComptimeContext) ?bool {
if (node.data != .binary_op) return null;
const bo = &node.data.binary_op;
if (bo.op != .eq and bo.op != .neq) return null;
const name = switch (bo.lhs.data) {
.identifier => |id| id.name,
else => return null,
};
if (std.mem.eql(u8, name, "OS") or std.mem.eql(u8, name, "ARCH")) {
const variant = switch (bo.rhs.data) {
.enum_literal => |el| el.name,
else => return null,
};
const target = if (std.mem.eql(u8, name, "OS")) ctx.os else ctx.arch;
const matches = std.mem.eql(u8, variant, target);
return if (bo.op == .eq) matches else !matches;
}
if (std.mem.eql(u8, name, "POINTER_SIZE")) {
const rhs_val: i64 = switch (bo.rhs.data) {
.int_literal => |il| il.value,
else => return null,
};
const matches = ctx.pointer_size == rhs_val;
return if (bo.op == .eq) matches else !matches;
}
return null;
}
fn evalComptimeMatch(me: *const ast.MatchExpr, ctx: ComptimeContext) ?*const Node {
const name = switch (me.subject.data) {
.identifier => |id| id.name,
else => return null,
};
if (std.mem.eql(u8, name, "OS") or std.mem.eql(u8, name, "ARCH")) {
const target = if (std.mem.eql(u8, name, "OS")) ctx.os else ctx.arch;
for (me.arms) |arm| {
const pattern = arm.pattern orelse continue;
const variant = switch (pattern.data) {
.enum_literal => |el| el.name,
else => continue,
};
if (std.mem.eql(u8, variant, target)) return arm.body;
}
for (me.arms) |arm| if (arm.pattern == null) return arm.body;
return null;
}
if (std.mem.eql(u8, name, "POINTER_SIZE")) {
for (me.arms) |arm| {
const pattern = arm.pattern orelse continue;
const rhs_val: i64 = switch (pattern.data) {
.int_literal => |il| il.value,
else => continue,
};
if (ctx.pointer_size == rhs_val) return arm.body;
}
for (me.arms) |arm| if (arm.pattern == null) return arm.body;
return null;
}
return null;
}
pub fn dirName(path: []const u8) []const u8 {
var last_sep: usize = 0;
var found = false;
for (path, 0..) |ch, i| {
if (ch == '/') {
last_sep = i;
found = true;
}
}
return if (found) path[0..last_sep] else ".";
}
/// Resolve an import path. Tries (in order):
/// 1. relative to `base_dir` (the importing file's directory)
/// 2. relative to CWD, absolutified via `root_path` if supplied
/// 3. relative to each path in `stdlib_paths` (the install-discovered stdlib)
/// Returns the first path that exists. Falls back to the raw path if nothing matches
/// so the caller's readFile produces a coherent "not found" error.
pub fn resolveImportPath(allocator: std.mem.Allocator, io: std.Io, base_dir: []const u8, raw_path: []const u8, root_path: ?[]const u8, stdlib_paths: []const []const u8) ![]const u8 {
if (!std.mem.eql(u8, base_dir, ".")) {
const rel_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ base_dir, raw_path });
// Check if it exists as file relative to base_dir
if (std.Io.Dir.readFileAlloc(.cwd(), io, rel_path, allocator, .limited(10 * 1024 * 1024))) |_| {
return rel_path;
} else |_| {}
// Check if it exists as directory relative to base_dir
if (std.Io.Dir.openDir(.cwd(), io, rel_path, .{})) |dir| {
dir.close(io);
return rel_path;
} else |_| {}
}
// Try CWD-relative (absolutified if root_path is known).
const cwd_candidate = if (root_path) |rp| blk: {
if (rp.len > 0 and raw_path.len > 0 and raw_path[0] != '/') {
break :blk try std.fmt.allocPrint(allocator, "{s}/{s}", .{ rp, raw_path });
}
break :blk raw_path;
} else raw_path;
if (std.Io.Dir.readFileAlloc(.cwd(), io, cwd_candidate, allocator, .limited(10 * 1024 * 1024))) |_| {
return cwd_candidate;
} else |_| {}
if (std.Io.Dir.openDir(.cwd(), io, cwd_candidate, .{})) |dir| {
dir.close(io);
return cwd_candidate;
} else |_| {}
// Try each stdlib search path.
for (stdlib_paths) |sp| {
const cand = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ sp, raw_path });
if (std.Io.Dir.readFileAlloc(.cwd(), io, cand, allocator, .limited(10 * 1024 * 1024))) |_| {
return cand;
} else |_| {}
if (std.Io.Dir.openDir(.cwd(), io, cand, .{})) |dir| {
dir.close(io);
return cand;
} else |_| {}
}
return cwd_candidate;
}
/// Discover candidate stdlib search paths from the running binary's location.
/// Honors the `SX_STDLIB_PATH` env var as an explicit override. Returns a slice
/// of absolute paths owned by the allocator.
pub fn discoverStdlibPaths(allocator: std.mem.Allocator) ![]const []const u8 {
var out = std.ArrayList([]const u8).empty;
// Env override via libc getenv (cross-stdlib-version stable).
if (c_getenv("SX_STDLIB_PATH")) |env_path| {
try out.append(allocator, try allocator.dupe(u8, std.mem.span(env_path)));
}
const exe_path = selfExePath(allocator) catch return try out.toOwnedSlice(allocator);
const exe_dir = dirName(exe_path);
// Stdlib paths are directories containing a `modules/` subdir; the import
// directive (e.g. `#import "modules/std.sx"`) supplies the rest.
// Dev: zig-out/bin/sx -> repo-root/library
try out.append(allocator, try std.fmt.allocPrint(allocator, "{s}/../../library", .{exe_dir}));
// Install: <prefix>/bin/sx -> <prefix>/library
try out.append(allocator, try std.fmt.allocPrint(allocator, "{s}/../library", .{exe_dir}));
// Alongside the binary.
try out.append(allocator, try std.fmt.allocPrint(allocator, "{s}/library", .{exe_dir}));
if (c_getenv("SX_DEBUG_STDLIB") != null) {
std.debug.print("[sx] exe_path={s}\n", .{exe_path});
for (out.items, 0..) |p, i| std.debug.print("[sx] stdlib_paths[{d}]={s}\n", .{ i, p });
}
return try out.toOwnedSlice(allocator);
}
const builtin = @import("builtin");
extern "c" fn _NSGetExecutablePath(buf: [*]u8, len: *u32) c_int;
extern "c" fn getenv(name: [*:0]const u8) ?[*:0]const u8;
fn c_getenv(name: [:0]const u8) ?[*:0]const u8 {
return getenv(name.ptr);
}
fn selfExePath(allocator: std.mem.Allocator) ![]const u8 {
var buf: [4096]u8 = undefined;
switch (builtin.os.tag) {
.macos, .ios => {
var len: u32 = buf.len;
if (_NSGetExecutablePath(&buf, &len) != 0) return error.PathBufferTooSmall;
const span = std.mem.sliceTo(&buf, 0);
return try allocator.dupe(u8, span);
},
.linux => {
const n = try std.posix.readlink("/proc/self/exe", &buf);
return try allocator.dupe(u8, n);
},
else => return error.UnsupportedHostOS,
}
}
/// 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),
/// 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, {});
}
}
if (append_to_global) try list.append(allocator, decl);
try own_list.append(allocator, decl);
return true;
}
/// 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| {
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. 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,
.data = .{ .namespace_decl = .{
.name = name,
.decls = other.decls,
} },
};
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),
own_list: *std.ArrayList(*Node),
) !void {
self.decls = try list.toOwnedSlice(allocator);
self.own_decls = try own_list.toOwnedSlice(allocator);
}
};
/// Module cache: maps resolved file paths to their ResolvedModules.
pub const ModuleCache = std.StringHashMap(ResolvedModule);
pub fn resolveImports(
allocator: std.mem.Allocator,
io: std.Io,
root: *Node,
base_dir: []const u8,
file_path: []const u8,
chain: *std.StringHashMap(void),
cache: *ModuleCache,
source_map: ?*std.StringHashMap([:0]const u8),
diagnostics: ?*errors.DiagnosticList,
stdlib_paths: []const []const u8,
import_graph: ?*std.StringHashMap(std.StringHashMap(void)),
comptime_ctx: ComptimeContext,
) !ResolvedModule {
// Record this file's edge set so `param_impl_map` lookups can filter
// candidates by what's been imported from where. Populated as each
// import resolves below; transitive closure computed on demand.
if (import_graph) |g| {
if (!g.contains(file_path)) {
try g.put(file_path, std.StringHashMap(void).init(allocator));
}
}
var mod = ResolvedModule{
.path = file_path,
.decls = &.{},
.own_decls = &.{},
.scope = std.StringHashMap(void).init(allocator),
};
if (root.data != .root) {
mod.decls = &.{};
return mod;
}
// Hoist top-level `inline if OS == .X { ... }` body decls (including
// any `#import`s inside them) to the top level before resolution
// proceeds. After this pass, the decl list contains no top-level
// `if_expr` / `match_expr` nodes with `is_comptime = true`.
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) {
// Resolve `#source` / `#include` paths through the same chain
// as `#import`: importing-file's directory → CWD → stdlib
// search paths. This lets sx-library modules ship their own
// C helpers (e.g. the Android JNI insets bridge) without
// forcing every consumer to vendor an identically-named copy.
{
const ci_pre = decl.data.c_import_decl;
if (ci_pre.sources.len > 0) {
var resolved = try allocator.alloc([]const u8, ci_pre.sources.len);
for (ci_pre.sources, 0..) |raw_src, idx| {
resolved[idx] = try resolveImportPath(allocator, io, base_dir, raw_src, null, stdlib_paths);
}
decl.data.c_import_decl.sources = resolved;
}
if (ci_pre.includes.len > 0) {
var resolved = try allocator.alloc([]const u8, ci_pre.includes.len);
for (ci_pre.includes, 0..) |raw_inc, idx| {
resolved[idx] = try resolveImportPath(allocator, io, base_dir, raw_inc, null, stdlib_paths);
}
decl.data.c_import_decl.includes = resolved;
}
}
const ci = decl.data.c_import_decl;
// Parse headers to get synthetic function declarations
const result = c_import.processCImport(
allocator,
ci.includes,
ci.defines,
ci.flags,
) catch |err| {
if (diagnostics) |diags| {
diags.addFmt(.err, decl.span, "#import c failed: {}", .{err});
}
return error.ImportError;
};
if (ci.name) |ns_name| {
// Namespaced: wrap fn_decls + c_import_decl in a namespace
var ns_decls = std.ArrayList(*Node).empty;
for (result.fn_decls) |fd| {
try ns_decls.append(allocator, fd);
}
// Keep c_import_decl inside namespace so codegen can find sources
try ns_decls.append(allocator, decl);
const ns_node = try allocator.create(Node);
ns_node.* = .{
.span = decl.span,
.data = .{ .namespace_decl = .{
.name = ns_name,
.decls = try ns_decls.toOwnedSlice(allocator),
} },
};
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.addOwnDecl(allocator, &decl_list, &own_decl_list, &seen_in_list, fd);
}
decl.source_file = file_path;
_ = 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.addOwnDecl(allocator, &decl_list, &own_decl_list, &seen_in_list, decl);
continue;
}
const imp = decl.data.import_decl;
const resolved_path = try resolveImportPath(allocator, io, base_dir, imp.path, null, stdlib_paths);
// Record direct-import edge file_path → resolved_path. Self-imports
// and chain duplicates are still recorded so the graph reflects what
// the user wrote (filter happens at lookup).
if (import_graph) |g| {
if (g.getPtr(file_path)) |set| {
set.put(resolved_path, {}) catch {};
}
}
// Circular import check — only along the current chain
if (chain.contains(resolved_path)) continue;
// Resolve or retrieve the imported module
const imported_mod = if (cache.get(resolved_path)) |cached|
cached
else blk: {
// Try as file first
if (std.Io.Dir.readFileAlloc(.cwd(), io, resolved_path, allocator, .limited(10 * 1024 * 1024))) |imp_bytes| {
const imp_source = try allocator.dupeZ(u8, imp_bytes);
if (source_map) |sm| {
sm.put(resolved_path, imp_source) catch {};
}
var p = parser.Parser.init(allocator, imp_source);
const imp_root = p.parse() catch {
if (diagnostics) |diags| {
diags.addFmt(.err, decl.span, "parse error in '{s}': {s}", .{ resolved_path, p.err_msg orelse "unknown" });
}
return error.ImportError;
};
// Push onto chain before recursing, pop after
try chain.put(resolved_path, {});
const imp_dir = dirName(resolved_path);
const result = try resolveImports(allocator, io, imp_root, imp_dir, resolved_path, chain, cache, source_map, diagnostics, stdlib_paths, import_graph, comptime_ctx);
_ = chain.remove(resolved_path);
// Cache
try cache.put(resolved_path, result);
break :blk result;
} else |_| {
// File read failed — try as directory import
const result = resolveDirectoryImport(allocator, io, resolved_path, chain, cache, source_map, diagnostics, decl.span, stdlib_paths, import_graph, comptime_ctx) catch {
if (diagnostics) |diags| {
diags.addFmt(.err, decl.span, "cannot read import '{s}' (not a file or directory)", .{resolved_path});
}
return error.ImportError;
};
try cache.put(resolved_path, result);
break :blk result;
}
};
if (imp.name) |ns_name| {
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, &seen_in_list, imported_mod);
}
}
try mod.finalize(allocator, &decl_list, &own_decl_list);
return mod;
}
/// Resolve a directory import by aggregating all .sx files in the directory.
fn resolveDirectoryImport(
allocator: std.mem.Allocator,
io: std.Io,
dir_path: []const u8,
chain: *std.StringHashMap(void),
cache: *ModuleCache,
source_map: ?*std.StringHashMap([:0]const u8),
diagnostics: ?*errors.DiagnosticList,
span: ast.Span,
stdlib_paths: []const []const u8,
import_graph: ?*std.StringHashMap(std.StringHashMap(void)),
comptime_ctx: ComptimeContext,
) anyerror!ResolvedModule {
// Open the directory with iteration capability
const dir = std.Io.Dir.openDir(.cwd(), io, dir_path, .{ .iterate = true }) catch {
return error.ImportError;
};
defer dir.close(io);
// Collect all .sx file names
var file_names = std.ArrayList([]const u8).empty;
var it = dir.iterate();
while (it.next(io) catch null) |entry| {
if (entry.kind != .file) continue;
if (!std.mem.endsWith(u8, entry.name, ".sx")) continue;
const name_copy = try allocator.dupe(u8, entry.name);
try file_names.append(allocator, name_copy);
}
// Sort alphabetically for deterministic ordering
std.mem.sort([]const u8, file_names.items, {}, struct {
fn lessThan(_: void, a: []const u8, b: []const u8) bool {
return std.mem.order(u8, a, b) == .lt;
}
}.lessThan);
// Add directory to chain for circular import detection
try chain.put(dir_path, {});
defer _ = chain.remove(dir_path);
// 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 });
if (chain.contains(file_path)) continue;
const file_mod = if (cache.get(file_path)) |cached|
cached
else file_blk: {
const imp_bytes = std.Io.Dir.readFileAlloc(.cwd(), io, file_path, allocator, .limited(10 * 1024 * 1024)) catch {
if (diagnostics) |diags| {
diags.addFmt(.err, span, "cannot read '{s}' in directory import", .{file_path});
}
return error.ImportError;
};
const imp_source = try allocator.dupeZ(u8, imp_bytes);
if (source_map) |sm| {
sm.put(file_path, imp_source) catch {};
}
var p = parser.Parser.init(allocator, imp_source);
const imp_root = p.parse() catch {
if (diagnostics) |diags| {
diags.addFmt(.err, span, "parse error in '{s}': {s}", .{ file_path, p.err_msg orelse "unknown" });
}
return error.ImportError;
};
try chain.put(file_path, {});
const result = try resolveImports(allocator, io, imp_root, dir_path, file_path, chain, cache, source_map, diagnostics, stdlib_paths, import_graph, comptime_ctx);
_ = chain.remove(file_path);
try cache.put(file_path, result);
break :file_blk result;
};
// 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, &own_decl_list);
return combined;
}