Merge branch 'flow/distribution/fix-0100'

This commit is contained in:
agra
2026-06-06 03:24:56 +03:00
21 changed files with 473 additions and 34 deletions

View File

@@ -0,0 +1,56 @@
// Two modules that each export a top-level `parse` — `std.cli.parse`
// (3-param subcommand dispatch) and `std.json.parse` (2-param document
// reader) — imported into ONE program under DISTINCT namespaces, with
// BOTH `parse`s actually called.
//
// Regression (issue 0100): same-name cross-module functions collided in
// the bare-name function table during IR lowering. Lowering re-resolved a
// call by SHORT name, so importing both modules and calling one bound the
// wrong-arity same-named function and tripped `lazyLowerFunction`'s
// param-count assert (panic). The fix resolves each `pkg.parse(...)` to a
// UNIQUE module-qualified FuncId, so `cli.parse` and `json.parse` are
// independent identities.
#import "modules/std.sx";
cli :: #import "modules/std/cli.sx";
json :: #import "modules/std/json.sx";
report :: (label: string, ok: bool) {
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
}
main :: () -> ! {
gpa := GPA.init();
arena := Arena.init(xx gpa, 8192);
defer arena.deinit();
// ── cli.parse: dispatch <group> <command> + a value flag ─────────
publish_flags : []FlagSpec = .[
FlagSpec.{ name = "out", takes_value = true, required = true },
];
cmds : []Command = .[
Command.{ group = "ci", command = "publish", flags = publish_flags },
Command.{ group = "ci", command = "status", flags = .[] },
];
argv : []string = .["ci", "publish", "--out", "dist"];
d : Diag = .{};
p := try cli.parse(argv, cmds, @d);
report("cli-group", p.group == "ci");
report("cli-command", p.command == "publish");
report("cli-index", p.cmd_index == 0);
report("cli-flag", p.value_of("out") == "dist");
// ── json.parse: read a small document into the value model ───────
doc := "{\"name\":\"sx\",\"xs\":[1,2,3]}";
root := try json.parse(doc, xx arena);
o := root.object;
report("json-members", o.len == 2);
report("json-key0", o.items[0].key == "name");
report("json-str", o.items[0].val.str == "sx");
xs := o.items[1].val.array;
report("json-arr-len", xs.len == 3);
report("json-arr-2", xs.items[2].int_ == 3);
print("=== DONE ===\n");
return;
}

View File

@@ -0,0 +1,26 @@
// Regression (issue 0100 F1): a QUALIFIED imported function that calls a
// function from its OWN flat import.
//
// `calc :: #import …` registers `calc.compute` as a module-qualified alias
// with a unique FuncId (the identity fix that resolves the cross-module
// same-name `parse` collision, issue 0100 / example 0719). That alias is
// lowered through `lazyLowerFunction`'s null-FuncId `lowerFunction` path,
// which has no declared `Function.source_file` to restore. Before the fix it
// lowered `calc.compute`'s body in the CALLER's (this file's) visibility
// context, so `compute`'s calls to `triple` / `base` — visible only from
// calc.sx's own `#import "util.sx"` — were rejected "not visible". The fix
// carries the alias's declaring source so it lowers in calc.sx's context.
#import "modules/std.sx";
calc :: #import "0720-modules-qualified-own-import/calc.sx";
report :: (label: string, ok: bool) {
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
}
main :: () -> s32 {
// 14 * 3 = 42, computed by calc.compute -> triple(base()), both of which
// live in calc.sx's own flat import.
report("qualified-own-import", calc.compute() == 42);
0
}

View File

@@ -0,0 +1,8 @@
// `calc` is pulled in under a QUALIFIED namespace by the consumer
// (`calc :: #import …`), yet its own body calls `triple` / `base` from
// calc.sx's OWN flat `#import "util.sx"`. The qualified alias `calc.compute`
// must lower in calc.sx's source context so those own-import callees stay
// visible — issue 0100 F1.
#import "util.sx";
compute :: () -> s64 { return triple(base()); }

View File

