Files
sx/issues/0121-pack-fn-alias-unresolved.md
agra 721369a711 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.
2026-06-11 18:47:16 +03:00

5.2 KiB

0121 — aliasing a comptime-pack fn (..$args): "unresolved ''"

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

#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).