lang: fn aliases dispatch like their target (fix 0121) — scan-time registration through the shared alias-chain walk

Renamed fn aliases failed for EVERY kind (the filed pack-only scope was
a same-name confound: same-name re-exports already resolved through the
name-keyed fn_ast_map). scanDecls now follows ident-/ns.X-RHS const
alias chains (aliasedFnDecl; 0120's hop walk extracted as
followAliasChain) and registers the alias name in fn_ast_map
(absent-only), so every dispatch path — early pack/comptime/generic,
plain lazy-lower, plan-side typing — sees the target decl unchanged.
my_print :: s.print; / my_format :: s.format; now work (the std.sx
re-export shape). Regression: examples/0546 (+rich). Gates: zig build
test 0, suite 588/588.
This commit is contained in:
agra
2026-06-11 18:47:16 +03:00
parent f2db8ecc53
commit 721369a711
11 changed files with 264 additions and 53 deletions

View File

@@ -0,0 +1,6 @@
// Companion of 0546: the authoring module for the fn-alias re-exports.
#import "modules/std.sx";
helper :: () -> s64 { 7 }
first_of :: (xs: []$T) -> T { xs[0] }
my_pack :: (..$args) -> s64 { args[0] + args[1] }

View File

@@ -0,0 +1,35 @@
// Function aliases dispatch exactly like their target, across every fn
// kind: plain, runtime-generic ([]$T), and comptime-pack (..$args) — with
// bare, renamed, and namespace-member RHS. The alias is an ordinary own
// declaration, so it re-exports one flat-import level (companion file
// -rich.sx authors the fns aliased here and via the namespace).
// Regression (issue 0121): comptime-pack fn aliases (and ALL renamed fn
// aliases) used to fail "unresolved '<alias>'" — only same-name re-exports
// worked, through the name-keyed global registry.
#import "modules/std.sx";
s :: #import "modules/std.sx";
r :: #import "0546-packs-fn-alias-rich.sx";
pack_sum :: (..$args) -> s64 {
args[0] + args[1]
}
sum_alias :: pack_sum; // same-file pack alias (the 0121 repro)
helper2 :: r.helper; // renamed plain, namespace RHS
head_of :: r.first_of; // renamed runtime-generic, namespace RHS
sum2 :: r.my_pack; // renamed pack, namespace RHS
my_print :: s.print; // std's print — comptime pack + $fmt
my_format :: s.format; // value-returning sibling
main :: () {
print("pack: {}\n", sum_alias(3, 4));
print("plain: {}\n", helper2());
arr := .[10, 20, 30];
xs : []s64 = arr;
print("generic: {}\n", head_of(xs));
print("ns-pack: {}\n", sum2(20, 22));
my_print("std-print: {} {}\n", 1, "two");
t := my_format("std-format {}", 42);
my_print("{}\n", t);
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,6 @@
pack: 7
plain: 7
generic: 10
ns-pack: 42
std-print: 1 two
std-format 42

View File

@@ -0,0 +1,112 @@
# 0121 — aliasing a comptime-pack fn (`..$args`): "unresolved '<alias>'"
> **RESOLVED** (2026-06-11, same session — Agra-directed). The symptom
> was broader than filed: RENAMED fn aliases failed for EVERY fn kind
> (plain `helper2 :: r.helper;` too) — the "plain fns verified working"
> claim below was a same-name confound (same-name re-exports resolve
> through the name-keyed global `fn_ast_map`, no alias mechanism
> involved; `my_pack :: r.my_pack;` already worked for packs too).
> Fix: fn aliases register at SCAN time — `scanDecls`' const-decl arm
> follows ident-/`ns.X`-RHS alias chains via `aliasedFnDecl`
> (nominal.zig; shares 0120's hop walk, now extracted as
> `followAliasChain`) and, when the chain terminates at a fn decl,
> registers the ALIAS name in `fn_ast_map` (absent-only — a real
> same-name fn keeps its slot). Every dispatch path reads that map
> (early pack/comptime/generic, plain lazy-lower, plan-side typing),
> so the alias dispatches exactly like the target with no per-path
> changes. Verified matrix: same-file pack alias (the repro), renamed
> plain / generic / pack through a facade, and `my_print :: s.print;`
> / `my_format :: s.format;` over std's real pack fns. Regression:
> `examples/0546-packs-fn-alias.sx` (+ `-rich.sx` companion). Gates:
> zig build test 0, suite 588/588.
## Symptom
A const alias of a function whose signature carries a comptime pack
(`..$args`) is not callable — every call through the alias fails with
`unresolved '<alias>'`. All three alias shapes fail identically:
- same-file bare: `sum_alias :: pack_sum;``unresolved 'sum_alias'`
- bare RHS over a flat import: `my_print :: print;` (std's `print`) →
`unresolved 'my_print'`
- namespace RHS: `my_print :: s.print;` with `s :: #import
"modules/std.sx";` → `unresolved 'my_print'`
Contrast (all verified working): plain concrete fns (`helper ::
r.helper;`), runtime-generic fns (`first_of :: r.first_of;` with
`(xs: []$T) -> T`), and — since 0120 — generic struct heads
(`List :: list.List;`). Only the comptime-pack shape misses.
Expected: the alias dispatches exactly like the target —
`my_print("x {}\n", 1)` behaves as `print("x {}\n", 1)`. (If fn
aliasing of pack fns is NOT meant to be promised, the hypothesis is
wrong and the decl or the call should get a clean tailored
diagnostic instead — but the std.sx-as-pure-re-exports restructure
wants `print :: fmt.print;` to work, so support is the desirable
outcome. Confirm with Agra only if support turns out prohibitively
deep.)
## Reproduction
```sx
#import "modules/std.sx";
pack_sum :: (..$args) -> s64 {
args[0] + args[1]
}
sum_alias :: pack_sum;
main :: () {
print("{}\n", sum_alias(3, 4)); // error: unresolved 'sum_alias'
}
```
Direct `pack_sum(3, 4)` works; only the aliased spelling fails.
## Investigation prompt
Comptime-pack calls (`..$args` — NOT slice-variadics `..xs: []T`)
dispatch EARLY in call lowering, keyed on the callee NAME against the
pack-fn template registry (the `fn_ast_map` entry whose FnDecl has a
comptime pack param; the early-dispatch gate lives in the call path —
`src/ir/lower/call.zig` / `src/ir/packs.zig`, grep for the pack-param
detection on the callee). An alias name has no `fn_ast_map` entry, so
the early pack dispatch misses; no later stage handles pack fns
(they cannot lower as ordinary declared functions — each call site
expands with its own bound pack), so the call falls through to the
generic `unresolved '<name>'`.
The fix likely: where the early pack dispatch resolves the callee
name, on a miss follow const-ALIAS decls to their target FnDecl and
dispatch with the TARGET's fd under the alias's call node. Issue
0120's fix added exactly this follow for generic STRUCT heads —
`aliasedStructTemplate` in `src/ir/lower/nominal.zig`
(`singleVisibleAuthor` + hop-by-hop `followToTemplate`, each hop
resolved from the ALIAS AUTHOR's source, `namespaceAliasVerdictFrom`
for `ns.X` RHS, depth-capped). Mirror that shape for fn targets — a
`followToFnDecl` sibling reusing `singleVisibleAuthor` (consider
extracting the shared walk) — and route the early pack dispatch
through it. Mind: own-wins / single-flat collision semantics must
match 0120's (≥2 flat alias authors → loud, no silent pick), and the
ufcs-alias map (`name :: ufcs target;`) is a DIFFERENT mechanism
(`ufcs_alias_map`) — don't conflate.
Verification:
1. The repro prints `7`, exit 0.
2. Matrix: same-file bare alias, bare RHS over a flat import
(`my_print :: print;`), namespace RHS (`my_print :: s.print;` /
`my_format :: s.format;` — formats AND returns a value), and a
consumer one flat hop from the aliasing facade.
3. Plain-fn and generic-fn aliases unchanged (examples 0211 family).
4. `bash tests/run_examples.sh` — 587/587 baseline must hold; pin the
repro as a regression example per CLAUDE.md.
Context: BLOCKS the std.sx-as-pure-re-exports restructure — `print` /
`format` are the prelude's most-used names and are exactly this shape
(`($fmt: string, ..$args)`). Generic struct heads (`List`) were
unblocked by 0120; this is the remaining known gap. Still unprobed
for the restructure (next session, after this fix): protocol aliases
(`Allocator`, parameterized `Into`), `#builtin` decl aliases
(`size_of`, `out`, `string :: []u8`), `#foreign` decl aliases
(`memcpy`).

View File

@@ -486,9 +486,10 @@ carry does not chain through a second flat hop.
**Re-exporting through alias declarations.** Since visibility never chains,
a facade re-exports another module's members as its OWN declarations —
ordinary aliases, which its direct flat importers then see bare. This works
for functions, plain types, and generic struct heads alike (the generic
alias binds the same template, so instantiation and methods resolve
through it):
for functions of every kind (plain, generic, comptime-pack like `print`),
plain types, and generic struct heads alike (the generic alias binds the
same template, so instantiation and methods resolve through it), renamed
or same-name:
```sx
// facade.sx

View File

@@ -1270,6 +1270,23 @@ chain resolves with the visibility of the file that declares THAT hop,
not the use site's. Not yet supported: a qualified head whose namespace
member is itself an alias (`ns.BoxAlias(..)`).
### Function Aliases
Functions alias the same way — bare or namespace-member RHS, renamed or
same-name — and the alias dispatches exactly like the target. This covers
every fn kind: plain, runtime-generic (`[]$T` / `$T: Type`), and
comptime-pack (`..$args`, e.g. `print` / `format`):
```sx
s :: #import "modules/std.sx";
my_print :: s.print; // comptime-pack fn through a namespace
helper2 :: r.helper; // renamed plain fn
my_print("x = {}\n", helper2());
```
(For making an alias *dot-callable*, see `name :: ufcs target;` in the
UFCS section — that is a separate, explicit opt-in.)
### Generic Functions (Monomorphization)
Functions can be parameterized over types using `$T` syntax. The `$` prefix introduces a type parameter; subsequent uses of the name reference it.
```sx

View File

@@ -1688,6 +1688,7 @@ pub const Lowering = struct {
pub const buildGenericStructTemplate = lower_nominal.buildGenericStructTemplate;
pub const qualifiedStructTemplate = lower_nominal.qualifiedStructTemplate;
pub const aliasedStructTemplate = lower_nominal.aliasedStructTemplate;
pub const aliasedFnDecl = lower_nominal.aliasedFnDecl;
pub const qualifiedMemberMissing = lower_nominal.qualifiedMemberMissing;
pub const bareVisibleStructDecl = lower_nominal.bareVisibleStructDecl;
pub const bareVisibleStructTemplate = lower_nominal.bareVisibleStructTemplate;

View File

@@ -211,8 +211,7 @@ pub fn checkRequiredEntryPoints(self: *Lowering) void {
}
if (self.diagnostics) |diags| {
diags.addFmt(.err, null,
"target is Android but no `#jni_main` Activity declared. " ++
diags.addFmt(.err, null, "target is Android but no `#jni_main` Activity declared. " ++
"The OS launches a Java-side Activity that delegates lifecycle " ++
"callbacks into sx — declare one like:\n\n" ++
" Bundle :: #foreign #jni_class(\"android/os/Bundle\") {{ }}\n\n" ++
@@ -366,8 +365,7 @@ pub fn detectContextDecl(decls: []const *const Node) bool {
for (decls) |decl| {
const found = switch (decl.data) {
.struct_decl => |sd| std.mem.eql(u8, sd.name, "Context"),
.const_decl => |cd|
std.mem.eql(u8, cd.name, "Context") and cd.value.data == .struct_decl,
.const_decl => |cd| std.mem.eql(u8, cd.name, "Context") and cd.value.data == .struct_decl,
.namespace_decl => |ns| detectContextDecl(ns.decls),
else => false,
};
@@ -596,30 +594,48 @@ pub fn scanDecls(self: *Lowering, decls: []const *const Node) void {
}
}
self.putTypeAlias(self.current_source_file, cd.name, target_ty);
} else if (cd.value.data == .identifier) {
// Identifier-RHS alias: MyAlias :: MyInt; WideAlias :: Wide.
// SOURCE-AWARE (E1.5). Resolve the RHS `B` AS SEEN FROM this
// alias's OWN source via `selectNominalLeaf` (E1's source-
// keyed nominal leaf), NEVER the global `type_alias_map` /
// global `findByName` (last-wins across modules). Only the
// `.resolved` outcome is written; `.pending` (B is itself a
// forward alias not resolved yet), `.undeclared`, and
// `.not_visible` (a same-name B authored only by a namespaced
// import) leave A UNWRITTEN so the source-aware
// `resolveForwardIdentifierAliases` fixpoint re-tries A once
// the local B registers. A GLOBAL selection here would bind A
// to a namespaced same-name B, and the per-source fixpoint
// guard (`aliasResolvedInSource`) would then SKIP A — leaving
// the wrong global TypeId and re-opening 0105 one layer down
// (R1, E1.5). Same unified `putTypeAlias` writer (no-drift).
const rhs = cd.value.data.identifier;
} else if (cd.value.data == .identifier or cd.value.data == .field_access) {
// FN alias (issue 0121): `print2 :: print;` /
// `my_print :: s.print;`. When the alias chain terminates
// at a fn decl, register the ALIAS name in `fn_ast_map`
// pointing at the target's decl — every dispatch path
// (early pack/comptime/generic, plain lazy-lower,
// plan-side return typing) reads that map, so the alias
// dispatches exactly like the target. Absent-only: a real
// same-name fn keeps its slot (same-name re-exports are
// a no-op — the target already owns the name).
if (self.current_source_file orelse self.main_file) |from| {
switch (self.selectNominalLeaf(rhs.name, from, rhs.is_raw)) {
.resolved => |tid| self.putTypeAlias(self.current_source_file, cd.name, tid),
// `.ambiguous` (same-name RHS authored by ≥2 flat
// imports) leaves A unwritten like `.not_visible`;
// the loud diagnostic fires where A is USED.
.pending, .forward, .undeclared, .not_visible, .ambiguous => {},
if (self.aliasedFnDecl(&decl.data.const_decl, from)) |target_fd| {
if (!self.program_index.fn_ast_map.contains(cd.name)) {
self.program_index.fn_ast_map.put(cd.name, target_fd) catch {};
}
}
}
if (cd.value.data == .identifier) {
// Identifier-RHS alias: MyAlias :: MyInt; WideAlias :: Wide.
// SOURCE-AWARE (E1.5). Resolve the RHS `B` AS SEEN FROM this
// alias's OWN source via `selectNominalLeaf` (E1's source-
// keyed nominal leaf), NEVER the global `type_alias_map` /
// global `findByName` (last-wins across modules). Only the
// `.resolved` outcome is written; `.pending` (B is itself a
// forward alias not resolved yet), `.undeclared`, and
// `.not_visible` (a same-name B authored only by a namespaced
// import) leave A UNWRITTEN so the source-aware
// `resolveForwardIdentifierAliases` fixpoint re-tries A once
// the local B registers. A GLOBAL selection here would bind A
// to a namespaced same-name B, and the per-source fixpoint
// guard (`aliasResolvedInSource`) would then SKIP A — leaving
// the wrong global TypeId and re-opening 0105 one layer down
// (R1, E1.5). Same unified `putTypeAlias` writer (no-drift).
const rhs = cd.value.data.identifier;
if (self.current_source_file orelse self.main_file) |from| {
switch (self.selectNominalLeaf(rhs.name, from, rhs.is_raw)) {
.resolved => |tid| self.putTypeAlias(self.current_source_file, cd.name, tid),
// `.ambiguous` (same-name RHS authored by ≥2 flat
// imports) leaves A unwritten like `.not_visible`;
// the loud diagnostic fires where A is USED.
.pending, .forward, .undeclared, .not_visible, .ambiguous => {},
}
}
}
}
@@ -686,7 +702,7 @@ pub fn scanDecls(self: *Lowering, decls: []const *const Node) void {
// template via the namespace edge (mirrors the annotation
// head site `resolveParameterizedWithBindings`), not the
// bare last-wins `struct_template_map`.
const pt_alias: ?[]const u8 = if (pt_qualified) pt.name[0 .. std.mem.indexOfScalar(u8, pt.name, '.').?] else null;
const pt_alias: ?[]const u8 = if (pt_qualified) pt.name[0..std.mem.indexOfScalar(u8, pt.name, '.').?] else null;
// Generic-struct alias base: route layout selection through the
// single choke-point (CP-1); the builtin parameterised-type
// path (Vector etc.) stays as the non-generic fall-through.
@@ -1669,7 +1685,9 @@ pub fn selectNominalLeaf(self: *Lowering, name: []const u8, from: []const u8, ra
else => self.namedRefTid(fa.raw, name),
};
if (fa_tid) |t| {
if (found_tid) |f| { if (t != f) return .ambiguous; } else found_tid = t;
if (found_tid) |f| {
if (t != f) return .ambiguous;
} else found_tid = t;
} else {
flat_has_unregistered = true;
}

View File

@@ -434,35 +434,48 @@ pub fn aliasedStructTemplate(self: *Lowering, name: []const u8, from: []const u8
}
/// One alias hop: a generic-struct author terminates the chain with its
/// rebuilt source-pinned template; an alias author recurses on its RHS —
/// bare identifier from the author's own source, `ns.X` through the
/// author's namespace edge into the target module's own member. The depth
/// cap breaks alias cycles (`A :: B; B :: A;`).
/// rebuilt source-pinned template; an alias author recurses via
/// `followAliasChain`.
fn followToTemplate(self: *Lowering, author: resolver_mod.RawAuthor, depth: u8) ?StructTemplate {
const terminal = followAliasChain(self, author, depth) orelse return null;
const sd = structDeclOfRaw(terminal.raw) orelse return null;
if (sd.type_params.len == 0) return null;
return self.buildGenericStructTemplate(sd, terminal.source);
}
/// Walk a chain of const ALIAS decls to its terminal author. Each hop
/// resolves the RHS from the hop AUTHOR's own source — a bare identifier
/// via the visible-author walk, `ns.X` through the author's namespace edge
/// into the target module's own member. A non-alias author terminates the
/// chain (callers unwrap it by domain: `structDeclOfRaw` / `fnDeclOfRaw`).
/// The depth cap breaks alias cycles (`A :: B; B :: A;`).
pub fn followAliasChain(self: *Lowering, author: resolver_mod.RawAuthor, depth: u8) ?resolver_mod.RawAuthor {
if (depth == 0) return null;
if (structDeclOfRaw(author.raw)) |sd| {
if (sd.type_params.len == 0) return null;
return self.buildGenericStructTemplate(sd, author.source);
}
const cd = constAliasOfRaw(author.raw) orelse return null;
switch (cd.value.data) {
.identifier => |id| {
const next = singleVisibleAuthor(self, id.name, author.source) orelse return null;
return followToTemplate(self, next, depth - 1);
},
.field_access => |fa| {
if (fa.object.data != .identifier) return null;
const cd = constAliasOfRaw(author.raw) orelse return author;
const next: ?resolver_mod.RawAuthor = switch (cd.value.data) {
.identifier => |id| singleVisibleAuthor(self, id.name, author.source),
.field_access => |fa| blk: {
if (fa.object.data != .identifier) break :blk null;
const target = switch (self.namespaceAliasVerdictFrom(fa.object.data.identifier.name, author.source)) {
.target => |t| t,
.none, .ambiguous => return null,
.none, .ambiguous => break :blk null,
};
var res = self.resolver();
const member_set = res.collectNamespaceAuthors(target, fa.field);
const member = member_set.own orelse return null;
return followToTemplate(self, member, depth - 1);
break :blk member_set.own;
},
else => return null,
}
else => null,
};
return followAliasChain(self, next orelse return null, depth - 1);
}
/// The fn decl a const ALIAS chain terminates at, or null when `cd` is not
/// an alias of a function. Entry for fn-alias registration (issue 0121):
/// `cd` itself seeds the chain (it IS the first alias hop), `from` is its
/// declaring source.
pub fn aliasedFnDecl(self: *Lowering, cd: *const ast.ConstDecl, from: []const u8) ?*const ast.FnDecl {
const terminal = followAliasChain(self, .{ .raw = .{ .const_decl = cd }, .source = from }, 9) orelse return null;
return Lowering.fnDeclOfRaw(terminal.raw);
}
/// The bare-VISIBLE single generic-struct author of `name` (its `StructDecl` +