@@ -0,0 +1,2 @@
triple :: (x: s64) -> s64 { return x * 3; }
base :: () -> s64 { return 14; }

View File

@@ -0,0 +1,31 @@
// Regression (issue 0100 F2): lowering a QUALIFIED imported function whose
// body terminates must leave the CALLER's lowering state untouched.
//
// `m :: #import …` registers `m.foo` as a module-qualified alias with a unique
// FuncId (the identity fix, issue 0100 / example 0719) and lowers it through
// `lazyLowerFunction`'s null-FuncId `lowerFunction` path. `foo`'s body folds
// `if true { return helper(); }` to an unconditional return, so its lowering
// ends with `block_terminated = true`. The null-FuncId path used to restore
// every saved caller field EXCEPT `block_terminated`, so that flag leaked back
// into `main`, and `main`'s own trailing `print` / `return 0` were treated as
// dead-after-terminator — the compiler rejected `return 0` with "body produces
// no value". The fix routes all exit paths through one save/restore defer, so
// the qualified alias is transparent to the caller. (`helper` also lives in
// m.sx's own flat import, exercising the F1 source-context restore too.)
#import "modules/std.sx";
m :: #import "0721-modules-qualified-terminating-callee/m.sx";
report :: (label: string, ok: bool) {
if ok { print("{}: ok\n", label); } else { print("{}: FAIL\n", label); }
}
main :: () -> s32 {
// Qualified callee whose body terminates via a constant-folded `if true`.
x := m.foo();
// Caller statements AFTER the call must still be emitted (not dead).
report("terminating-callee", x == 7);
print("after\n");
// The caller's OWN return — rejected pre-fix because block_terminated leaked.
return 0;
}

View File

@@ -0,0 +1,3 @@
// Lives in m.sx's OWN flat import — reachable from `foo` but not from the
// top-level consumer that imports m.sx under a qualified namespace.
helper :: () -> s64 { return 7; }

View File

