Files
sx/issues/0121-pack-fn-alias-unresolved.md
agra b9cfe2554f refactor(ffi-linkage): Phase 9.3/9.4 — purge 'foreign' from issues/*.md; GATE PASS
Rewrote 20 issue writeups to the extern/runtime-class vocabulary (#foreign→extern,
foreign_class_map→runtime_class_map, parseForeignClassDecl→parseRuntimeClassDecl,
findForeignMethodInChain→findRuntimeMethodInChain, dedupeForeignSymbol→
dedupeExternSymbol, is_foreign_c_api→is_extern_c_api, stale filename refs to the
renamed examples, foreign-class→runtime-class, bare foreign→extern). Renamed
issues/0043-…-foreign-class-…→…-runtime-class-….

PHASE 9 COMPLETE — 9.4 GATE PASSES: zero 'foreign' across src/library/examples/
issues/docs/editors/specs/readme/CLAUDE, excluding only the SQLite API constant
SQLITE_CONSTRAINT_FOREIGNKEY + vendored sqlite3.c/.h (upstream third-party).
Suite green (644 corpus / 443 unit, 0 failed).
2026-06-15 11:18:35 +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) -> i64 {
    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), extern decl aliases (memcpy).