ROOT FIX for issue 0106's library-metaprogram half — no exemption.
attempt-2 masked the 0106 fallout with an `in_insert_expansion` flag that
made the visibility adapters fall open during ANY `#insert` expansion,
including a USER's `#insert <expr>` — so a bare reach into a namespaced-only
import from user `#insert` code wrongly compiled (Adi's blocker). The flag
was the wrong shape. This removes it and fixes the real cause.
Root cause: a metaprogram's body (`std.print` / `std.format` / `log.*`,
whose `#insert build_format(fmt)` + `#insert "out(result);"` reference
std-internal bare names) was lowered under the CALL SITE's
`current_source_file`, so those names were policed against the consumer's
imports. Normal functions get this right via `lowerFunctionBodyInto`, which
pins `func.source_file`; the two monomorphizers don't:
- `monomorphizePackFn` — bare `print(...)` / `format(...)` (pack path).
- `lowerComptimeCall` — namespaced `std.print` / `log.warn` (reached via
the field-access `hasComptimeParams` branch).
Fix: both paths now save/set/restore `current_source_file` to the body's
DEFINING module around the BODY lowering only (call-site args stay in the
caller's context). The defining path is stamped onto each function body node
by `resolveImports` (`stampFnBodySource`), mirroring `Function.source_file`.
So library internals resolve in std.sx/log.sx naturally, while a USER's
`#insert <expr>` is still checked in the user's context.
- Exemption GONE: `in_insert_expansion` flag + both adapter fall-open checks
deleted; `isNameVisible`/`isCImportVisible` are byte-identical adapters.
- New pinned regression: examples/0737-modules-insert-bare-not-visible.sx
(+ a.sx) — a USER `#insert secret()` into a namespaced-only import errors
('secret' is not visible). fail-before exit 0 on the attempt-2 binary /
pass-after exit 1.
- face #1 (0736) still errors; face #2 (0015/0700/0718/1030) pass again WITH
NO exemption — the metaprogram body resolves in its own module.
- run_examples 472 -> 473; zig build test 412/412; m3te ios-sim build exit 0.
- issues/0106 RESOLVED banner updated (root cause + no-exemption fix).
980 lines
44 KiB
Zig
980 lines
44 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;
|
|
|
|
/// The `*const ast.FnDecl` a function-authoring decl carries, or null when the
|
|
/// decl is not a function — either a bare `fn_decl` (`f :: (…) -> T { … }`) or a
|
|
/// `const_decl` whose value is a function. Drives the per-module `module_fns`
|
|
/// identity index (fix-0102a).
|
|
fn fnDeclOf(decl: *const Node) ?*const ast.FnDecl {
|
|
return switch (decl.data) {
|
|
.fn_decl => &decl.data.fn_decl,
|
|
.const_decl => |cd| if (cd.value.data == .fn_decl) &cd.value.data.fn_decl else null,
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
/// 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 two ways: named decls by name (first-wins on cross-module
|
|
/// collisions), and EVERY decl by node identity. The latter matters for
|
|
/// anonymous decls — `impl` blocks have no `declName`, so under a diamond
|
|
/// import the same cached node would otherwise be appended once per path
|
|
/// and registered twice (e.g. `duplicate impl 'Into'`).
|
|
pub fn mergeFlat(
|
|
self: *ResolvedModule,
|
|
allocator: std.mem.Allocator,
|
|
list: *std.ArrayList(*Node),
|
|
seen_list: *std.StringHashMap(void),
|
|
seen_nodes: *std.AutoHashMap(*Node, void),
|
|
other: ResolvedModule,
|
|
) !void {
|
|
_ = self;
|
|
for (other.decls) |decl| {
|
|
if (seen_nodes.contains(decl)) continue;
|
|
if (decl.data.declName()) |name| {
|
|
if (seen_list.contains(name)) continue;
|
|
try seen_list.put(name, {});
|
|
}
|
|
try seen_nodes.put(decl, {});
|
|
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).
|
|
///
|
|
/// Returns `false` (and adds nothing) when `name` already names a decl
|
|
/// authored in THIS module — symmetric to `addOwnDecl`, so a namespace
|
|
/// alias colliding with a prior same-module name is dropped rather than
|
|
/// shadowing it. The caller surfaces the drop via `reportDuplicateName`;
|
|
/// the reverse order (alias first, then a same-name authored decl) is
|
|
/// caught by `addOwnDecl` seeing the alias already in `scope`.
|
|
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,
|
|
is_raw: bool,
|
|
) !bool {
|
|
if (self.scope.contains(name)) return false;
|
|
const ns_node = try allocator.create(Node);
|
|
ns_node.* = .{
|
|
.span = span,
|
|
.data = .{ .namespace_decl = .{
|
|
.name = name,
|
|
.decls = other.decls,
|
|
// The module's OWN authored decls — what `ns.fn` should bind
|
|
// to (issue 0100). `decls` stays the full transitive list so
|
|
// the lowering pass can still resolve transitive callees.
|
|
.own_decls = other.own_decls,
|
|
// The aliased module's resolved path (== the `resolved_path`
|
|
// computed for this import). Retained for `buildImportFacts`.
|
|
.target_module_path = other.path,
|
|
// Carry the backtick raw escape from the `name :: #import …`
|
|
// form so a reserved-name namespace is exempt from the decl
|
|
// check, symmetric to every other decl site (issue 0089).
|
|
.is_raw = is_raw,
|
|
} },
|
|
};
|
|
try self.scope.put(name, {});
|
|
try seen_list.put(name, {});
|
|
try list.append(allocator, ns_node);
|
|
try own_list.append(allocator, ns_node);
|
|
return true;
|
|
}
|
|
|
|
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);
|
|
|
|
/// Per-module function identity index: function NAME → the `*const FnDecl` that
|
|
/// module AUTHORS. Mirrors a single module's slice of `module_scopes`.
|
|
pub const FnIndex = std.StringHashMap(*const ast.FnDecl);
|
|
|
|
/// `path → name → *const FnDecl`, mirroring `module_scopes`. One entry per
|
|
/// resolved module keyed by its path (a directory's combined module keyed by
|
|
/// `dir_path`); each entry indexes only what that module AUTHORS. Two modules
|
|
/// each authoring `f` are retained under their own paths — the identity index
|
|
/// fix-0102c's bare-name disambiguation consults to bind a flat call to the
|
|
/// right author.
|
|
pub const ModuleFns = std.StringHashMap(FnIndex);
|
|
|
|
/// Index a single module's authored functions (`own_decls`) into `out[path]`.
|
|
/// First-wins WITHIN a module mirrors the scan pass; cross-module same-name
|
|
/// authors live under their own `path` keys.
|
|
fn indexModuleFns(allocator: std.mem.Allocator, out: *ModuleFns, path: []const u8, own_decls: []const *Node) !void {
|
|
const gop = try out.getOrPut(path);
|
|
if (!gop.found_existing) gop.value_ptr.* = FnIndex.init(allocator);
|
|
for (own_decls) |decl| {
|
|
const fd = fnDeclOf(decl) orelse continue;
|
|
const name = decl.data.declName() orelse continue;
|
|
if (gop.value_ptr.contains(name)) continue;
|
|
try gop.value_ptr.put(name, fd);
|
|
}
|
|
}
|
|
|
|
/// Build the per-module function index from a resolved program: the main module
|
|
/// (keyed by `main_path`) plus every cached module (keyed by its own path).
|
|
/// Mirrors how `core.zig` fills `module_scopes` from `mod.scope` + the cache.
|
|
pub fn buildModuleFns(allocator: std.mem.Allocator, main_path: []const u8, main_mod: ResolvedModule, cache: *const ModuleCache, out: *ModuleFns) !void {
|
|
try indexModuleFns(allocator, out, main_path, main_mod.own_decls);
|
|
var it = cache.iterator();
|
|
while (it.next()) |entry| {
|
|
try indexModuleFns(allocator, out, entry.key_ptr.*, entry.value_ptr.own_decls);
|
|
}
|
|
}
|
|
|
|
// ── Raw import facts (the unified-resolver store; nothing consumes it yet) ──
|
|
//
|
|
// `buildImportFacts` produces two source-keyed views over the resolved program,
|
|
// callable WITHOUT IR lowering (the LSP reuses it later): a scalar per-module
|
|
// raw-decl index (`name → RawDeclRef`) and the namespace import edges
|
|
// (`importer → alias → NamespaceTarget`). Both are built from each module's
|
|
// `own_decls` — exactly the same modules `buildModuleFns` walks.
|
|
|
|
/// A named top-level declaration the resolver may select, kept as the raw AST
|
|
/// node pointer (NOT pre-classified — a `const_decl` whose value is a function
|
|
/// stays a `.const_decl`; classification is a later phase's job). `impl_block`
|
|
/// is deliberately absent: it has no `declName` and is deduped by node identity
|
|
/// (`mergeFlat`), so it never enters the scalar index.
|
|
pub const RawDeclRef = union(enum) {
|
|
fn_decl: *const ast.FnDecl,
|
|
const_decl: *const ast.ConstDecl,
|
|
struct_decl: *const ast.StructDecl,
|
|
enum_decl: *const ast.EnumDecl,
|
|
union_decl: *const ast.UnionDecl,
|
|
error_set_decl: *const ast.ErrorSetDecl,
|
|
protocol_decl: *const ast.ProtocolDecl,
|
|
foreign_class_decl: *const ast.ForeignClassDecl,
|
|
namespace_decl: *const ast.NamespaceDecl,
|
|
};
|
|
|
|
/// A raw declaration paired with the source file that authors it.
|
|
pub const RawAuthor = struct { raw: RawDeclRef, source: []const u8 };
|
|
|
|
/// One module's scalar raw-decl index: `name → ONE RawDeclRef`. Scalar because
|
|
/// `addOwnDecl` refuses a same-module same-name second author (returns false),
|
|
/// so a module's `own_decls` carries at most one author per name. Cross-module
|
|
/// multiplicity lives one level up, keyed by path in `ModuleDecls`.
|
|
pub const ModuleRawDeclIndex = struct { source: []const u8, names: std.StringHashMap(RawDeclRef) };
|
|
|
|
/// `path → ModuleRawDeclIndex`. Two modules each authoring `f` are retained
|
|
/// under their own paths — the cross-module same-name authors the unified
|
|
/// resolver's collector will surface.
|
|
pub const ModuleDecls = std.StringHashMap(ModuleRawDeclIndex);
|
|
|
|
/// One namespace import edge: `alias :: #import "…"` (or `alias :: #import c …`).
|
|
/// `target_module_path` is captured at resolution time (otherwise lost — it is
|
|
/// not derivable from the namespace node alone). `is_pub` stays false until the
|
|
/// `pub`-import phase lands the front-end form.
|
|
pub const NamespaceTarget = struct {
|
|
alias: []const u8,
|
|
importer_source: []const u8,
|
|
target_module_path: []const u8,
|
|
own_decls: []const *Node,
|
|
is_pub: bool = false,
|
|
};
|
|
|
|
/// `importer_source → alias → NamespaceTarget`.
|
|
pub const NamespaceEdges = std.StringHashMap(std.StringHashMap(NamespaceTarget));
|
|
|
|
/// The `RawDeclRef` a top-level node carries, or null when the node is not a
|
|
/// selectable named declaration (e.g. `impl_block`, `var_decl`, `ufcs_alias`,
|
|
/// a flat `c_import_decl`). Public so the unified resolver's namespace collector
|
|
/// can classify a `NamespaceTarget.own_decls` node without re-deriving the map.
|
|
pub fn rawDeclRefOf(decl: *const Node) ?RawDeclRef {
|
|
return switch (decl.data) {
|
|
.fn_decl => |*d| .{ .fn_decl = d },
|
|
.const_decl => |*d| .{ .const_decl = d },
|
|
.struct_decl => |*d| .{ .struct_decl = d },
|
|
.enum_decl => |*d| .{ .enum_decl = d },
|
|
.union_decl => |*d| .{ .union_decl = d },
|
|
.error_set_decl => |*d| .{ .error_set_decl = d },
|
|
.protocol_decl => |*d| .{ .protocol_decl = d },
|
|
.foreign_class_decl => |*d| .{ .foreign_class_decl = d },
|
|
.namespace_decl => |*d| .{ .namespace_decl = d },
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
/// Index one module's authored decls (`own_decls`) into `decls[path]` and record
|
|
/// any namespace aliases into `ns_edges[path]`. First-wins WITHIN a module
|
|
/// mirrors `indexModuleFns`; `own_decls` is already name-deduped by `addOwnDecl`,
|
|
/// so the first-wins guard never actually fires here.
|
|
fn indexModuleDecls(
|
|
allocator: std.mem.Allocator,
|
|
decls: *ModuleDecls,
|
|
ns_edges: *NamespaceEdges,
|
|
path: []const u8,
|
|
own_decls: []const *Node,
|
|
) !void {
|
|
const gop = try decls.getOrPut(path);
|
|
if (!gop.found_existing) gop.value_ptr.* = .{ .source = path, .names = std.StringHashMap(RawDeclRef).init(allocator) };
|
|
const index = gop.value_ptr;
|
|
for (own_decls) |decl| {
|
|
const ref = rawDeclRefOf(decl) orelse continue;
|
|
const name = decl.data.declName() orelse continue;
|
|
const name_gop = try index.names.getOrPut(name);
|
|
if (!name_gop.found_existing) name_gop.value_ptr.* = ref;
|
|
|
|
if (decl.data == .namespace_decl) {
|
|
const ns = &decl.data.namespace_decl;
|
|
const edge_gop = try ns_edges.getOrPut(path);
|
|
if (!edge_gop.found_existing) edge_gop.value_ptr.* = std.StringHashMap(NamespaceTarget).init(allocator);
|
|
const tgt_gop = try edge_gop.value_ptr.getOrPut(ns.name);
|
|
if (!tgt_gop.found_existing) tgt_gop.value_ptr.* = .{
|
|
.alias = ns.name,
|
|
.importer_source = path,
|
|
.target_module_path = ns.target_module_path,
|
|
.own_decls = ns.own_decls,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Build the raw import facts from a resolved program: the main module (keyed by
|
|
/// `main_path`) plus every cached module (keyed by its own path). The same module
|
|
/// set `buildModuleFns` walks. No IR lowering required.
|
|
pub fn buildImportFacts(
|
|
allocator: std.mem.Allocator,
|
|
main_path: []const u8,
|
|
main_mod: ResolvedModule,
|
|
cache: *const ModuleCache,
|
|
) !struct { decls: ModuleDecls, ns_edges: NamespaceEdges } {
|
|
var decls = ModuleDecls.init(allocator);
|
|
var ns_edges = NamespaceEdges.init(allocator);
|
|
try indexModuleDecls(allocator, &decls, &ns_edges, main_path, main_mod.own_decls);
|
|
var it = cache.iterator();
|
|
while (it.next()) |entry| {
|
|
try indexModuleDecls(allocator, &decls, &ns_edges, entry.key_ptr.*, entry.value_ptr.own_decls);
|
|
}
|
|
return .{ .decls = decls, .ns_edges = ns_edges };
|
|
}
|
|
|
|
/// Surface a same-module duplicate top-level declaration as a hard error at an
|
|
/// explicit name + span. `addOwnDecl` / `addNamespace` return `false` when the
|
|
/// name is already in this module's scope and drop the second author; without
|
|
/// this the drop is silent, and the scalar `ModuleRawDeclIndex` would lose an
|
|
/// authored name with no diagnostic.
|
|
fn reportDuplicateName(diagnostics: ?*errors.DiagnosticList, added: bool, name: []const u8, span: ast.Span) void {
|
|
if (added) return;
|
|
const diags = diagnostics orelse return;
|
|
diags.addFmt(.err, span, "duplicate top-level declaration '{s}'", .{name});
|
|
}
|
|
|
|
/// Stamp the DEFINING module path onto a function body node, so a later
|
|
/// pack/comptime monomorphization can pin `current_source_file` to the body's
|
|
/// own module and resolve its bare names in that module's visibility context
|
|
/// (issue 0106) — mirroring how a normally-declared function carries
|
|
/// `Function.source_file`. Only top-level decl Nodes are otherwise stamped, so
|
|
/// the body Node would carry no source; a null body source after this means a
|
|
/// synthesized/sourceless decl (the monomorphizer then keeps its caller's
|
|
/// context, the legitimate fall-open).
|
|
fn stampFnBodySource(decl: *Node, file_path: []const u8) void {
|
|
switch (decl.data) {
|
|
.fn_decl => |fd| fd.body.source_file = file_path,
|
|
.const_decl => |cd| switch (cd.value.data) {
|
|
.fn_decl => |fd| fd.body.source_file = file_path,
|
|
else => {},
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
/// `reportDuplicateName` keyed off a node whose `declName()` carries the name
|
|
/// (the regular authored-decl sites; an `import_decl` has no `declName`, so a
|
|
/// namespace alias must use `reportDuplicateName` with the alias directly).
|
|
fn reportDuplicateDecl(diagnostics: ?*errors.DiagnosticList, added: bool, decl: *const Node) void {
|
|
if (added) return;
|
|
const name = decl.data.declName() orelse return;
|
|
reportDuplicateName(diagnostics, added, name, decl.span);
|
|
}
|
|
|
|
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)),
|
|
flat_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));
|
|
}
|
|
}
|
|
// FLAT-only edge set: identical to `import_graph` but records ONLY bare
|
|
// `#import "…"` edges (`imp.name == null`), never a namespaced
|
|
// `ns :: #import "…"`. fix-0102c's bare-name disambiguation walks this to
|
|
// decide which same-name authors a flat importer can actually reach.
|
|
if (flat_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);
|
|
// Node-identity set for the same purpose, covering anonymous decls
|
|
// (impl blocks) that carry no name to dedupe on.
|
|
var seen_nodes = std.AutoHashMap(*Node, 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_slice = try ns_decls.toOwnedSlice(allocator);
|
|
const ns_node = try allocator.create(Node);
|
|
ns_node.* = .{
|
|
.span = decl.span,
|
|
.data = .{ .namespace_decl = .{
|
|
.name = ns_name,
|
|
.decls = ns_slice,
|
|
// A C-import namespace authors exactly the wrapped fn
|
|
// decls — they ARE its own decls (issue 0100).
|
|
.own_decls = ns_slice,
|
|
// No separate sx module: the synthesized members are
|
|
// authored in THIS file. Record the importer's path.
|
|
.target_module_path = file_path,
|
|
.is_raw = ci.is_raw,
|
|
} },
|
|
};
|
|
ns_node.source_file = file_path;
|
|
if (mod.scope.contains(ns_name)) {
|
|
reportDuplicateName(diagnostics, false, ns_name, decl.span);
|
|
} else {
|
|
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;
|
|
reportDuplicateDecl(diagnostics, try mod.addOwnDecl(allocator, &decl_list, &own_decl_list, &seen_in_list, fd), fd);
|
|
}
|
|
decl.source_file = file_path;
|
|
reportDuplicateDecl(diagnostics, try mod.addOwnDecl(allocator, &decl_list, &own_decl_list, &seen_in_list, decl), decl);
|
|
}
|
|
continue;
|
|
}
|
|
if (decl.data != .import_decl) {
|
|
decl.source_file = file_path;
|
|
stampFnBodySource(decl, file_path);
|
|
reportDuplicateDecl(diagnostics, try mod.addOwnDecl(allocator, &decl_list, &own_decl_list, &seen_in_list, decl), 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 {};
|
|
}
|
|
}
|
|
// The same edge, FLAT-only: recorded only for a bare `#import`
|
|
// (`imp.name == null`), excluding a namespaced `ns :: #import`. Covers
|
|
// both a flat file import and a flat directory import (`resolved_path`
|
|
// is the directory in the latter case).
|
|
if (imp.name == null) {
|
|
if (flat_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, flat_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, flat_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| {
|
|
const added = try mod.addNamespace(allocator, &decl_list, &own_decl_list, &seen_in_list, ns_name, imported_mod, decl.span, imp.is_raw);
|
|
reportDuplicateName(diagnostics, added, ns_name, decl.span);
|
|
} else {
|
|
try mod.mergeFlat(allocator, &decl_list, &seen_in_list, &seen_nodes, 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)),
|
|
flat_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);
|
|
var seen_nodes = std.AutoHashMap(*Node, 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, flat_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 (seen_nodes.contains(decl)) continue;
|
|
if (decl.data.declName()) |name| {
|
|
if (seen_in_list.contains(name)) continue;
|
|
try seen_in_list.put(name, {});
|
|
}
|
|
try seen_nodes.put(decl, {});
|
|
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;
|
|
}
|