@@ -0,0 +1,10 @@
// `foo` is pulled in QUALIFIED by the consumer (`m :: #import …`). Its body
// terminates via a constant-folded `if true { return … }`, and the `return`
// calls `helper` from m.sx's OWN flat import. Lowering `foo` as a qualified
// alias must be transparent to the caller's lowering state — issue 0100 F2.
#import "helper.sx";
foo :: () -> s64 {
if true { return helper(); }
return 0;
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,10 @@
cli-group: ok
cli-command: ok
cli-index: ok
cli-flag: ok
json-members: ok
json-key0: ok
json-str: ok
json-arr-len: ok
json-arr-2: ok
=== DONE ===

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
qualified-own-import: ok

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,2 @@
terminating-callee: ok
after

View File

@@ -0,0 +1,178 @@
# 0100 — cross-module same-name function lowering collision
**RESOLVED.** Two modules each exporting a top-level function with the same
short name (`std.cli.parse`, 3 params; `std.json.parse`, 2 params) collided
in IR lowering's bare-name function table. `fn_ast_map` (short name → AST)
was **last-wins**, while `module.functions` / `resolveFuncByName` are
**first-wins**, so importing both modules and calling one bound the AST of
one function against the FuncId of the other and tripped
`lazyLowerFunction`'s param-count assert (`src/ir/lower.zig:1606`,
`func.params.len == fd.params.len + ctx_slots`) — `panic: reached
unreachable code`. Qualified imports (`j :: #import`) did not help: lowering
keyed everything by short name, so `j.parse` and a bare `parse` resolved to
the same colliding entry.
**Fix** (`src/ir/lower.zig`, `src/ast.zig`, `src/imports.zig`):
1. **Module-qualified identity.** A namespaced import's OWN plain functions
are now registered under their qualified name (`ns.fn`) in `fn_ast_map`,
giving `cli.parse` / `json.parse` independent identities. The qualified
resolution paths in `CallResolver.plan` and `lowerCall` already prefer
`ns.fn` — they just had nothing to find. `NamespaceDecl` carries the
module's `own_decls` (populated in `imports.addNamespace`) so the
registration covers authored decls, not transitive flat imports. Generic
/ comptime / pack / foreign functions are excluded — they dispatch by
monomorphization off the bare template name, not the plain
`resolveFuncByName` path, so a qualified alias would strand their
per-call type bindings. The qualified function is declared + lowered on
demand by `lazyLowerFunction`'s null-FuncId path (no eager `declareFunction`,
which would resolve types before the forward-alias fixpoint).
2. **First-wins bare registration.** `scanDecls` no longer lets a later
namespace recursion clobber an existing bare `fn_ast_map` entry, aligning
it with `mergeFlat` / `resolveFuncByName`. A bare `parse` with one module
flat-imported now consistently resolves to the first (unqualified-scope)
function instead of splitting AST/FuncId across modules.
Regression: `examples/0719-modules-cli-and-json.sx` imports BOTH `std.cli`
and `std.json` under distinct namespaces and calls both `cli.parse`
(dispatch) and `json.parse` (document read), asserting correct results.
Panics on pre-fix code; passes after.
## F1 follow-up — qualified alias must lower in its own source context
The identity fix above registered `ns.fn` in `fn_ast_map` WITHOUT an eager
`declareFunction`, so the qualified alias is lowered through
`lazyLowerFunction`'s **null-FuncId** `lowerFunction` path — which had no
`Function.source_file` to restore (the non-null path does
`setCurrentSourceFile(func.source_file)`). The alias therefore lowered in the
**caller's** visibility context, and a qualified function calling a helper
from **its own module's flat import** was rejected:
```
m :: #import "m.sx"; // m.sx: `#import "helper.sx"; foo :: () { helper() }`
main :: () -> s32 { print("{}\n", m.foo()); 0 } // → 'helper' is not visible
```
**Fix** (`src/ir/program_index.zig`, `src/ir/lower.zig`):
- New `ProgramIndex.qualified_fn_source` (qualified name → declaring source
file), populated in `registerQualifiedFn` from the decl's own source.
- `lazyLowerFunction`'s null-FuncId branch restores that source via
`setCurrentSourceFile` before calling `lowerFunction`, so `ns.fn`'s body
lowers in its own module's context and its own-import callees resolve.
- `lowerFunction` now records `Function.source_file = current_source_file`
on the freshly-begun function (matching `declareFunction`), so the lowered
alias carries its own module for diagnostics/emit.
Regression: `examples/0720-modules-qualified-own-import.sx``calc.compute`
(a qualified alias) calls `triple` / `base` from calc.sx's own flat import.
Reports `'triple' is not visible` on the attempt-1 code; passes after. 0719's
cross-module dual-`parse` assertion stays green.
## F2 follow-up — null-FuncId path must restore the FULL caller lowering state
The F1 fix patched the **source file** in `lazyLowerFunction`'s null-FuncId
branch, but that branch still restored only a SUBSET of the caller state the
non-null branch restores — it omitted `self.block_terminated`. A qualified
alias whose body terminates (e.g. a constant-folded `if true { return … }`)
leaves `block_terminated = true` after `lowerFunction`; the null branch then
returned without resetting it, so the flag leaked into the **caller's** body
lowering and the caller's own trailing statements / `return` were treated as
dead-after-terminator:
```
m :: #import "m.sx"; // m.sx: `#import "helper.sx"; foo :: () -> s64 { if true { return helper(); } return 0; }`
main :: () -> s32 {
x := m.foo();
print("after\n"); // dropped
return 0; // → error: body produces no value
}
```
**Fix** (`src/ir/lower.zig`): the three exit paths of `lazyLowerFunction` (the
null-FuncId branch, the already-promoted early return, and the bottom of the
non-null branch) duplicated the restore, and the null branch's copy drifted.
They are now collapsed into a **single `defer`** registered right after the
state is saved, so every exit path restores the identical full set and the
class can't diverge again. The fields the defer now restores on all paths:
- `current_source_file` (via `setCurrentSourceFile`, which also resyncs
`diagnostics.current_source_file`) — F1
- `scope`
- `func_defer_base`
- `block_terminated`**F2** (was missing on the null path)
- `force_block_value`
- `builder.func`
- `builder.current_block`
- `builder.inst_counter`
(The `current_foreign_class`, `jni_env_stack_base`, and pack-mono /
`inline_return_target` fields already had their own `defer`s and apply on all
paths; they are unchanged.)
Regression: `examples/0721-modules-qualified-terminating-callee.sx``m.foo`
(a qualified alias) folds `if true { return helper(); }` and is followed by
caller statements + the caller's own `return 0`. Reports `body produces no
value` on the attempt-2 code; prints `terminating-callee: ok` / `after` and
exits 0 after. 0719 and 0720 stay green.
## Symptom
- **Observed:** a program that imports two modules each exporting a
same-named top-level function AND calls one crashes IR lowering:
`panic: reached unreachable code` at `src/ir/lower.zig:1606`
(`lazyLowerFunction`) via `lowerCall`.
- **Expected:** each `pkg.fn(...)` resolves to its own module's function;
the program compiles and runs.
## Reproduction
```sx
#import "modules/std.sx";
cli :: #import "modules/std/cli.sx";
json :: #import "modules/std/json.sx";
main :: () -> s32 {
gpa := GPA.init();
arena := Arena.init(xx gpa, 8192);
defer arena.deinit();
cmds : []Command = .[ Command.{ group = "ci", command = "publish", flags = .[] } ];
argv : []string = .["ci", "publish"];
d : Diag = .{};
p, e := cli.parse(argv, cmds, @d); // 3-param cli.parse
if e { return 64; }
v, je := json.parse("[1,2,3]", xx arena); // 2-param json.parse
if je { return 65; }
return 0;
}
```
Pre-fix: `panic: reached unreachable code` at `src/ir/lower.zig:1606`.
Post-fix: compiles and runs (exit 0).
## Root cause
`fn_ast_map`, `module.functions` (matched by interned name), and
`lowered_functions` were all keyed by a function's SHORT name. Two functions
sharing a short name across modules occupied the same key; the `put`-order
mismatch (AST last-wins vs FuncId first-wins) drove `lazyLowerFunction` to
lower one signature against the other's body. The qualified-call resolution
machinery already existed but was never fed module-qualified entries.
## Fix verification
- `zig build` → 0
- `zig build test` → 0 (incl. LSP corpus sweep, 473 examples; 397/397 tests)
- `bash tests/run_examples.sh` → 456 passed, 0 failed
- `examples/0719-modules-cli-and-json.sx`: panics pre-fix, passes post-fix.
- `examples/0720-modules-qualified-own-import.sx`: `'… is not visible'` on
the attempt-1 code, passes after the F1 fix.
- `examples/0721-modules-qualified-terminating-callee.sx`: `body produces no
value` on the attempt-2 code, passes after the F2 fix.
Regression tests: `examples/0719-modules-cli-and-json.sx` (collision),
`examples/0720-modules-qualified-own-import.sx` (F1 own-import visibility),
`examples/0721-modules-qualified-terminating-callee.sx` (F2 terminating
qualified callee — caller state transparency).

View File

@@ -674,6 +674,12 @@ pub const SpreadExpr = struct {
pub const NamespaceDecl = struct {
name: []const u8,
decls: []const *Node,
/// Decls AUTHORED in the namespaced module itself (its `own_decls`), a
/// subset of `decls` (which also carries the module's transitive flat
/// imports). Lowering registers these under their module-qualified name
/// (`ns.fn`) so `pkg.fn(...)` resolves to a unique FuncId distinct from a
/// same-named function in another module (issue 0100).
own_decls: []const *Node = &.{},
/// True when the namespace NAME was a backtick raw identifier — exempt
/// from the reserved-type-name decl check (issue 0089).
is_raw: bool = false,

View File

@@ -362,6 +362,10 @@ pub const ResolvedModule = struct {
.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,
// 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).
@@ -486,12 +490,16 @@ pub fn resolveImports(
// 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 = try ns_decls.toOwnedSlice(allocator),
.decls = ns_slice,
// A C-import namespace authors exactly the wrapped fn
// decls — they ARE its own decls (issue 0100).
.own_decls = ns_slice,
.is_raw = ci.is_raw,
} },
};

View File

@@ -594,6 +594,7 @@ pub const Lowering = struct {
.namespace_decl => |ns| {
self.registerNamespacedForeignClasses(ns);
if (self.main_file != null) {
self.registerNamespaceQualifiedFns(ns.name, ns.own_decls);
self.lowerDecls(ns.decls);
}
},
@@ -691,15 +692,26 @@ pub const Lowering = struct {
false;
switch (decl.data) {
.fn_decl => |fd| {
self.program_index.fn_ast_map.put(fd.name, &decl.data.fn_decl) catch {};
self.program_index.import_flags.put(fd.name, is_imported) catch {};
// First-wins on a bare-name collision, matching `mergeFlat`
// and `resolveFuncByName`. A later namespace recursion that
// re-introduces a same-named function (e.g. a second module
// also exporting `parse`) must NOT clobber the AST while the
// function table keeps the first — that split lowers one
// signature against the other's body (issue 0100). The
// shadowed function stays reachable via its qualified name.
if (!self.program_index.fn_ast_map.contains(fd.name)) {
self.program_index.fn_ast_map.put(fd.name, &decl.data.fn_decl) catch {};
self.program_index.import_flags.put(fd.name, is_imported) catch {};
}
// Declare extern stub for all functions (bodies lowered lazily)
self.declareFunction(&fd, fd.name);
},
.const_decl => |cd| {
if (cd.value.data == .fn_decl) {
self.program_index.fn_ast_map.put(cd.name, &cd.value.data.fn_decl) catch {};
self.program_index.import_flags.put(cd.name, is_imported) catch {};
if (!self.program_index.fn_ast_map.contains(cd.name)) {
self.program_index.fn_ast_map.put(cd.name, &cd.value.data.fn_decl) catch {};
self.program_index.import_flags.put(cd.name, is_imported) catch {};
}
self.declareFunction(&cd.value.data.fn_decl, cd.name);
} else if (cd.value.data == .struct_decl) {
self.registerStructDecl(&cd.value.data.struct_decl);
@@ -884,6 +896,7 @@ pub const Lowering = struct {
self.registerNamespacedForeignClasses(ns);
if (self.main_file != null) {
self.scanDecls(ns.decls);
self.registerNamespaceQualifiedFns(ns.name, ns.own_decls);
}
},
.ufcs_alias => |ua| {
@@ -1421,6 +1434,65 @@ pub const Lowering = struct {
func.has_implicit_ctx = wants_ctx;
}
/// Register a namespaced import's OWN functions under their module-qualified
/// name (`ns.fn`), giving each a UNIQUE FuncId in the function table. Two
/// modules each exporting a top-level `parse` otherwise collide in the
/// bare-name `fn_ast_map` / function table (last-wins) while `resolveFuncByName`
/// picks the first declared, so `lazyLowerFunction` lowers one signature
/// against the other's body and trips its param-count assert (issue 0100).
/// The bare recursion in `scanDecls` still registers intra-module bare calls;
/// this adds the qualified identity the `pkg.fn(...)` resolution paths in
/// `CallResolver.plan` / `lowerCall` already prefer.
fn registerNamespaceQualifiedFns(self: *Lowering, ns_name: []const u8, own_decls: []const *Node) void {
const saved_source = self.current_source_file;
defer self.setCurrentSourceFile(saved_source);
for (own_decls) |decl| {
self.setCurrentSourceFile(decl.source_file);
switch (decl.data) {
.fn_decl => self.registerQualifiedFn(ns_name, &decl.data.fn_decl, decl.data.fn_decl.name),
.const_decl => |cd| {
if (cd.value.data == .fn_decl) {
self.registerQualifiedFn(ns_name, &cd.value.data.fn_decl, cd.name);
}
},
else => {},
}
}
}
fn registerQualifiedFn(self: *Lowering, ns_name: []const u8, fd: *const ast.FnDecl, short: []const u8) void {
// Only PLAIN free functions need a qualified identity. Generic /
// comptime / pack functions (`Vector`, `print`, `any_to_string`) are
// dispatched by monomorphization off their BARE template name, not the
// plain `resolveFuncByName` / `lazyLowerFunction` path that trips the
// collision assert (issue 0100); registering a qualified alias for them
// would divert that machinery and strand a per-call type binding.
if (fd.type_params.len > 0 or hasComptimeParams(fd) or isPackFn(fd)) return;
// Foreign / builtin / #compiler bodies keep their literal name; a
// qualified alias has no distinct symbol to resolve to.
switch (fd.body.data) {
.foreign_expr, .builtin_expr, .compiler_expr => return,
else => {},
}
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ ns_name, short }) catch return;
if (self.program_index.fn_ast_map.contains(qualified)) return;
self.program_index.fn_ast_map.put(qualified, fd) catch {};
self.program_index.import_flags.put(qualified, true) catch {};
// Carry the alias's OWN declaring source file (the caller in
// `registerNamespaceQualifiedFns` pins `current_source_file` to the
// decl's source before each call). `lazyLowerFunction`'s null-FuncId
// path restores this so `ns.fn`'s body lowers in its own module's
// visibility context, not the call site's (issue 0100 F1).
if (self.current_source_file) |src| {
self.program_index.qualified_fn_source.put(qualified, src) catch {};
}
// No eager `declareFunction` here: the extern stub's param/return types
// would be resolved now, before the forward-alias fixpoint, caching an
// `.unresolved` for any type declared later in the module. The qualified
// function is declared + lowered on demand by `lazyLowerFunction`'s
// null-FuncId path (`lowerFunction`), which runs after all types resolve.
}
/// Check if a C-imported function is visible from the current source file.
/// Returns true for non-C functions (always visible) or if no scoping info available.
fn isCImportVisible(self: *Lowering, fn_name: []const u8) bool {
@@ -1518,6 +1590,25 @@ pub const Lowering = struct {
const saved_block_terminated = self.block_terminated;
const saved_force_block_value = self.force_block_value;
const saved_source_file = self.current_source_file;
// Lowering a callee must be transparent to the caller's lowering
// state: restore the FULL saved context on EVERY exit path through one
// defer so the three exits (non-null branch, already-promoted early
// return, null-FuncId `ns.fn` alias branch) cannot drift. Notably
// `block_terminated` — a qualified alias whose body terminates (e.g. a
// constant-folded `if true { return … }`) leaves it true, and leaking
// that into the caller marks the caller's own trailing statements
// dead-after-terminator (issue 0100 F2). The jni/pack/foreign-class
// fields keep their own defers above.
defer {
self.setCurrentSourceFile(saved_source_file);
self.scope = saved_scope;
self.func_defer_base = saved_defer_base;
self.block_terminated = saved_block_terminated;
self.force_block_value = saved_force_block_value;
self.builder.func = saved_func;
self.builder.current_block = saved_block;
self.builder.inst_counter = saved_counter;
}
// The `#jni_env` Ref stack is lexical within ONE function's instruction
// stream — Refs from the caller don't dereference correctly in this
// callee's body. Move the visible base to the current top so
@@ -1566,17 +1657,19 @@ pub const Lowering = struct {
}
if (func_id == null) {
// Function not yet declared — create it fresh via lowerFunction
// Function not yet declared — create it fresh via lowerFunction.
// A module-qualified alias (`ns.fn`, issue 0100) is registered in
// `fn_ast_map` without an eager `declareFunction`, so there's no
// `Function.source_file` to switch to (the path above). Restore the
// alias's OWN declaring source before lowering its body, otherwise
// it lowers in the caller's visibility context and an own-import
// callee (`foo` calling `helper` from `foo`'s module's flat import)
// is reported "not visible" (issue 0100 F1).
if (self.program_index.qualified_fn_source.get(name)) |src| {
self.setCurrentSourceFile(src);
}
self.lowerFunction(fd, name, false);
// Restore builder state
self.setCurrentSourceFile(saved_source_file);
self.scope = saved_scope;
self.func_defer_base = saved_defer_base;
self.force_block_value = saved_force_block_value;
self.builder.func = saved_func;
self.builder.current_block = saved_block;
self.builder.inst_counter = saved_counter;
return;
return; // caller state restored by the top-level defer
}
if (func_id) |fid| {
@@ -1585,15 +1678,8 @@ pub const Lowering = struct {
const func = &self.module.functions.items[@intFromEnum(fid)];
self.setCurrentSourceFile(func.source_file);
if (!func.is_extern) {
// Already promoted (e.g., via lowerComptimeDeps) — skip
self.setCurrentSourceFile(saved_source_file);
self.scope = saved_scope;
self.func_defer_base = saved_defer_base;
self.block_terminated = saved_block_terminated;
self.force_block_value = saved_force_block_value;
self.builder.func = saved_func;
self.builder.current_block = saved_block;
self.builder.inst_counter = saved_counter;
// Already promoted (e.g., via lowerComptimeDeps) — skip.
// Caller state restored by the top-level defer.
return;
}
func.is_extern = false; // promote from extern stub to real function
@@ -1663,16 +1749,7 @@ pub const Lowering = struct {
self.builder.finalize();
}
// Restore builder state
self.setCurrentSourceFile(saved_source_file);
self.scope = saved_scope;
self.func_defer_base = saved_defer_base;
self.block_terminated = saved_block_terminated;
self.force_block_value = saved_force_block_value;
self.builder.func = saved_func;
self.builder.current_block = saved_block;
self.builder.inst_counter = saved_counter;
// Caller state restored by the top-level defer.
}
/// Lower a single function declaration.
@@ -1739,6 +1816,11 @@ pub const Lowering = struct {
);
_ = func_id;
self.builder.currentFunc().has_implicit_ctx = wants_ctx;
// Record the declaring source so the function carries its own module
// for diagnostics/emit and for any later `lazyLowerFunction` re-entry
// that switches to `func.source_file`. The caller sets
// `current_source_file` to the decl's source before lowering (issue 0100 F1).
self.builder.currentFunc().source_file = self.current_source_file;
// Set linkage. Default for fn defs is `internal` (LLVM DCE-friendly,
// matches C `static`). isExportedEntryName lists the names the OS

View File

@@ -591,6 +591,14 @@ pub const ProgramIndex = struct {
// ── Declaration maps ──
/// Function name → AST decl.
fn_ast_map: std.StringHashMap(*const ast.FnDecl),
/// Module-qualified function name (`ns.fn`) → its declaring source file.
/// A qualified alias is registered in `fn_ast_map` WITHOUT an eager
/// `declareFunction`, so `lazyLowerFunction` lowers it through the
/// null-FuncId `lowerFunction` path with no `Function.source_file` to
/// restore. This carries the alias's OWN module source so its body lowers
/// in the right visibility context — its intra-module / own-import callees
/// resolve (issue 0100 F1). Keyed/allocated with the lowering allocator.
qualified_fn_source: std.StringHashMap([]const u8),
/// sx alias → ForeignClassDecl (jni_class / objc_class / swift_class / ... — registered in scan pass).
foreign_class_map: std.StringHashMap(*const ast.ForeignClassDecl) = std.StringHashMap(*const ast.ForeignClassDecl).init(std.heap.page_allocator),
/// `#run` global name → GlobalId.
@@ -613,6 +621,7 @@ pub const ProgramIndex = struct {
return .{
.import_flags = std.StringHashMap(bool).init(alloc),
.fn_ast_map = std.StringHashMap(*const ast.FnDecl).init(alloc),
.qualified_fn_source = std.StringHashMap([]const u8).init(alloc),
.global_names = std.StringHashMap(GlobalInfo).init(alloc),
};
}
@@ -621,6 +630,7 @@ pub const ProgramIndex = struct {
// Owned maps only — module_scopes / import_graph are borrowed.
self.import_flags.deinit();
self.fn_ast_map.deinit();
self.qualified_fn_source.deinit();
self.foreign_class_map.deinit();
self.global_names.deinit();
self.type_alias_map.deinit();