Files
sx/src/ir/lower/decl.zig
agra cd147942e4 refactor(ffi-linkage): Phase 9.1c — delete dead VarDecl legacy foreign fields
VarDecl carried BOTH the legacy is_foreign/foreign_lib/foreign_name AND the new
is_extern/extern_lib/extern_name (parallel forms coalesced during the migration).
The global #foreign parse path now rejects, so the legacy trio is write-dead and
read in only 3 coalescing sites (decl.zig). Simplified those readers
(vd.extern_name orelse vd.name; vd.is_extern) and deleted the dead fields. Build
confirms no other setter/reader. Snapshot-neutral; suite green (646/444).

Remaining linkage (9.1): foreign_expr (25, still built by c_import.zig auto-synth)
+ ForeignClassDecl.is_foreign (runtime-class, → 9.2). Runtime-class family (9.2,
Decision 5) is the big remaining src/ rename.
2026-06-15 08:46:59 +03:00

2694 lines
142 KiB
Zig

const std = @import("std");
const Allocator = std.mem.Allocator;
const ast = @import("../../ast.zig");
const Node = ast.Node;
const types = @import("../types.zig");
const inst_mod = @import("../inst.zig");
const mod_mod = @import("../module.zig");
const type_bridge = @import("../type_bridge.zig");
const unescape = @import("../../unescape.zig");
const errors = @import("../../errors.zig");
const program_index_mod = @import("../program_index.zig");
const resolver_mod = @import("../resolver.zig");
const ProgramIndex = program_index_mod.ProgramIndex;
const GlobalInfo = program_index_mod.GlobalInfo;
const ModuleConstInfo = program_index_mod.ModuleConstInfo;
const TypeResolver = @import("../type_resolver.zig").TypeResolver;
const CallResolver = @import("../calls.zig").CallResolver;
const ProtocolResolver = @import("../protocols.zig").ProtocolResolver;
const ErrorFlow = @import("../error_flow.zig").ErrorFlow;
const semantic_diagnostics = @import("../semantic_diagnostics.zig");
const TypeId = types.TypeId;
const StringId = types.StringId;
const Ref = inst_mod.Ref;
const FuncId = inst_mod.FuncId;
const Function = inst_mod.Function;
const Module = mod_mod.Module;
const lower = @import("../lower.zig");
const Lowering = lower.Lowering;
const Scope = lower.Scope;
const nameVisibleOverEdges = lower.nameVisibleOverEdges;
const SelectedConst = Lowering.SelectedConst;
const FnBodyReentry = Lowering.FnBodyReentry;
const hasComptimeParams = Lowering.hasComptimeParams;
const isPlainFreeFn = Lowering.isPlainFreeFn;
const topLevelTypeDecl = Lowering.topLevelTypeDecl;
const isFloat = Lowering.isFloat;
const isPackFn = Lowering.isPackFn;
/// Names that must keep external LLVM linkage because the OS loader (not
/// sx code) is the caller. Without this they'd default to internal and
/// either DCE away or stay hidden from the dynamic symbol table.
/// Anything starting with `Java_` is a JNI native method that Android's
/// runtime resolves by name mangling — same rule.
fn isExportedEntryName(name: []const u8) bool {
return std.mem.eql(u8, name, "main") or
std.mem.eql(u8, name, "JNI_OnLoad") or
std.mem.startsWith(u8, name, "Java_");
}
/// Lower all top-level declarations from a root node.
/// Pass 1: Scan all declarations (register ASTs, types, extern stubs).
/// Pass 2: Lower only `main` (everything else is lowered lazily on demand).
pub fn lowerRoot(self: *Lowering, root: *const Node) void {
const decls = switch (root.data) {
.root => |r| r.decls,
else => return,
};
// Pass 0: pre-scan for `Context :: struct {...}`. If the program
// imports `std.sx` it has Context, and every default-conv sx
// function gets the implicit `__sx_ctx` param. Otherwise the
// implicit-ctx machinery stays fully disabled — programs that
// call only libc directly keep their bare C ABI.
self.implicit_ctx_enabled = detectContextDecl(decls);
self.module.has_implicit_ctx = self.implicit_ctx_enabled;
// Pass 1: scan — register all function ASTs, struct types, extern stubs
self.scanDecls(decls);
// Pass 1b: inject compile-time constants (OS, ARCH, POINTER_SIZE) from target config
self.injectComptimeConstants();
// Pass 1c: emit the process-wide default Context global, statically
// initialised to a CAllocator-backed Allocator value. Used by FFI
// wrappers in Step 4 and by the interp's `callWithDefaultContext`
// entry. Only fires when the program imports `std.sx` (so Context +
// Allocator + CAllocator are all registered).
self.emitDefaultContextGlobal();
// Pass 1d: converge inferred (`bare !`) error sets across the whole
// program (ERR E1.4b). Runs before body lowering so `lowerTry`'s
// named-caller widening sees each bare-`!` callee's converged set; also
// emits the empty-inferred warning.
self.convergeInferredErrorSets();
// Pass 1d': converge inferred (`bare !`) error sets per closure/fn-type
// SHAPE (ERR E5.1 sub-feature 2). Runs after the name-keyed pass so a
// closure's `try named_fn()` edge resolves against the converged
// top-level sets; before body lowering so `try slot(x)` widening sees
// the full per-shape union.
self.convergeClosureShapeSets();
// Pass 1e: error-flow checks (ERR E1.8 value-slot liveness + E1.7
// cleanup-body absorption) over the main file's functions. Runs after
// the error-set convergence passes (so failable callees resolve) and
// before body lowering — purely a diagnostic pass; `core.zig` halts on
// any error before codegen.
self.errorFlow().checkErrorFlow(decls);
// Pass 1f: reject identifiers used in a type position that name no
// declared type / primitive / in-scope generic param.
// Runs after scanning (so every real type name is registered) and
// before body lowering, so the diagnostic halts via `core.zig`
// `hasErrors()` before the empty-struct stub can reach codegen. Owned by
// `semantic_diagnostics.UnknownTypeChecker` (A2.4); built only when
// diagnostics are active, querying ProgramIndex + TypeResolver.
if (self.diagnostics) |diags| {
const checker = semantic_diagnostics.UnknownTypeChecker{
.alloc = self.alloc,
.diagnostics = diags,
.types = &self.module.types,
.index = &self.program_index,
.main_file = self.main_file,
};
checker.run(decls);
}
// Pass 2: lower main (and comptime side-effects)
self.lowerMainAndComptime(decls);
// Pass 3: lower deferred functions (any_to_string etc.) now that all types are registered
self.lowerDeferredTypeFns();
// Pass 4: target-specific entry-point sanity checks
self.checkRequiredEntryPoints();
// Pass 4a: validate main's signature (ERR E4.2 entry-point gate).
self.validateMainSignature();
// Pass 4b: eagerly lower bodied methods on sx-defined `#objc_class`
// declarations. The Obj-C runtime calls these via IMP pointers
// registered in M1.2 A.4 — no sx-side call path drives lazy
// lowering, so we trigger it here. Mirrors the JNI eager-lower
// pattern in Pass 5.
self.lowerObjcDefinedClassMethods();
// Pass 5: synthesize JNI-mangled exports for `#jni_main` bodied methods.
// Android's JNI runtime resolves `private native sx_<m>(...)` declared in
// the bundled classes.dex by looking up the symbol
// `Java_<pkg-mangled>_<Class>_sx_1<m-mangled>` in the loaded .so. Each
// bodied method on a `#jni_main #jni_class` decl becomes an exported
// C-ABI fn with that name; the JNIEnv* / jobject params are prepended,
// then the user-declared params (with type-erased pointers since JNI
// doesn't carry sx-side types across the binding).
self.synthesizeJniMainStubs();
// CP coverage lock: every generic instance carries both a template and an
// author stamp (body-author ≡ layout-author by construction).
self.assertInstanceMapsCoincide();
}
/// ERR E4.2: the entry-point signature gate. `main` must take no parameters
/// and have a SINGLE-slot return: void (`()` / `-> ()` / `-> void`), an
/// integer (POSIX exit code, truncated to u8), or `-> !` / `-> !Named` (the
/// error tag rides the single return register). The multi-slot
/// `-> (T, !)` tuple return is NOT yet supported — the JIT calls main as
/// `() -> i32`, so a 2-slot `{value, error}` return ABI-mismatches and
/// segfaults; that shape lands with the E4.2 entry-point wrapper. Any other
/// shape (`-> string`, `-> f64`, a non-failable tuple, …) is a clean
/// diagnostic rather than a silent miscompile.
pub fn validateMainSignature(self: *Lowering) void {
const fd = self.program_index.fn_ast_map.get("main") orelse return;
if (fd.params.len != 0) {
if (self.diagnostics) |diags| {
diags.addFmt(.err, fd.params[0].name_span, "main: parameters must be empty; return type must be void, an integer, or `!`", .{});
}
return;
}
const rt = self.resolveReturnType(fd);
// Single-slot returns the JIT's `() -> i32` ABI handles directly:
// void / integer, and a pure failable `-> !` (a bare u32 error tag).
if (rt == .void or self.isIntEx(rt)) return;
if (self.errorChannelOf(rt)) |chan| {
if (rt == chan) {
// pure `-> !` / `-> !Named`. The emitted entry-point wrapper
// (emit_llvm `emitFailableMainRet`) calls `sx_trace_report_unhandled`
// on an escaping error, so the AOT path must auto-link the trace
// runtime even when the body emits no other push/clear.
self.needs_trace_runtime = true;
return;
}
// `-> (T, !)` — value-carrying failable. Accepted only for a single
// **integer** value slot (`{int, error_set}`): the wrapper extracts
// the value + tag from the returned tuple, exits `value as u8` on
// success / reports + exits 1 on error. Multi-value `-> (T1, T2, !)`
// or a non-integer value slot stays rejected — there's no single
// integer exit code to map it to.
const ti = self.module.types.get(rt);
if (ti == .tuple and ti.tuple.fields.len == 2 and self.isIntEx(ti.tuple.fields[0])) {
self.needs_trace_runtime = true;
return;
}
if (self.diagnostics) |diags| {
diags.addFmt(.err, if (fd.return_type) |rtn| rtn.span else null, "a value-carrying failable `main` must be `-> (int, !)` (one integer value slot); got '{s}'. Use `-> !` (no value), `-> (int, !)`, or a non-failable integer return", .{self.formatTypeName(rt)});
}
return;
}
if (self.diagnostics) |diags| {
diags.addFmt(.err, if (fd.return_type) |rtn| rtn.span else null, "main: return type must be void, an integer, or `!`; got '{s}'", .{self.formatTypeName(rt)});
}
}
// ERR E1.7 / E1.8 — path-sensitive error-flow diagnostics (Pass 1e) live in
// `error_flow.zig` (`ErrorFlow`, a `*Lowering` facade). `lowerRoot` calls
// `self.errorFlow().checkErrorFlow(decls)`.
/// On Android, the OS loads the .so via a Java-side Activity declared
/// with `#jni_main #jni_class("...")`. The Java class drives the
/// lifecycle (onCreate / onPause / etc.) and sx provides the native
/// delegates bound via JNI name mangling. Without a `#jni_main` decl
/// there's no entry point — the .so would load but Android has nothing
/// to call into.
pub fn checkRequiredEntryPoints(self: *Lowering) void {
const tc = self.target_config orelse return;
if (!tc.isAndroid()) return;
var it = self.program_index.foreign_class_map.iterator();
while (it.next()) |entry| {
const fcd = entry.value_ptr.*;
if (fcd.is_main and !fcd.is_foreign and fcd.runtime == .jni_class) return;
}
if (self.diagnostics) |diags| {
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" ++
" MyApp :: #jni_main #jni_class(\"co/example/MyApp\") {{\n" ++
" onCreate :: (self: *Self, b: *Bundle) {{ /* ... */ }}\n" ++
" }}", .{});
}
}
/// Inject compile-time constants from target_config into comptime_constants.
/// Called after scanDecls so that enum types (OperatingSystem, Architecture) are registered.
pub fn injectComptimeConstants(self: *Lowering) void {
const tc = self.target_config orelse return;
// OS: OperatingSystem enum { macos; linux; windows; wasm; unknown; }
const os_name_id = self.module.types.internString("OperatingSystem");
if (self.module.types.findByName(os_name_id)) |os_ty| {
const os_info = self.module.types.get(os_ty);
if (os_info == .@"enum") {
const tag: u32 = if (tc.isWasm())
self.findVariantIndex(os_info.@"enum".variants, "wasm")
else if (tc.isWindows())
self.findVariantIndex(os_info.@"enum".variants, "windows")
else if (tc.isAndroid())
self.findVariantIndex(os_info.@"enum".variants, "android")
else if (tc.isLinux())
self.findVariantIndex(os_info.@"enum".variants, "linux")
else if (tc.isIOS())
self.findVariantIndex(os_info.@"enum".variants, "ios")
else if (tc.isMacOS())
self.findVariantIndex(os_info.@"enum".variants, "macos")
else
self.findVariantIndex(os_info.@"enum".variants, "unknown");
self.comptime_constants.put("OS", .{ .enum_tag = .{ .ty = os_ty, .tag = tag } }) catch {};
}
}
// ARCH: Architecture enum { aarch64; x86_64; wasm32; wasm64; unknown; }
const arch_name_id = self.module.types.internString("Architecture");
if (self.module.types.findByName(arch_name_id)) |arch_ty| {
const arch_info = self.module.types.get(arch_ty);
if (arch_info == .@"enum") {
const tag: u32 = if (tc.isWasm32())
self.findVariantIndex(arch_info.@"enum".variants, "wasm32")
else if (tc.isWasm64())
self.findVariantIndex(arch_info.@"enum".variants, "wasm64")
else if (tc.isAarch64())
self.findVariantIndex(arch_info.@"enum".variants, "aarch64")
else if (tc.isX86_64())
self.findVariantIndex(arch_info.@"enum".variants, "x86_64")
else
self.findVariantIndex(arch_info.@"enum".variants, "unknown");
self.comptime_constants.put("ARCH", .{ .enum_tag = .{ .ty = arch_ty, .tag = tag } }) catch {};
}
}
// POINTER_SIZE: i64 (4 for wasm32, 8 for wasm64 and other 64-bit targets)
const ptr_size: i64 = if (tc.isWasm32()) 4 else 8;
self.comptime_constants.put("POINTER_SIZE", .{ .int_val = ptr_size }) catch {};
}
pub fn findVariantIndex(self: *Lowering, variants: []const types.StringId, name: []const u8) u32 {
const name_id = self.module.types.internString(name);
for (variants, 0..) |v, i| {
if (v == name_id) return @intCast(i);
}
return 0; // fallback to first variant
}
/// Lower functions that were deferred because they use type-category matching.
/// At this point, main is fully lowered and all types are in the TypeTable.
pub fn lowerDeferredTypeFns(self: *Lowering) void {
if (self.deferred_type_fns.items.len == 0) return;
self.processing_deferred = true;
for (self.deferred_type_fns.items) |name| {
self.lazyLowerFunction(name);
}
self.processing_deferred = false;
}
/// Lower a list of top-level declarations (used by irComptimeEval — non-lazy path).
/// This preserves the old behavior for comptime evaluation contexts.
pub fn lowerDecls(self: *Lowering, decls: []const *const Node) void {
for (decls) |decl| {
self.setCurrentSourceFile(decl.source_file);
const is_imported = if (self.main_file) |mf|
(if (decl.source_file) |sf| !std.mem.eql(u8, sf, mf) else false)
else
false;
switch (decl.data) {
.fn_decl => |fd| {
self.program_index.fn_ast_map.put(fd.name, &decl.data.fn_decl) catch {};
self.lowerFunction(&fd, fd.name, is_imported);
},
.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.lowerFunction(&cd.value.data.fn_decl, cd.name, is_imported);
} else if (cd.value.data == .struct_decl) {
self.registerStructDecl(&cd.value.data.struct_decl, decl.source_file);
} else if (cd.value.data == .enum_decl) {
self.registerEnumDecl(&cd.value.data.enum_decl);
} else if (cd.value.data == .union_decl) {
self.registerUnionDecl(&cd.value.data.union_decl);
} else if (cd.value.data == .comptime_expr) {
self.lowerComptimeGlobal(cd.name, cd.value.data.comptime_expr.expr, cd.type_annotation);
}
},
.comptime_expr => |ct| {
self.lowerComptimeSideEffect(ct.expr);
},
.struct_decl => {
self.registerStructDecl(&decl.data.struct_decl, decl.source_file);
},
.enum_decl => {
self.registerEnumDecl(&decl.data.enum_decl);
},
.union_decl => {
self.registerUnionDecl(&decl.data.union_decl);
},
.error_set_decl => {
self.registerErrorSetDecl(decl);
},
.protocol_decl => {
self.registerProtocolDecl(&decl.data.protocol_decl);
},
.impl_block => {
self.protocolResolver().registerImplBlock(&decl.data.impl_block, is_imported, decl);
},
.foreign_class_decl => {
self.registerForeignClassDecl(&decl.data.foreign_class_decl);
},
.namespace_decl => |ns| {
self.registerNamespacedForeignClasses(ns);
if (self.main_file != null) {
self.registerNamespaceQualifiedFns(ns.name, ns.own_decls);
self.lowerDecls(ns.decls);
}
},
else => {},
}
}
}
/// Detect whether `Context :: struct {...}` is declared anywhere in the
/// program. Used to gate the implicit `__sx_ctx` param machinery: when
/// `std.sx` is in the dep graph, `Context` is declared and every sx
/// function gets the implicit param. Otherwise the program runs with a
/// bare C ABI (no global Context, no implicit param, no FFI wrappers).
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,
.namespace_decl => |ns| detectContextDecl(ns.decls),
else => false,
};
if (found) return true;
}
return false;
}
/// Returns true if a sx function declaration should receive the
/// implicit `__sx_ctx` parameter. False for foreign-libc bindings,
/// #builtin / #compiler bodies, and C-conv functions (which keep
/// their literal C ABI). Also false for OS-called entry points
/// (`isExportedEntryName`): main and JNI hooks are invoked by the
/// dyld / JVM with no `__sx_ctx` arg, so the visible signature must
/// not include one. Their bodies are still sx code — they
/// synthesise `&__sx_default_context` at entry and use it as their
/// own `current_ctx_ref`. Full FFI-wrapper split (a separate
/// `__sx_<name>_impl` with the ctx param) lands in Step 4 proper.
pub fn funcWantsImplicitCtx(self: *const Lowering, fd: *const ast.FnDecl) bool {
if (!self.implicit_ctx_enabled) return false;
if (fd.call_conv == .c) return false;
// `extern` imports and `export` defines are external C symbols —
// C ABI, no sx context (Phase 2, gap iv).
if (fd.extern_export != .none) return false;
return switch (fd.body.data) {
.foreign_expr, .builtin_expr, .compiler_expr => false,
else => !isExportedEntryName(fd.name),
};
}
/// Returns true if a fn-pointer of the given type carries an implicit
/// `__sx_ctx` at LLVM slot 0. Default-conv sx fn-pointers do; C-conv
/// (and any non-function type) does not.
pub fn fnPtrTypeWantsCtx(self: *const Lowering, ty: TypeId) bool {
if (!self.implicit_ctx_enabled) return false;
if (ty.isBuiltin()) return false;
const ti = self.module.types.get(ty);
if (ti != .function) return false;
return ti.function.call_conv != .c;
}
// ── Unified declaration-fact writers (R5 §#4) ──
// The SOLE writers of the three semantic maps — global
// `type_alias_map` / `module_const_map` / `global_names` AND their
// source-partitioned analogues (`*_by_source`). Invariant: the global and
// by-source write for a name are inseparable — a write-site that mirrors
// one without the other lets a ns-only author miss `*_by_source` and leak
// past the source-aware bare-TYPE gate. No raw `.put`/`.remove` to the
// three maps exists outside these helpers (grep-checkable — mirrors the
// no-raw-`TypeTable.update` discipline). The global map stays the only
// READER for now; the per-source cache feeds the gate. A null source
// (unreachable for a scanned top-level decl post-import-resolution) falls
// back to the main file; if even that is absent only the by-source write is
// skipped — the global map is always written.
pub fn putTypeAlias(self: *Lowering, source: ?[]const u8, name: []const u8, tid: TypeId) void {
self.program_index.type_alias_map.put(name, tid) catch {};
if (source orelse self.main_file) |src| self.program_index.putTypeAliasBySource(src, name, tid);
}
pub fn putModuleConst(self: *Lowering, source: ?[]const u8, name: []const u8, info: program_index_mod.ModuleConstInfo) void {
self.program_index.module_const_map.put(name, info) catch {};
if (source orelse self.main_file) |src| self.program_index.putModuleConstBySource(src, name, info);
}
pub fn putGlobal(self: *Lowering, source: ?[]const u8, name: []const u8, info: program_index_mod.GlobalInfo) void {
self.program_index.global_names.put(name, info) catch {};
if (source orelse self.main_file) |src| self.program_index.putGlobalBySource(src, name, info);
}
pub fn dropModuleConst(self: *Lowering, source: ?[]const u8, name: []const u8) void {
_ = self.program_index.module_const_map.remove(name);
if (source orelse self.main_file) |src| self.program_index.removeModuleConstBySource(src, name);
}
/// Pass 1: Scan declarations — register ASTs and extern stubs, but don't lower bodies.
pub fn scanDecls(self: *Lowering, decls: []const *const Node) void {
// Pass 0: register every numeric-literal module const (`N :: 16` and the
// typed `N : i64 : 16`, plus float-valued `N :: 4.0` / `N : f64 : 4.0`)
// BEFORE any type alias is resolved below. A type alias whose dimension is
// a named const (`Arr :: [N]T`) resolves its dimension eagerly here, on
// the stateless registration path; that path can only read
// `module_const_map`. Untyped consts would otherwise be registered only in
// declaration order (pass 1) and typed ones only after the alias fixpoint
// (pass 2) — so an alias declared before its const, or any alias over a
// typed const, saw an empty table and miscompiled the dimension to length
// 0. A float-valued const resolves to a dimension only when
// its value is integral (`floatToIntExact`); pre-registering it keeps the
// forward-alias float path identical to the int path. The dimension only
// needs the value, so a placeholder type is fine; pass 2 overwrites typed
// consts with the resolved annotation type.
for (decls) |decl| {
if (decl.data != .const_decl) continue;
const cd = decl.data.const_decl;
switch (cd.value.data) {
.int_literal => {
const info = program_index_mod.ModuleConstInfo{ .value = cd.value, .ty = .i64 };
self.putModuleConst(decl.source_file, cd.name, info);
},
.float_literal => {
const info = program_index_mod.ModuleConstInfo{ .value = cd.value, .ty = .f64 };
self.putModuleConst(decl.source_file, cd.name, info);
},
// A const whose RHS is an integer EXPRESSION over other consts
// (`M :: 2; N :: M + 1`) is itself a usable count: register it so
// `moduleConstInt` can fold the RHS through `evalConstIntExpr`
//. Placeholder `.i64` type — the count consumers read
// only the value; if the expression doesn't fold (references a
// non-const), `moduleConstInt` yields null and the use diagnoses.
.binary_op, .unary_op => {
const info = program_index_mod.ModuleConstInfo{ .value = cd.value, .ty = .i64 };
self.putModuleConst(decl.source_file, cd.name, info);
},
else => {},
}
}
// Pass 0b: reserve every GENUINE same-name NAMED-TYPE shadow's DISTINCT
// nominal slot BEFORE the registration loop resolves any fields (E2/F1, and
// enum/union from E6a). A field / variant type referencing a shadow name —
// self (`next: *Box`), or a forward / mutual ref to a shadow declared LATER
// in the same module (`peer: *Node`) — then binds to its OWN nominal TypeId
// via `type_decl_tids`, never the global findByName first-author fallback
//
// "Genuine" = ≥2 DISTINCT decls of the SAME KIND in THIS scan author the name
// (so it needs ≥2 distinct nominal TypeIds). Grouping by (kind, name) keeps a
// `struct Foo` and an `enum Foo` in separate groups — neither is a shadow of
// the other. Gating on the scanned decls — NOT `nameHasMultipleTypeAuthors`
// (the raw import facts, which over-count one file reached via two
// un-normalized import spellings, e.g. `math/matrix44` pulled in twice) —
// keeps a single-real-decl name on the legacy id-0 path, byte-identical. ALL
// authors of a genuine shadow reserve, in declaration order: the FIRST at id
// 0, the rest at fresh nonzero ids, matching the per-decl registration order
// so the first-author-keeps-0 assignment holds.
const ShadowKey = struct { kind: u8, name: types.StringId };
var shadow_first = std.AutoHashMap(ShadowKey, *const anyopaque).init(self.alloc);
defer shadow_first.deinit();
var genuine_shadows = std.AutoHashMap(ShadowKey, void).init(self.alloc);
defer genuine_shadows.deinit();
for (decls) |decl| {
const td = topLevelTypeDecl(decl) orelse continue;
if (td.isGeneric()) continue;
const sk = ShadowKey{ .kind = @intFromEnum(std.meta.activeTag(td)), .name = self.module.types.internString(td.name()) };
const gop = shadow_first.getOrPut(sk) catch continue;
if (gop.found_existing) {
if (gop.value_ptr.* != td.key()) genuine_shadows.put(sk, {}) catch {};
} else gop.value_ptr.* = td.key();
}
for (decls) |decl| {
const td = topLevelTypeDecl(decl) orelse continue;
const sk = ShadowKey{ .kind = @intFromEnum(std.meta.activeTag(td)), .name = self.module.types.internString(td.name()) };
if (!genuine_shadows.contains(sk)) continue;
self.setCurrentSourceFile(decl.source_file);
self.reserveShadowSlot(td);
}
for (decls) |decl| {
self.setCurrentSourceFile(decl.source_file);
const is_imported = if (self.main_file) |mf|
(if (decl.source_file) |sf| !std.mem.eql(u8, sf, mf) else false)
else
false;
switch (decl.data) {
.fn_decl => |fd| {
// 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. 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). Key the identity map (`fn_decl_fids`, inside
// `declareFunction`) by the STABLE AST field pointer — the
// same `&decl.data.fn_decl` stored in `fn_ast_map` and the
// `module_decls` raw facts — not the switch-capture copy `fd`,
// whose address is a per-iteration stack temporary that no
// later decl-identity lookup can reproduce.
self.declareFunction(&decl.data.fn_decl, fd.name);
},
.const_decl => |cd| {
if (cd.value.data == .fn_decl) {
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, decl.source_file);
} else if (cd.value.data == .enum_decl) {
// Per-decl nominal identity for enum/tagged-union types (E6a)
self.registerEnumDecl(&cd.value.data.enum_decl);
} else if (cd.value.data == .union_decl) {
// Per-decl nominal identity for plain union types (E6a)
self.registerUnionDecl(&cd.value.data.union_decl);
} else if (cd.value.data == .type_expr or
cd.value.data == .pointer_type_expr or
cd.value.data == .many_pointer_type_expr or
cd.value.data == .array_type_expr or
cd.value.data == .slice_type_expr or
cd.value.data == .optional_type_expr or
cd.value.data == .function_type_expr)
{
// Type alias: MyFloat :: f64; Ptr :: *u8; Cb :: (i32) -> i32;
const target_ty = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
// The stateless resolver yields `.unresolved` for a shape
// it cannot build — e.g. `Arr :: [<computed>]T`, whose
// dimension is not a compile-time integer constant. Surface
// it as a clean diagnostic so the build aborts here rather
// than letting `.unresolved` reach codegen and `@panic` in
// sizeOf (no fabricated 0-length array). For a
// top-level array alias, re-fold the dimension so an
// oversized / negative constant emits the SAME precise
// message as the direct form (`a : [N]T`) via the shared
// `program_index.reportDimError` — only a genuinely
// non-const dim gets the generic alias message.
if (target_ty == .unresolved) {
if (self.diagnostics) |d| {
const precise: ?program_index_mod.DimU32 = if (cd.value.data == .array_type_expr) blk: {
const dim = type_bridge.foldArrayDim(cd.value.data.array_type_expr.length, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
break :blk switch (dim) {
.too_large, .below_min, .non_integral_float => dim,
else => null,
};
} else null;
if (precise) |dim|
program_index_mod.reportDimError(d, cd.value.data.array_type_expr.length.span, dim)
else
d.addFmt(.err, cd.value.span, "type alias '{s}' could not be resolved: an array dimension is not a compile-time integer constant", .{cd.name});
}
}
self.putTypeAlias(self.current_source_file, cd.name, target_ty);
} 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| {
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 => {},
}
}
}
}
// Handle generic struct instantiation: Vec3 :: Vec(3, f32)
// Parser produces a .call node for these (not parameterized_type_expr)
if (cd.value.data == .call) {
const call_data = &cd.value.data.call;
const callee_name = switch (call_data.callee.data) {
.identifier => |id| id.name,
.field_access => |fa| fa.field,
else => "",
};
// A namespaced callee (`ns.Box(..)`) is an explicit qualified
// reach, exempt from the bare-head visibility gate (E4).
const head_qualified = call_data.callee.data == .field_access;
// A qualified head `ABox :: a.Box(i64)` selects a's OWN
// template via the namespace edge (mirrors the annotation
// head site `resolveTypeCallWithBindings`), not the bare
// last-wins `struct_template_map`.
const qual_alias: ?[]const u8 = if (head_qualified and call_data.callee.data.field_access.object.data == .identifier)
call_data.callee.data.field_access.object.data.identifier.name
else
null;
if (callee_name.len > 0) {
// Generic-struct alias head (`ABox :: Box(i64)` /
// `a.Box(i64)`): route layout selection through the single
// choke-point (CP-1); the Vector / type-fn branches stay
// as the non-generic fall-through.
switch (self.selectGenericStructHead(callee_name, qual_alias, head_qualified, call_data.callee.span)) {
.template => |t| self.registerGenericStructAlias(cd.name, &t, call_data.args),
.poisoned => self.putTypeAlias(self.current_source_file, cd.name, .unresolved),
.not_generic => {
if (std.mem.eql(u8, callee_name, "Vector")) {
// Builtin type constructor — checked BEFORE
// the generic `fn_ast_map` branch because
// `Vector` IS in `fn_ast_map` (declared as a
// `#builtin` fn) but `instantiateTypeFunction`
// can't resolve it (no body). Use
// `resolveTypeCallWithBindings` which
// hard-codes the vector layout.
const result_ty = self.resolveTypeCallWithBindings(call_data);
if (result_ty != .void) {
self.putTypeAlias(self.current_source_file, cd.name, result_ty);
}
} else if (self.program_index.fn_ast_map.get(callee_name)) |fd| {
// Type-returning function: Foo :: Complex(u32)
if (fd.type_params.len > 0) {
if (!head_qualified and self.headFnLeak(callee_name, call_data.callee.span)) {
self.putTypeAlias(self.current_source_file, cd.name, .unresolved);
} else if (self.instantiateTypeFunction(cd.name, callee_name, fd, call_data.args)) |result_ty| {
self.putTypeAlias(self.current_source_file, cd.name, result_ty);
}
}
}
},
}
}
} else if (cd.value.data == .parameterized_type_expr) {
// Type alias for generic struct (from type_bridge path)
const pt = &cd.value.data.parameterized_type_expr;
const base_name = if (std.mem.lastIndexOfScalar(u8, pt.name, '.')) |dot| pt.name[dot + 1 ..] else pt.name;
const pt_qualified = std.mem.indexOfScalar(u8, pt.name, '.') != null;
// A qualified base `ABox :: a.Box(i64)` selects a's OWN
// 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;
// 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.
switch (self.selectGenericStructHead(base_name, pt_alias, pt_qualified, cd.value.span)) {
.template => |t| self.registerGenericStructAlias(cd.name, &t, pt.args),
.poisoned => self.putTypeAlias(self.current_source_file, cd.name, .unresolved),
.not_generic => {
// Builtin parameterised type (Vector(N, T) etc) —
// resolve via type_bridge and register the result
// under the alias name so `Vec4` in expression
// position can `const_type(<vector tid>)`.
const result_ty = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
if (result_ty != .void and result_ty != .unresolved) {
self.putTypeAlias(self.current_source_file, cd.name, result_ty);
}
},
}
}
// comptime_expr handled in Pass 2
// Typed value constants (`AF_INET :i32: 2`) are registered in
// pass 2 below — after the forward-alias fixpoint — so a
// forward identifier alias in the annotation resolves to its
// target instead of a fabricated stub. Untyped
// literal constants carry no annotation to resolve, so they
// stay here (their type comes from the literal / inference).
if (cd.type_annotation == null) {
// Untyped literal constants (e.g. UI_VERT_SRC :: #string GLSL...GLSL;)
const lit_ty: ?TypeId = switch (cd.value.data) {
.string_literal => .string,
.int_literal => .i64,
.float_literal => .f64,
.bool_literal => .bool,
// Complex constant expressions (e.g. COLOR_WHITE :: Color.{ r = 255, ... })
.struct_literal => self.inferExprType(cd.value),
else => null,
};
if (lit_ty) |ty| {
const info = program_index_mod.ModuleConstInfo{ .value = cd.value, .ty = ty };
self.putModuleConst(self.current_source_file, cd.name, info);
}
}
},
.struct_decl => {
self.registerStructDecl(&decl.data.struct_decl, decl.source_file);
},
.enum_decl => {
// Per-decl nominal identity for enum/tagged-union types (E6a)
self.registerEnumDecl(&decl.data.enum_decl);
},
.union_decl => {
// Per-decl nominal identity for plain union types (E6a)
self.registerUnionDecl(&decl.data.union_decl);
},
.error_set_decl => {
self.registerErrorSetDecl(decl);
},
.protocol_decl => {
self.registerProtocolDecl(&decl.data.protocol_decl);
},
.impl_block => {
self.protocolResolver().registerImplBlock(&decl.data.impl_block, is_imported, decl);
},
.foreign_class_decl => {
self.registerForeignClassDecl(&decl.data.foreign_class_decl);
},
.namespace_decl => |ns| {
self.registerNamespacedForeignClasses(ns);
if (self.main_file != null) {
self.scanDecls(ns.decls);
self.registerNamespaceQualifiedFns(ns.name, ns.own_decls);
}
},
.ufcs_alias => |ua| {
self.program_index.ufcs_alias_map.put(ua.name, ua.target) catch {};
},
// Top-level globals are registered in a second pass (below),
// after the forward-alias fixpoint, so a forward identifier
// alias used as a global's type annotation resolves.
.var_decl => {},
else => {},
}
}
self.resolveForwardIdentifierAliases(decls);
// Pass 2: registrations that resolve a top-level type annotation run
// after the alias fixpoint, so a forward identifier alias used as the
// annotation resolves to its target.
for (decls) |decl| {
self.setCurrentSourceFile(decl.source_file);
switch (decl.data) {
.var_decl => self.registerTopLevelGlobal(&decl.data.var_decl),
.const_decl => |cd| if (cd.value.data == .array_literal)
self.registerConstArrayGlobal(&cd)
else {
self.registerTypedModuleConst(&cd);
self.maybeRegisterConstStructGlobal(&cd);
},
else => {},
}
}
// Pass 2b: untyped consts whose RHS is a CONST-AGGREGATE leaf
// (`L :: K.len`, `E :: K[1]`, `R :: LIT.r`) register with the count
// placeholder so the folders reach them. Runs AFTER pass 2 so the
// aggregate (array const / struct const) is registered regardless of
// declaration order; gated on the receiver naming a const aggregate so
// a namespaced member (`F :: m.PI_ISH`) is never mis-typed.
for (decls) |decl| {
if (decl.data != .const_decl) continue;
const cd = decl.data.const_decl;
if (cd.type_annotation != null) continue;
self.setCurrentSourceFile(decl.source_file);
const obj: *const Node = switch (cd.value.data) {
.field_access => |fa| fa.object,
.index_expr => |ie| ie.object,
else => continue,
};
if (obj.data != .identifier) continue;
const recv_is_agg = switch (self.selectModuleConst(obj.data.identifier.name)) {
.resolved => |sel| sel.info.value.data == .array_literal or sel.info.value.data == .struct_literal,
.own_opaque, .ambiguous, .none => false,
};
if (!recv_is_agg) continue;
self.putModuleConst(decl.source_file, cd.name, .{ .value = cd.value, .ty = .i64 });
}
}
/// Register a typed module-level value constant (`AF_INET :i32: 2`). Run in
/// scanDecls pass 2 (after `resolveForwardIdentifierAliases`) so a forward
/// identifier alias in the annotation (`A :: B; B :: i32; K : A : 42;`)
/// resolves to its target rather than a fabricated empty-struct stub, which
/// would otherwise mistype the constant.
pub fn registerTypedModuleConst(self: *Lowering, cd: *const ast.ConstDecl) void {
const ta = cd.type_annotation orelse return;
// Only initializer shapes that pass 0 (binary_op / unary_op → placeholder
// `.i64`) or the literal path register as a USABLE module const need
// reconciling against the annotation. Every other shape (call,
// struct/array literal, bare identifier) is never registered as a
// foldable / emittable const, so it cannot manifest a
// wrong-type fold/emit; a use-site diagnostic covers it.
switch (cd.value.data) {
.int_literal, .float_literal, .bool_literal, .string_literal, .undef_literal, .null_literal, .binary_op, .unary_op, .struct_literal => {},
else => return,
}
const ty = self.resolveType(ta);
// An unresolvable annotation is already diagnosed by the type resolver;
// don't pile a bogus type-mismatch on top, and don't leave the pass-0
// placeholder behind as a usable const.
if (ty == .unresolved) {
self.dropModuleConst(self.current_source_file, cd.name);
return;
}
// Validate the initializer against the explicit annotation BY TYPE, so a
// const-EXPRESSION initializer (`N : string : M + 2`) is checked exactly
// like a literal rather than skipped. A mismatch is a type error, not a
// silently-accepted const — registering it would let `emitModuleConst`
// stamp the value with the wrong IR type (an int emitted as a `string`
// const → a bogus pointer that segfaults at the use site) and let the
// count path fold it (`[N]i64` → 4). Issue 0088.
if (!self.typedConstInitFits(cd.value, ty)) {
// A non-integral compile-time float into an integer const is the
// same implicit-narrowing failure as a typed local/field/param —
// report it with the unified wording (integral floats now FOLD here,
// so the old generic "initializer is a float literal/expression"
// message is stale). Every other mismatch keeps the generic wording.
if (self.isIntEx(ty) and isFloat(self.inferExprType(cd.value))) {
if (program_index_mod.evalConstFloatExpr(cd.value, self)) |fv| {
self.diagNonIntegralNarrow(cd.value.span, fv, ty);
self.dropModuleConst(self.current_source_file, cd.name);
return;
}
}
if (self.diagnostics) |d| {
d.addFmt(.err, cd.value.span, "type mismatch: constant '{s}' is declared '{s}' but its initializer is {s}", .{
cd.name, self.formatTypeName(ty), self.initializerDescription(cd.value),
});
}
// Evict the pass-0 placeholder (`N : string : 4` and
// `N : string : M + 2` are both pre-registered as `.i64` in scanDecls
// pass 0); leaving it would let a count use still fold `N`.
self.dropModuleConst(self.current_source_file, cd.name);
return;
}
// Reconcile the registration with the resolved annotation (pass 0 stored
// a literal/expression placeholder type), so the const folds and emits at
// its declared type — the same `put` the literal path always did.
const info = program_index_mod.ModuleConstInfo{ .value = cd.value, .ty = ty };
self.putModuleConst(self.current_source_file, cd.name, info);
}
/// True iff a literal initializer of `value`'s kind is faithfully
/// representable at the declared `dst_ty` — the precondition
/// `emitModuleConst` relies on when it materialises the constant. The arms
/// match `emitModuleConst`'s arms exactly, using the same type-kind
/// predicates (`isIntEx` / `isFloat` / the `module.types.get` tag) the rest
/// of lowering uses.
///
/// Deliberately NOT routed through `coercionResolver().classify`
/// (conversions.zig): that planner judges RUNTIME value coercions and is
/// unsound as a compile-time literal-representability oracle here — a `null`
/// literal's natural type is `.void`, so `classify(.void, *T)` yields `.none`
/// and would reject the valid `P : *void : null`; `bool` is 1 bit wide, so
/// `classify(.bool, i64)` yields `.widen` and would accept the bogus
/// `B : i64 : true`.
pub fn typedConstInitFits(self: *Lowering, value: *const Node, dst_ty: TypeId) bool {
// An INTEGER-annotated constant accepts a compile-time INTEGRAL float —
// a literal (`K : i64 : 4.0`), an int-leaf expression (`K : i64 : M + 2.0`
// → 4), or a float-const-leaf expression whose SUM is integral
// (`F : f64 : 2.5; K : i64 : F + 1.5` → 4). Integrality is judged on the
// FLOAT fold (`evalConstFloatExpr` + `floatToIntExact`) — the SAME facility
// the typed-local path (`foldComptimeFloatInit`) uses — not the int-only
// folder, which folds leaf-by-leaf in `i64` and so misses an integral SUM
// built from a non-integral float leaf. A non-integral fold (`1.5`,
// `M + 0.5`, `F + 0.25`) yields null here and falls through to the
// rejecting checks below, where `registerTypedModuleConst` emits the
// unified narrowing diagnostic.
if (self.isIntEx(dst_ty)) {
switch (value.data) {
.float_literal, .binary_op, .unary_op => {
if (program_index_mod.evalConstFloatExpr(value, self)) |fv| {
if (program_index_mod.floatToIntExact(fv) != null) return true;
}
},
else => {},
}
}
return switch (value.data) {
// `---` zero-inits at any type.
.undef_literal => true,
// Integer literal → any integer (incl. custom widths) or float
// (`WIDTH : f32 : 800`).
.int_literal => self.isIntEx(dst_ty) or isFloat(dst_ty),
// Float literal → a float type only (the float arm emits `constFloat`).
.float_literal => isFloat(dst_ty),
.bool_literal => dst_ty == .bool,
.string_literal => dst_ty == .string,
// `null` → a pointer or optional.
.null_literal => !dst_ty.isBuiltin() and switch (self.module.types.get(dst_ty)) {
.pointer, .many_pointer, .optional => true,
else => false,
},
// Const-EXPRESSION initializer (binary_op / unary_op — the only
// non-literal kinds the caller admits): validate by the initializer's
// INFERRED type so coverage is type-based, not a per-node-kind
// allowlist where an unenumerated kind silently escapes. The integer/float fit mirrors the literal arms above.
else => self.constExprInitFits(self.inferExprType(value), dst_ty),
};
}
/// True iff a const-expression initializer of inferred type `init_ty` is
/// faithfully representable at the declared `dst_ty`. Type-based so it covers
/// every const-expression shape (binary_op, unary_op, …) through one check
/// rather than per-node-kind arms. The integer/float arms mirror the
/// int/float literal arms of `typedConstInitFits` (an integer expression fits
/// an integer or float annotation; a float expression fits a float).
pub fn constExprInitFits(self: *Lowering, init_ty: TypeId, dst_ty: TypeId) bool {
// An initializer whose type we couldn't infer is left for the use-site /
// emission diagnostic rather than rejected here (no over-rejection).
if (init_ty == .unresolved) return true;
if (self.isIntEx(init_ty)) return self.isIntEx(dst_ty) or isFloat(dst_ty);
if (isFloat(init_ty)) return isFloat(dst_ty);
if (init_ty == .bool) return dst_ty == .bool;
if (init_ty == .string) return dst_ty == .string;
// Any other concrete initializer type must match the annotation exactly.
return init_ty == dst_ty;
}
/// Register an array-typed `::` constant (`K : [4]i64 : .[...]`, or the
/// untyped `A :: .[1, 2, 3]`) as an IMMUTABLE module global: one storage,
/// reads GEP it, the emitter marks it LLVMSetGlobalConstant, dead-global
/// elimination drops it when unused. Source-aware reads come for free via
/// `selectGlobalAuthor` (the per-source partition is written here).
pub fn registerConstArrayGlobal(self: *Lowering, cd: *const ast.ConstDecl) void {
const al = &cd.value.data.array_literal;
const arr_ty: TypeId = blk: {
if (cd.type_annotation) |ta| {
const t = self.resolveType(ta);
if (t == .unresolved) return; // annotation already diagnosed
if (t.isBuiltin() or self.module.types.get(t) != .array) {
if (self.diagnostics) |d|
d.addFmt(.err, cd.value.span, "constant '{s}' has an array-literal initializer but its annotation is not an array type", .{cd.name});
return;
}
const dim = self.module.types.get(t).array.length;
if (dim != al.elements.len) {
if (self.diagnostics) |d|
d.addFmt(.err, cd.value.span, "constant '{s}' declares [{d}] elements but its initializer has {d}", .{ cd.name, dim, al.elements.len });
return;
}
break :blk t;
}
break :blk self.inferConstArrayType(cd.name, al.elements, cd.value.span) orelse return;
};
const init_val = self.constArrayLiteral(al.elements, arr_ty) orelse {
if (self.diagnostics) |d|
d.addFmt(.err, cd.value.span, "constant '{s}' must be initialized by compile-time constant elements", .{cd.name});
return;
};
const name_id = self.module.types.internString(cd.name);
const gid = self.module.addGlobal(.{
.name = name_id,
.ty = arr_ty,
.init_val = init_val,
.is_const = true,
});
self.putGlobal(self.current_source_file, cd.name, .{ .id = gid, .ty = arr_ty });
// ALSO register as a module const so the comptime folders see the
// elements (`K.len` / `K[<const idx>]` in dims and const exprs).
// Bare value reads still hit the GLOBAL arm first (identifier arm
// order), so this never double-emits.
self.putModuleConst(self.current_source_file, cd.name, .{ .value = cd.value, .ty = arr_ty });
}
/// Infer `[N]T` for an untyped array-literal constant. Element types unify:
/// all ints → i64; ANY float promotes the element type to f64 (ints convert
/// exactly — the int+float promotion rule for consts, element-wise); bool /
/// string homogeneous only. A non-numeric mix or a non-inferable element
/// shape (nested aggregate, enum literal, named const) asks for an
/// annotation rather than guessing.
pub fn inferConstArrayType(self: *Lowering, name: []const u8, elements: []const *const Node, span: ast.Span) ?TypeId {
if (elements.len == 0) {
if (self.diagnostics) |d|
d.addFmt(.err, span, "constant '{s}' is an empty array literal — annotate the type (e.g. `{s} : [0]i64 : .[]`)", .{ name, name });
return null;
}
var elem_ty: ?TypeId = null;
for (elements) |e| {
const leaf: ?TypeId = switch (e.data) {
.int_literal => .i64,
.float_literal => .f64,
.bool_literal => .bool,
.string_literal => .string,
.unary_op => |uo| if (uo.op == .negate) switch (uo.operand.data) {
.int_literal => .i64,
.float_literal => .f64,
else => null,
} else null,
else => null,
};
const lt = leaf orelse {
if (self.diagnostics) |d|
d.addFmt(.err, e.span, "cannot infer the element type of constant '{s}' from this element — annotate the array type", .{name});
return null;
};
if (elem_ty) |prev| {
if (prev == lt) continue;
// Numeric mix promotes to the float element type.
const numeric_pair = (prev == .i64 and lt == .f64) or (prev == .f64 and lt == .i64);
if (numeric_pair) {
elem_ty = .f64;
continue;
}
if (self.diagnostics) |d|
d.addFmt(.err, e.span, "constant '{s}' mixes incompatible element types — annotate the array type", .{name});
return null;
}
elem_ty = lt;
}
return self.module.types.arrayOf(elem_ty.?, @intCast(elements.len));
}
/// Migrate a struct-literal constant to an IMMUTABLE GLOBAL when every
/// field serializes (literals, enum tags, nested aggregates, and — via the
/// fold helpers — const expressions over named consts / const-aggregate
/// leaves). One storage, reads GEP it, `@W` becomes addressable. A const
/// with a NON-serializable field (a call, a runtime read) keeps the inline
/// re-lowering path — per-use evaluation is that class's documented
/// contract. The module-const registration stays either way (folds +
/// fallback share it); bare reads hit the GLOBAL arm first when migrated.
pub fn maybeRegisterConstStructGlobal(self: *Lowering, cd: *const ast.ConstDecl) void {
if (cd.value.data != .struct_literal) return;
const src = self.current_source_file orelse self.main_file orelse return;
const ci = self.sourceModuleConst(src, cd.name) orelse return;
if (ci.ty.isBuiltin() or self.module.types.get(ci.ty) != .@"struct") return;
const init_val = self.constStructLiteral(&cd.value.data.struct_literal, ci.ty) orelse return;
const name_id = self.module.types.internString(cd.name);
const gid = self.module.addGlobal(.{
.name = name_id,
.ty = ci.ty,
.init_val = init_val,
.is_const = true,
});
self.putGlobal(self.current_source_file, cd.name, .{ .id = gid, .ty = ci.ty });
}
/// Register a top-level mutable global (e.g., `context : Context = ---;`).
/// Run AFTER `resolveForwardIdentifierAliases` so a forward identifier alias
/// in the type annotation (`A :: B; B :: i32; g : A = 7;`) resolves to its
/// target instead of a fabricated empty-struct stub, which would otherwise
/// give the global a type that mismatches its initializer at LLVM
/// verification. Globals can't be named in a type position, so
/// deferring them past type/alias registration introduces no ordering hazard.
pub fn registerTopLevelGlobal(self: *Lowering, vd: *const ast.VarDecl) void {
// Use self.resolveType so type aliases like `Handle :: u32;` resolve
// to their target type (not a synthetic empty struct). When the
// user omitted the annotation, infer from the initializer
// expression; foreign globals with no annotation are diagnosed
// because their type can't be inferred without an initializer.
const var_ty: TypeId = if (vd.type_annotation) |ta|
self.resolveType(ta)
else if (vd.value) |val|
self.inferExprType(val)
else blk: {
if (self.diagnostics) |d|
d.addFmt(.err, null, "top-level var '{s}' has no type annotation and no initializer to infer from", .{vd.name});
break :blk .void;
};
// Foreign / extern globals reference a symbol defined in libSystem etc.
// (`_NSConcreteStackBlock : *void #foreign;` or `… : *void extern;`). The C
// symbol name is the optional override (`extern_name`) or the sx name itself.
const sym_name = vd.extern_name orelse vd.name;
const name_id = self.module.types.internString(sym_name);
const init_val = self.globalInitValue(vd, var_ty);
const gid = self.module.addGlobal(.{
.name = name_id,
.ty = var_ty,
.init_val = init_val,
.is_const = false,
.is_extern = vd.is_extern,
});
self.putGlobal(self.current_source_file, vd.name, .{ .id = gid, .ty = var_ty });
}
/// Serialize a top-level global's initializer into a static `ConstantValue`.
/// Extern globals (external symbol) and value-less declarations carry no
/// payload — they default to zero/extern at link, which is correct. An
/// identifier initializer that names a module constant is materialized from
/// the recorded constant (`K : A : 42; g : A = K;` → 42); a
/// global initialized from an identifier that resolves to no usable constant
/// is rejected with a diagnostic rather than silently zero-initialized — a
/// global has no run site for a dynamic initializer.
pub fn globalInitValue(self: *Lowering, vd: *const ast.VarDecl, var_ty: TypeId) ?inst_mod.ConstantValue {
if (vd.is_extern) return null;
const v = vd.value orelse return null;
return switch (v.data) {
.undef_literal => .zeroinit,
.null_literal => .null_val,
.int_literal => |il| blk: {
self.checkIntLiteralFits(il.value, var_ty, v.span);
break :blk .{ .int = il.value };
},
// A negated literal (`g : i64 = -1;`) folds through the shared
// const-expr serializer. The folded value follows the same rules as
// the direct literal arms: int fits-check; a float at an integer
// global narrows only when integral.
.unary_op => blk: {
if (self.constExprValue(v, var_ty)) |cv| {
switch (cv) {
.int => |iv| self.checkIntLiteralFits(iv, var_ty, v.span),
.float => |fv| if (self.isIntEx(var_ty)) {
if (program_index_mod.floatToIntExact(fv)) |iv| break :blk inst_mod.ConstantValue{ .int = iv };
self.diagNonIntegralNarrow(v.span, fv, var_ty);
break :blk null;
},
else => {},
}
break :blk cv;
}
break :blk self.diagnoseNonConstGlobal(vd, v);
},
.bool_literal => |bl| .{ .boolean = bl.value },
// A float initializer at an integer-typed global follows the
// implicit narrowing rule (integral folds, non-integral errors).
.float_literal => |fl| blk: {
if (self.isIntEx(var_ty)) {
if (program_index_mod.floatToIntExact(fl.value)) |iv| break :blk inst_mod.ConstantValue{ .int = iv };
self.diagNonIntegralNarrow(v.span, fl.value, var_ty);
break :blk null;
}
break :blk inst_mod.ConstantValue{ .float = fl.value };
},
.string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) },
.array_literal => |al| self.constArrayLiteral(al.elements, var_ty) orelse self.diagnoseNonConstGlobal(vd, v),
.struct_literal => |sl| self.constStructLiteral(&sl, var_ty) orelse self.diagnoseNonConstGlobal(vd, v),
.identifier => |id| blk: {
// A global initialized from a module constant copies the
// constant's recorded value (typed module consts land in
// `module_const_map` via `registerTypedModuleConst`, run in the
// same pass-2 before this). F1/F2: copy the SOURCE-AWARE author's
// value (own-wins), folding its RHS in the author's context, and
// reject a ≥2-flat ambiguity loudly.
if (self.program_index.module_const_map.get(id.name)) |ci_global| {
const sel: SelectedConst = switch (self.selectModuleConst(id.name)) {
.resolved => |s| s,
.none => .{ .info = ci_global, .source = null },
.own_opaque => {
if (self.diagnostics) |d|
d.addFmt(.err, v.span, "global '{s}' must be initialized by a compile-time constant; '{s}' is not a usable constant here", .{ vd.name, id.name });
break :blk null;
},
.ambiguous => {
if (self.diagnostics) |d|
d.addFmt(.err, v.span, "'{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{id.name});
break :blk null;
},
};
const author_pin = self.pinConstAuthorSource(sel.source);
defer author_pin.unpin();
if (self.constExprValue(sel.info.value, var_ty)) |cv| break :blk cv;
}
if (self.diagnostics) |d|
d.addFmt(.err, v.span, "global '{s}' must be initialized by a compile-time constant; '{s}' is not a usable constant here", .{ vd.name, id.name });
break :blk null;
},
// An enum-literal global (`chosen : Color = .green;`) serializes to
// the variant's tag value against the destination enum type (issue
// 0082). The compiler-injected `OS`/`ARCH` globals flow through here
// too; their runtime reads resolve via `comptime_constants`, so the
// serialized tag only affects the static initializer.
.enum_literal => |el| self.constEnumLiteral(&el, var_ty, v.span),
// Any other initializer shape (`.field_access` on a const, a call, an
// arithmetic expression, …) is not a static constant the compiler can
// evaluate here. Diagnose loudly rather than emit a null payload that
// silently zero-initializes the global.
else => blk: {
if (self.diagnostics) |d|
d.addFmt(.err, v.span, "global '{s}' must be initialized by a compile-time constant", .{vd.name});
break :blk null;
},
};
}
/// A global aggregate initializer (array/struct literal) that does not fully
/// reduce to a compile-time constant is rejected loudly. Without this the
/// `null` payload would fall through to a zero-initialized global, silently
/// dropping the declared fields.
pub fn diagnoseNonConstGlobal(self: *Lowering, vd: *const ast.VarDecl, v: *const Node) ?inst_mod.ConstantValue {
if (self.diagnostics) |d|
d.addFmt(.err, v.span, "global '{s}' must be initialized by a compile-time constant", .{vd.name});
return null;
}
/// Resolve identifier-RHS type aliases whose target is declared LATER in the
/// file. The forward scan above only registers an alias (`A :: B`) when `B`
/// is already resolved as a type author; a forward target isn't yet present,
/// so `A` is left unregistered and its uses get falsely flagged as an unknown
/// type. Re-resolve to a fixpoint now that every top-level name
/// has been seen, so `A :: B; B :: i32;` converges the same as the ordered
/// `B :: i32; A :: B;`. A value const is never an `.identifier` node
/// (`NotAType :: 123` is an int literal), and an alias whose target is a value
/// const stays unresolved, so neither this pass nor the unknown-type suppression can register a
/// non-type name.
///
/// SOURCE-AWARE (R5 §4, E1.5). The target `B` is resolved AS SEEN FROM `A`'s
/// OWN source via the source-aware nominal leaf (`selectNominalLeaf` over
/// `type_aliases_by_source` / `moduleTypeAuthor` — E1), NEVER the global
/// `type_alias_map` / global `findByName`. The "already resolved" guard is
/// likewise per-source. When a same-name `B` is authored by a *different*
/// source (e.g. a namespaced import polluting the global alias map last-wins),
/// a global fixpoint would bind `A` to the wrong `B` and re-open 0105 one
/// layer down once E2 registers shadows; resolving against `A`'s source binds
/// the local `B`. The `.pending` outcome (B is itself a not-yet-resolved
/// forward alias) routes BACK into this fixpoint — `A` is skipped this round
/// and converges on a later iteration. `.undeclared` (no type author) and
/// `.not_visible` (a namespaced-only type, not bare-aliasable) leave `A`
/// unwritten; its uses surface the stub / diagnostic, never a silent global
/// leak. The write stays on the unified `putTypeAlias` helper (E1 no-drift
/// invariant — only the helper touches the maps).
pub fn resolveForwardIdentifierAliases(self: *Lowering, decls: []const *const Node) void {
var progressed = true;
while (progressed) {
progressed = false;
for (decls) |decl| {
const cd = switch (decl.data) {
.const_decl => |c| c,
else => continue,
};
if (cd.value.data != .identifier) continue;
const src = decl.source_file orelse self.main_file orelse continue;
if (self.aliasResolvedInSource(src, cd.name)) continue;
const rhs = cd.value.data.identifier;
// Pass the backtick raw flag so a forward alias whose RHS is a raw
// identifier (`` RawAlias :: `i2 ``, target declared later) resolves
// to the nominal `` `i2 `` author, not the builtin `i2` spelling.
switch (self.selectNominalLeaf(rhs.name, src, rhs.is_raw)) {
.resolved => |tid| {
self.putTypeAlias(decl.source_file, cd.name, tid);
progressed = true;
},
// B not yet a resolved type author from this source: a forward
// alias still pending (re-tried next round), a forward / not-
// yet-registered named author, an undeclared name, a
// namespaced-only type that is not bare-aliasable, or an
// ambiguous same-name shadow (≥2 flat authors). Leave A
// unwritten — no global last-wins leak; the ambiguity surfaces
// where A is used.
.pending, .forward, .undeclared, .not_visible, .ambiguous => {},
}
}
}
}
/// TRUE iff `name` is already recorded as a type alias FROM `src` — the
/// per-source analogue of `type_alias_map.contains`, so the forward-alias
/// fixpoint resolves a same-name alias in each source independently (E1.5).
pub fn aliasResolvedInSource(self: *Lowering, src: []const u8, name: []const u8) bool {
if (self.program_index.type_aliases_by_source.get(src)) |inner| return inner.contains(name);
return false;
}
/// Pass 2: Lower main function body and comptime side-effects.
pub fn lowerMainAndComptime(self: *Lowering, decls: []const *const Node) void {
for (decls) |decl| {
// A `#run` body lowers in its OWN module's source context: `NAME :: #run f()` written in an imported module must
// resolve a bare `f` from that module's flat imports, not the main
// file's. Without this, `selectPlainCallableAuthor` runs with the main
// file's perspective and reports a genuine per-source author as
// ambiguous. Mirrors `scanDecls` / `lowerDecls`, which already set
// the source file per decl.
self.setCurrentSourceFile(decl.source_file);
switch (decl.data) {
.const_decl => |cd| {
if (cd.value.data == .fn_decl) {
// `export` defines are roots: their purpose is external
// consumption (often never called from sx), so force-lower
// them like OS-called entry points — else lazy lowering
// leaves them as bodiless `declare` stubs (Phase 2).
if (isExportedEntryName(cd.name) or cd.value.data.fn_decl.extern_export == .export_) {
self.lazyLowerFunction(cd.name);
}
} else if (cd.value.data == .comptime_expr) {
self.lowerComptimeGlobal(cd.name, cd.value.data.comptime_expr.expr, cd.type_annotation);
}
},
.fn_decl => |fd| {
if (isExportedEntryName(fd.name) or fd.extern_export == .export_) {
self.lazyLowerFunction(fd.name);
}
},
.comptime_expr => |ct| {
self.lowerComptimeSideEffect(ct.expr);
},
.namespace_decl => |ns| {
if (self.main_file != null) {
self.lowerMainAndComptime(ns.decls);
}
},
else => {},
}
}
}
/// Lower every SHADOWED same-name function author into its OWN FuncId with a
/// real (non-extern) body — the identity-addressable lowering PATH this step
/// adds. It does NOT run during a default compile: the name path
/// stays the sole resolver, so the suite is byte-for-byte unchanged. The bare-call
/// disambiguation invokes it as part of routing bare flat calls to the right author; until
/// then it is exercised by the lower-test regression that asserts two distinct
/// non-extern bodies for a same-name collision.
///
/// The first-wins flat/directory merge keeps exactly one author per name in
/// the merged decl list; `scanDecls` declares that WINNER (lowered on demand
/// through the name-keyed `lazyLowerFunction`). The merge retains every
/// dropped same-name author in the `module_decls` raw facts (path → name →
/// `RawDeclRef`) without touching resolution; this walks that index, filters
/// each author to its `*FnDecl` (`fnDeclOfRaw`), and gives each shadowed
/// author its own slot: `declareFunction` (identity-mapped to a fresh
/// same-name FuncId) + `lowerFunctionBodyInto` (its body, in its own module's
/// visibility context). Two same-name authors then carry distinct FuncIds and
/// distinct bodies, while `resolveFuncByName` still returns the first (winner)
/// author so existing calls bind first-wins.
///
/// Scoped to DIRECT flat imports of the main file: a `module_decls` entry
/// whose path is the main file or one of its bare `#import` edges. A
/// namespaced (`ns :: #import`) author has no bare-name winner and is excluded
/// both by that flat-edge gate and by the `fn_ast_map` winner lookup below.
pub fn lowerRetainedSameNameAuthors(self: *Lowering) void {
const module_decls = self.program_index.module_decls orelse return;
const main_file = self.main_file orelse return;
const flat_graph = self.program_index.flat_import_graph orelse return;
const main_flat_edges = flat_graph.get(main_file);
var path_it = module_decls.iterator();
while (path_it.next()) |path_entry| {
const path = path_entry.key_ptr.*;
const is_eligible = std.mem.eql(u8, path, main_file) or
(main_flat_edges != null and main_flat_edges.?.contains(path));
if (!is_eligible) continue;
var fn_it = path_entry.value_ptr.names.iterator();
while (fn_it.next()) |fn_entry| {
const name = fn_entry.key_ptr.*;
const fd = fnDeclOfRaw(fn_entry.value_ptr.*) orelse continue;
// A name with no bare winner is namespaced-only (`ns.fn`) — it
// never participated in the flat merge, so it has no shadow to
// lower. The author already owning the name-keyed slot (the
// first-wins winner) lowers through the normal lazy path.
const winner = self.program_index.fn_ast_map.get(name) orelse continue;
if (winner == fd) continue;
// Only plain free functions get an out-of-line slot; generic /
// foreign / builtin / #compiler authors keep their existing
// dispatch (mirrors lazyLowerFunction / declareFunction guards).
if (!isPlainFreeFn(fd)) continue;
_ = self.bareAuthorFuncId(fd, name, path);
}
}
}
/// Result of bare-call disambiguation (now over the Phase B
/// author collector).
pub const BareCallee = union(enum) {
/// Bind the call to this specific author, carried as the shared
/// `SelectedFunc` (R5 §#3): its `*FnDecl` + authoring source, FuncId
/// materialized on demand. Every callee-signature decision in the call
/// path (variadic packing, param typing, default expansion) reads the
/// RESOLVED author from this one object — never a first-wins re-lookup
/// by name.
func: SelectedFunc,
/// ≥2 distinct flat authors are reachable from the caller and none is
/// the caller's own — the bare call can't pick one; require a qualifier.
ambiguous,
/// 0 or 1 reachable author, or the resolved author IS the existing
/// bare-name winner — defer to the existing path, byte-for-byte.
none,
};
/// The single bare-call author object (R5 §#3): the `*FnDecl` that defines
/// the call and the SOURCE file that authors it, kept together so the call
/// path has ONE source of truth for the callee. `materialized` holds the
/// author's FuncId once a site needs it; it is filled on demand by
/// `selectedFuncId` (→ `bareAuthorFuncId`), NOT during selection — so a
/// selection that only needs the decl (default-arg expansion), or a shadow
/// taken purely as a value, never lowers the first-wins winner (0102d).
pub const SelectedFunc = struct {
decl: *const ast.FnDecl,
source: []const u8,
materialized: ?FuncId = null,
};
/// Outcome of the source-aware bare TYPE leaf (`selectNominalLeaf`, R5 §E).
/// The type-position analogue of `BareCallee`: the nominal author is selected
/// over the ONE graph-walk collector and resolved against the source-keyed
/// caches, never the global `findByName` first-match / global alias map.
pub const TypeHeadResolution = union(enum) {
/// A builtin primitive, a registered named type, or a resolved alias.
resolved: TypeId,
/// A const author is visible but its alias target is not resolved yet —
/// a forward identifier alias. Routes back into the existing
/// `resolveForwardIdentifierAliases` fixpoint (source-aware in E1.5).
/// `resolveNominalLeaf` keeps the empty-struct stub (the alias resolves on
/// a later fixpoint round).
pending,
/// A flat-visible author DOES declare `name` as a type, but its TypeId
/// slot is not registered yet — a forward / self / mutual reference
/// resolved mid-registration (`next: *ArenaChunk`), or a foreign /
/// lazily-registered author with no `findByName` slot. `resolveNominalLeaf`
/// keeps the empty-struct stub, which `internNamedTypeDecl` ADOPTS (key-
/// stable `updatePreservingKey`) when the type registers — so the forward
/// reference binds to the eventually-filled type. NOT an error: the author
/// exists, it is simply not interned yet.
forward,
/// NO author anywhere declares `name` as a type, an alias, or a const —
/// a genuinely-undeclared name (a typo, or a value parameter used as a
/// type). `resolveNominalLeaf` poisons it with the `.unresolved` sentinel
/// + an "unknown type" diagnostic, never a silently-fabricated 0-field
/// struct (which would mis-size every downstream load / store). In the
/// MAIN file the `UnknownTypeChecker` is the diagnostic authority (it owns
/// scope context + value-param hints, and a valid unbound generic leaf
/// like `-> T` on a template legitimately lands here), so the leaf keeps
/// the legacy stub there and defers the diagnostic to the checker.
undeclared,
/// `name` IS a registered named type, but it is reachable from the
/// querying module ONLY through a namespaced import (or over more than one
/// flat hop) — not bare-visible over the single-hop direct flat-import set
/// (the type analog of Phase B's bare-call tightening, F1). The user must
/// qualify it (`ns.Type`) or `#import` the declaring module directly.
/// `resolveNominalLeaf` surfaces the "not visible" diagnostic and returns
/// the `.unresolved` poison sentinel — NEVER the global `findByName` match
/// (which would leak the type) and NEVER a silent empty-struct stub (which
/// would mis-size it).
not_visible,
/// ≥2 DISTINCT same-name type authors are flat-visible from the querying
/// source and none is its own (E2). The selection is genuinely
/// ambiguous: `resolveNominalLeaf` emits a loud diagnostic and returns the
/// `.unresolved` poison sentinel — never a silent first-/last-wins pick.
ambiguous,
};
/// THE plain bare-name call selector. `resolveBareCallee`'s
/// body verbatim, now over the Phase B author collector
/// (`resolver.collectVisibleAuthors` — the ONE graph-walk) instead of a direct
/// `module_decls` + `flat_import_graph` traversal. Routes a bare identifier
/// call `name` from `caller_file` to the right same-name author when flat
/// imports introduce a genuine collision. Every single-author / local /
/// parameter / std / qualified name resolves through the EXISTING path
/// unchanged: the selector returns `.none` whenever the outcome would match
/// first-wins, so nothing on the common path is perturbed.
///
/// The collector returns RAW authors across ALL decl domains; this selector
/// reproduces a fn-only author view by filtering each author through
/// `fnDeclOfRaw` (a `const`-wrapped fn unwraps to its inner fn; every other
/// domain drops out), preserving resolveBareCallee's negative space
/// byte-for-byte.
///
/// - **own-author wins**: if `caller_file` authors `name` as a fn and the
/// bare-name first-wins winner is a DIFFERENT author, select the caller's
/// own author. (When the winner already IS the caller's own — the
/// single-author and first-importer cases — `.none` lets the existing path
/// bind it.)
/// - else select among the authors reachable via `caller_file`'s FLAT import
/// edges (bare `#import` of a file or directory, never a namespaced
/// `ns :: #import`), deduped by author identity (a diamond import of the
/// same module is one author): `≥2 distinct` → `.ambiguous`; exactly one
/// that DIFFERS from the winner → select it; otherwise `.none`.
///
/// Generic / comptime / foreign / builtin authors are never rerouted — the
/// existing dispatch owns those shapes; `isPlainFreeFn` filters them out
/// BEFORE the count gate (so a same-name collision of non-plain authors is
/// NOT ambiguous), and the selector returns `.none`. No eager
/// materialization: the returned `SelectedFunc` carries decl + source and
/// `materialized = null`; a consumer fills the FuncId via `selectedFuncId`
/// only when it truly needs it (0102d).
pub fn selectPlainCallableAuthor(self: *Lowering, name: []const u8, caller_file: []const u8) BareCallee {
const winner = self.program_index.fn_ast_map.get(name);
var res = self.resolver();
const set = res.collectVisibleAuthors(name, caller_file, .user_bare_flat);
defer if (set.flat.len > 0) self.alloc.free(set.flat);
// own-author wins. The collector's `own` spans all domains; a non-fn
// (or a const not bound to a function) means `caller_file` has no fn
// `name` — fall through to the flat authors, exactly as a fn-only walk
// would.
if (set.own) |own_author| {
if (fnDeclOfRaw(own_author.raw)) |own| {
if (winner != null and winner.? == own) return .none;
if (!isPlainFreeFn(own)) return .none;
return .{ .func = .{ .decl = own, .source = own_author.source } };
}
}
// Caller does not author `name` as a fn → its flat-reachable authors.
// Filter to plain free functions BEFORE counting: a same-name collision
// of non-plain authors (e.g. two flat-imported modules each `#foreign`ing
// the same symbol) is NOT counted as ambiguous — it falls through to
// `.none` and the existing first-wins path.
var the_one: ?*const ast.FnDecl = null;
var the_source: []const u8 = &.{};
var count: usize = 0;
for (set.flat) |fa| {
const fd = fnDeclOfRaw(fa.raw) orelse continue;
if (!isPlainFreeFn(fd)) continue;
count += 1;
if (count >= 2) return .ambiguous;
the_one = fd;
the_source = fa.source;
}
if (count == 0) return .none;
if (winner != null and winner.? == the_one.?) return .none;
return .{ .func = .{ .decl = the_one.?, .source = the_source } };
}
/// THE source-aware bare TYPE leaf (R5 §E, E1). The type-position analogue
/// of `selectPlainCallableAuthor`: resolve a bare type name `name` referenced
/// from `from` by selecting its nominal author over the ONE graph-walk
/// collector (`resolver.collectVisibleAuthors`) and reading the alias from the
/// source-keyed cache (`type_aliases_by_source`, E0's write side) keyed by the
/// selected author's OWN source — never the global `findByName` first-match
/// nor the global `type_alias_map`.
///
/// `raw` is the backtick raw-identifier escape: a raw reference
/// bypasses the builtin classifier and resolves only through the nominal
/// author / alias path.
///
/// E1 is single-author: `collectVisibleAuthors` returns ≤1 author, so the
/// selection is unambiguous and resolution is byte-identical to the legacy
/// leaf. Same-name shadows (≥2 authors) and the `.ambiguous` outcome (0105)
/// land in E2; the per-author `nominal_id` TypeId that makes a shadow
/// representable also lands then (today a registered named type resolves to
/// its unique `findByName` match, which IS the single author's TypeId).
/// Generic / parameterized-protocol / Vector / type-function heads never
/// reach this leaf — `resolveTypeWithBindings` owns those above the leaf
/// switch, so they stay legacy.
pub fn selectNominalLeaf(self: *Lowering, name: []const u8, from: []const u8, raw: bool) TypeHeadResolution {
const table = &self.module.types;
// Builtin primitive keyword / arbitrary-width int — unless a raw escape
// routes the literal name straight to nominal resolution.
if (!raw) {
if (TypeResolver.resolveBuiltinName(name, table)) |id| return .{ .resolved = id };
}
// Structural string-forms that reach the leaf as a literal type-expr
// name (`[:0]u8` → string, `[*]T`, `*T`, `?T`) carry NO nominal author —
// they are wrappers, not declarations, so source-keying does not apply.
// Resolve them through the stateless namer exactly as the legacy leaf
// did; only the bare nominal name below cuts over to the collector.
if (name.len > 0 and (name[0] == '[' or name[0] == '*' or name[0] == '?')) {
return .{ .resolved = self.typeResolver().resolveName(name, raw) };
}
// Bare nominal name. A bare TYPE name is visible iff a flat-import-
// reachable module authors it AS A TYPE — and a TYPE author is EITHER a
// named type (struct/enum/union/error-set/protocol/foreign class) OR a
// type ALIAS (`Name :: <type>`, a `const_decl` whose value resolved to a
// type, recorded in E0's `type_aliases_by_source`). Both kinds are gated
// identically: `moduleTypeAuthor` is the SINGLE source of truth, so a
// namespaced-only alias leaks no more than a namespaced-only named type,
// and a flat-visible alias is never poisoned by an invisible same-name
// named type (and vice-versa) — R4. A same-name flat VALUE/FUNCTION is
// NOT a type author (R1); a value-const (`N :: 7`) lives in
// `module_consts_by_source`, never in `type_aliases_by_source`, so it is
// correctly excluded too.
//
// The TYPE reachability here is SINGLE-HOP — `from`'s own author plus its
// DIRECT flat-import edges (`flatTypeAuthorCount`), the same non-transitive
// set the bare VALUE / FUNCTION / CONST leaves use (E4, consistent with
// 0706). A library template's INTERNAL type refs (`List.append`'s
// `alloc: Allocator`) still resolve because every instantiation kind
// (generic struct / fn / pack fn / param protocol / type fn) is
// source-pinned to the template's defining module, so the query
// originates THERE — where the type is a direct flat import — not at the
// cross-module call site.
const name_id = table.internString(name);
const registered = table.findByName(name_id);
// Compiler-synthesized default-Context emission resolves the built-in
// allocator types as infrastructure — fall open (the gate is for USER bare
// references, not compiler internals).
if (self.emitting_default_context) {
if (registered) |existing| return .{ .resolved = existing };
}
// Import facts unwired (registration / comptime host with no module_decls
// or flat graph): there is no querying context to gate against — preserve
// the legacy resolution (registered → existing; else forward-alias /
// undeclared).
if (self.program_index.module_decls == null or self.program_index.flat_import_graph == null) {
if (registered) |existing| return .{ .resolved = existing };
// Direct per-source lookup for resolved alias, then pending check.
if (self.program_index.type_aliases_by_source.get(from)) |inner| {
if (inner.get(name)) |tid| return .{ .resolved = tid };
}
if (self.program_index.module_decls) |decls| {
if (decls.get(from)) |m| if (m.names.get(name)) |ref| if (ref == .const_decl) return .pending;
}
return .undeclared;
}
// Single graph-walk over flat imports: one `collectVisibleAuthors` call
// replaces `moduleTypeAuthor` + `ownConstDeclIsPendingAlias` +
// `flatTypeAuthorCount` + `forwardAliasOrUndeclared`.
var res_walk = self.resolver();
const author_set = res_walk.collectVisibleAuthors(name, from, .user_bare_flat);
defer if (author_set.flat.len > 0) self.alloc.free(author_set.flat);
// 1a. Own type author wins outright (own-wins).
if (author_set.own) |own| switch (own.raw) {
.const_decl => {
// Type alias: present in type_aliases_by_source → resolved.
if (self.program_index.type_aliases_by_source.get(own.source)) |inner| {
if (inner.get(name)) |tid| return .{ .resolved = tid };
}
// Own const_decl not yet resolved: pending (own takes priority
// over any flat author — prevents flat-preemption).
return .pending;
},
else => if (isNamedTypeKind(own.raw)) {
if (self.namedRefTid(own.raw, name)) |tid| return .{ .resolved = tid };
return .forward; // named type exists but slot not yet interned
},
// fn_decl / namespace_decl: not a type author, fall to flat walk
};
// 1b. Flat type authors (named types and resolved aliases only; pending
// flat aliases handled below).
var found_tid: ?TypeId = null;
var flat_type_count: usize = 0;
var flat_has_unregistered = false;
for (author_set.flat) |fa| {
const is_type = switch (fa.raw) {
.const_decl => blk: {
if (self.program_index.type_aliases_by_source.get(fa.source)) |inner|
break :blk inner.contains(name);
break :blk false;
},
else => isNamedTypeKind(fa.raw),
};
if (!is_type) continue;
flat_type_count += 1;
const fa_tid: ?TypeId = switch (fa.raw) {
.const_decl => blk: {
if (self.program_index.type_aliases_by_source.get(fa.source)) |inner|
break :blk inner.get(name);
break :blk null;
},
else => self.namedRefTid(fa.raw, name),
};
if (fa_tid) |t| {
if (found_tid) |f| {
if (t != f) return .ambiguous;
} else found_tid = t;
} else {
flat_has_unregistered = true;
}
}
if (flat_type_count > 0) {
if (found_tid) |t| return .{ .resolved = t };
return .forward; // flat author exists but TypeId not yet registered
}
// 1c. Pending flat aliases (const_decl in a flat-imported module but not
// yet resolved in type_aliases_by_source — the forward-alias fixpoint
// will settle these).
for (author_set.flat) |fa| {
if (fa.raw == .const_decl) {
if (self.program_index.type_aliases_by_source.get(fa.source)) |inner| {
if (inner.get(name)) |tid| return .{ .resolved = tid };
}
return .pending;
}
}
// 2. A block-local type (declared inside a fn / init body) clobbers the
// global entry for its name, so `existing` IS that local type. A local is
// visible ONLY in its OWN source. Resolve it ungated when the query
// originates in the local's source: a legitimately-scoped local must
// not be rejected just because a namespaced-only import also authors a
// top-level type of the same name. When the same name is a block-local of a
// DIFFERENT source — e.g. an imported template's field naming a type the
// CALLER declared block-local — the local is NOT visible here.
if (self.localTypeInSource(from, name)) {
if (registered) |existing| return .{ .resolved = existing };
} else if (self.localTypeInAnySource(name)) {
return .undeclared; // local in another source; no pending alias possible here
}
// 3. Authored as a TYPE (named OR alias) in some module, but NOT flat-
// import-reachable from `from` → reachable only over a namespace edge.
if (self.nameAuthoredAsTypeAnywhere(name)) return .not_visible;
// 4. Not a cross-module type author. A registered generic type-param bound
// or fabricated empty-struct stub resolves ungated.
if (registered) |existing| return .{ .resolved = existing };
return .undeclared;
}
/// TRUE iff `raw` declares a NAMED TYPE — struct / enum / union / error-set /
/// protocol / foreign class. A `fn_decl`, a value-or-alias `const_decl`, and a
/// `namespace_decl` are NOT named types. A type ALIAS is a `const_decl`;
/// it is recognised via `type_aliases_by_source` separately from named types.
pub fn isNamedTypeKind(raw: resolver_mod.RawDeclRef) bool {
return switch (raw) {
.struct_decl, .enum_decl, .union_decl, .error_set_decl, .protocol_decl, .foreign_class_decl => true,
.fn_decl, .const_decl, .var_decl, .namespace_decl => false,
};
}
/// The per-decl nominal TypeId of a NAMED-type `RawDeclRef` author, or null
/// when its slot is not registered yet (a forward / self reference resolved
/// mid-registration → the caller yields the legacy empty-struct stub). A
/// STRUCT resolves first through its `type_decl_tids` nominal identity (E2)
/// keyed by the raw-facts decl pointer, so two same-name struct authors in
/// different sources resolve to their OWN distinct TypeIds. A
/// `type_decl_tids` MISS falls back to the global `findByName` — correct for a
/// SINGLE-author struct registered via a non-`internNamedTypeDecl` path (a
/// `struct #compiler`, a protocol-backed struct, a generic instance) or before
/// it registers; a genuine same-name SHADOW always registers through
/// `internNamedTypeDecl` and so is in `type_decl_tids`, never reaching the
/// fallback. ENUM and UNION resolve the same per-decl way (E6a): registered
/// through `internNamedTypeDecl` (`registerEnumDecl` / `registerUnionDecl`),
/// keyed by the raw-facts decl pointer, with the `findByName` fallback for a
/// single author registered before its slot lands. error-set / protocol /
/// foreign-class keep the legacy `findByName` resolution (their same-name
/// shadows are later E6 sub-steps — E6b/E6c/E6d).
pub fn namedRefTid(self: *Lowering, ref: resolver_mod.RawDeclRef, name: []const u8) ?TypeId {
const table = &self.module.types;
return switch (ref) {
.struct_decl => |d| (table.type_decl_tids.get(@ptrCast(d)) orelse table.findByName(table.internString(name))),
.enum_decl => |d| (table.type_decl_tids.get(@ptrCast(d)) orelse table.findByName(table.internString(name))),
.union_decl => |d| (table.type_decl_tids.get(@ptrCast(d)) orelse table.findByName(table.internString(name))),
.error_set_decl, .protocol_decl, .foreign_class_decl => table.findByName(table.internString(name)),
.fn_decl, .const_decl, .var_decl, .namespace_decl => null,
};
}
/// TRUE iff `name` is authored as a TYPE — a NAMED type OR a type ALIAS — in
/// ANY module's raw facts. The leak detector: a name that is a type author
/// somewhere but not flat-visible from the querying module is reachable only
/// over a namespace edge. Both kinds are checked (R4): named types via
/// `module_decls`, aliases via E0's `type_aliases_by_source`. Distinguishes a
/// real cross-module TYPE author from a LOCAL type / generic-param /
/// fabricated empty-struct stub (findByName-registered but authored in no
/// module) and from a same-name VALUE/FUNCTION author (not a type). Unwired
/// facts → false (nothing to gate; resolve ungated).
pub fn nameAuthoredAsTypeAnywhere(self: *Lowering, name: []const u8) bool {
if (self.program_index.module_decls) |decls| {
var it = decls.valueIterator();
while (it.next()) |m| {
if (m.names.get(name)) |ref| if (isNamedTypeKind(ref)) return true;
}
}
var ait = self.program_index.type_aliases_by_source.valueIterator();
while (ait.next()) |inner| {
if (inner.contains(name)) return true;
}
return false;
}
/// Record a name declared as a BLOCK-LOCAL type so the bare-TYPE gate never
/// mistakes it for a namespaced-only leak (see `local_type_names`). Keyed by the
/// declaring source (the function being lowered) so the local is visible only
/// within that source.
pub fn recordLocalTypeName(self: *Lowering, name: []const u8) void {
const src = self.current_source_file orelse self.main_file orelse return;
const gop = self.local_type_names.getOrPut(src) catch return;
if (!gop.found_existing) gop.value_ptr.* = std.StringHashMap(void).init(std.heap.page_allocator);
gop.value_ptr.put(name, {}) catch {};
}
/// TRUE iff `name` is a block-local type declared in `source`.
pub fn localTypeInSource(self: *Lowering, source: []const u8, name: []const u8) bool {
if (self.local_type_names.get(source)) |inner| return inner.contains(name);
return false;
}
/// TRUE iff `name` is a block-local type declared in ANY source. A name that is a
/// local SOMEWHERE but not in the querying source is a cross-source local — not
/// visible from the querying source.
pub fn localTypeInAnySource(self: *Lowering, name: []const u8) bool {
var it = self.local_type_names.valueIterator();
while (it.next()) |inner| if (inner.contains(name)) return true;
return false;
}
/// Resolve the bare TYPE leaf to a `TypeId` for `resolveTypeWithBindings`.
/// Routes through the source-aware `selectNominalLeaf`. `.pending` (forward
/// alias) and `.forward` (a real author not interned yet — self / forward /
/// foreign reference) keep the empty-struct stub, which the type ADOPTS on
/// registration (`internNamedTypeDecl`). `.undeclared` (NO author anywhere)
/// is genuinely-undeclared: in a NON-main module — which the
/// `UnknownTypeChecker` trusts and never walks — the leaf is the only guard,
/// so it emits "unknown type" and poisons with `.unresolved` (never a silent
/// 0-field struct). In the MAIN file the checker owns the diagnostic (and a
/// valid unbound generic leaf legitimately reaches here), so the leaf keeps
/// the legacy stub. `.not_visible` / `.ambiguous` surface their own loud
/// diagnostic + `.unresolved`. When the source context is unwired
/// (`current_source_file` null — comptime / registration callers), there is no
/// querying module to collect from, so fall open to the legacy namer.
pub fn resolveNominalLeaf(self: *Lowering, name: []const u8, raw: bool, span: ?ast.Span) TypeId {
const from = self.current_source_file orelse
return self.typeResolver().resolveName(name, raw);
return switch (self.selectNominalLeaf(name, from, raw)) {
.resolved => |t| t,
// A forward alias (`.pending`) or a forward / not-yet-interned named
// author (`.forward`) — keep the empty-struct stub the type adopts
// when it registers. A raw or non-raw bare name both land the same
// stub here.
.pending, .forward => self.module.types.intern(.{ .@"struct" = .{
.name = self.module.types.internString(name),
.fields = &.{},
} }),
// Genuinely undeclared: no type / alias / const author anywhere.
.undeclared => {
// The MAIN file is the `UnknownTypeChecker`'s domain — it emits
// the canonical "unknown type" (with scope context + value-param
// hints) and `hasErrors` halts before the stub reaches codegen,
// and a valid unbound generic leaf (`-> T` on a template) also
// lands here — so keep the legacy stub and do NOT double-report.
// A NON-main (imported / library) module is checker-trusted, so
// this leaf is the sole guard: emit + poison with `.unresolved`.
const is_main = if (self.main_file) |mf| std.mem.eql(u8, from, mf) else true;
if (is_main) return self.module.types.intern(.{ .@"struct" = .{
.name = self.module.types.internString(name),
.fields = &.{},
} });
if (self.diagnostics) |d|
d.addFmt(.err, span, "unknown type '{s}'", .{name});
return .unresolved;
},
// Registered, but reachable only through a namespaced import: emit the
// diagnostic at the reference and poison the result so no downstream
// check (field access, size) trusts a leaked / mis-sized type.
// `.unresolved` is poison-suppressed, so there is no secondary
// "field not found" cascade.
.not_visible => {
if (self.diagnostics) |d|
d.addFmt(.err, span, "type '{s}' is not visible; #import the module that declares it", .{name});
return .unresolved;
},
// ≥2 distinct same-name type authors flat-visible, none own (issue
// 0105 case 4): a genuine collision the source can't disambiguate.
// Emit a loud diagnostic and poison — never a silent first-/last-wins.
.ambiguous => {
if (self.diagnostics) |d|
d.addFmt(.err, span, "type '{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{name});
return .unresolved;
},
};
}
/// The `*FnDecl` a raw author wraps, or null when the author is not a
/// function — unwraps a `RawDeclRef` so the collector's all-domain authors
/// yield a fn-only view (a `const`-wrapped fn unwraps to its inner fn; every
/// other domain → null). The single place function authors are read out of
/// the `module_decls` raw facts.
pub fn fnDeclOfRaw(ref: resolver_mod.RawDeclRef) ?*const ast.FnDecl {
return switch (ref) {
.fn_decl => |fd| fd,
.const_decl => |cd| if (cd.value.data == .fn_decl) &cd.value.data.fn_decl else null,
else => null,
};
}
/// The `*StructDecl` a raw author wraps, or null when the author is not a
/// struct — a top-level `Box :: struct(...)` is recorded either as a bare
/// `struct_decl` RawDeclRef or a `const_decl` whose value is one, so both
/// unwrap to the same decl (mirrors `qualifiedStructTemplate`'s own-decl walk).
pub fn structDeclOfRaw(ref: resolver_mod.RawDeclRef) ?*const ast.StructDecl {
return switch (ref) {
.struct_decl => |sd| sd,
.const_decl => |cd| if (cd.value.data == .struct_decl) &cd.value.data.struct_decl else null,
else => null,
};
}
/// The single bare-VISIBLE generic-struct author selected by
/// `bareVisibleStructDecl`: the `StructDecl` plus the source that DECLARED it.
pub const VisibleStructAuthor = struct {
sd: *const ast.StructDecl,
source: []const u8,
};
/// The `fn_decl` of struct `sd`'s method named `method`, or null when `sd`
/// declares no such method. Used to source-pin a static-method head's body to
/// the bare-visible author's own method (`b.Box.make`), bypassing the name-keyed
/// last-wins `fn_ast_map` ("Box.make") that a 2-flat-hop same-name template's
/// method would otherwise win (E4 #1, static-method site).
pub fn structMethodFn(sd: *const ast.StructDecl, method: []const u8) ?*const ast.FnDecl {
for (sd.methods) |mn| {
if (mn.data == .fn_decl and std.mem.eql(u8, mn.data.fn_decl.name, method))
return &mn.data.fn_decl;
}
return null;
}
/// TRUE iff `ref` is a TYPE-FUNCTION head author — a `fn_decl` (or const-
/// wrapped fn) declaring at least one `$`-parameter, i.e. instantiable as a
/// bare type head (`Make(i64)` where `Make :: ($T) -> Type`). Mirrors the
/// `fd.type_params.len > 0` gate every instantiation site uses to recognize a
/// type-fn head, so an ORDINARY same-name function (`Make :: () -> i32`, zero
/// type params) is NOT a type-fn author and does NOT vouch for a hidden 2-flat-
/// hop type-fn head (E4 attempt-8: a `fn_decl != null` author view let any
/// visible function — type-fn or not — authorize a type head).
pub fn typeFnAuthor(ref: resolver_mod.RawDeclRef) bool {
const fd = fnDeclOfRaw(ref) orelse return false;
return fd.type_params.len > 0;
}
/// Materialize (lower-on-demand) the FuncId for a selected bare-call author,
/// caching into `sf.materialized`. Shadow-only: the winner owns the
/// name-keyed slot and lowers through the lazy path, so
/// `selectPlainCallableAuthor` returns `.none` for it and this is never asked
/// to lower the winner (0102d). `name` is the call name (== the author's
/// registered name); `sf.source` pins the author's own visibility context.
pub fn selectedFuncId(self: *Lowering, sf: *SelectedFunc, name: []const u8) FuncId {
if (sf.materialized) |fid| return fid;
const fid = self.bareAuthorFuncId(sf.decl, name, sf.source);
sf.materialized = fid;
return fid;
}
/// The FuncId for a resolved bare-call author, ensuring its body is lowered.
/// Only ever called for a SHADOW (an author that is not the name-keyed
/// winner): the winner owns the name-keyed slot and lowers through the
/// normal lazy path, so `selectPlainCallableAuthor` returns `.none` for it. A shadow
/// is declared a fresh same-name FuncId in its OWN module's visibility
/// context and its body lowered into that slot via the identity-
/// addressable `lowerFunctionBodyInto`. Idempotent: `lowered_fids` tracks
/// which slots already carry a body.
pub fn bareAuthorFuncId(self: *Lowering, fd: *const ast.FnDecl, name: []const u8, path: []const u8) FuncId {
if (self.fn_decl_fids.get(fd)) |fid| {
if (!self.lowered_fids.contains(fid)) {
self.lowered_fids.put(fid, {}) catch {};
self.lowerFunctionBodyInto(fd, fid, name);
}
return fid;
}
const saved_src = self.current_source_file;
self.setCurrentSourceFile(path);
self.declareFunction(fd, name);
self.setCurrentSourceFile(saved_src);
const fid = self.fn_decl_fids.get(fd).?;
self.lowered_fids.put(fid, {}) catch {};
self.lowerFunctionBodyInto(fd, fid, name);
return fid;
}
/// Walk a return-type expression for a `$T` generic leaf, returning the
/// first generic name found. The parser builds `fd.type_params` from
/// PARAMS only (`collectTypeParams`), so a `$`-generic that appears ONLY
/// in the return type makes the fn look non-generic while its return can
/// never be bound — `declareFunction` rejects that shape loudly.
fn returnGenericLeaf(node: *const Node) ?[]const u8 {
return switch (node.data) {
.type_expr => |te| if (te.is_generic) te.name else null,
.pointer_type_expr => |pte| returnGenericLeaf(pte.pointee_type),
.many_pointer_type_expr => |mpte| returnGenericLeaf(mpte.element_type),
.slice_type_expr => |ste| returnGenericLeaf(ste.element_type),
.array_type_expr => |ate| returnGenericLeaf(ate.element_type),
.optional_type_expr => |ote| returnGenericLeaf(ote.inner_type),
.parameterized_type_expr => |pte| {
for (pte.args) |arg| if (returnGenericLeaf(arg)) |n| return n;
return null;
},
.tuple_type_expr => |tte| {
for (tte.field_types) |ft| if (returnGenericLeaf(ft)) |n| return n;
return null;
},
.closure_type_expr => |cte| {
for (cte.param_types) |pt| if (returnGenericLeaf(pt)) |n| return n;
if (cte.return_type) |rt| return returnGenericLeaf(rt);
return null;
},
.function_type_expr => |fte| {
for (fte.param_types) |pt| if (returnGenericLeaf(pt)) |n| return n;
if (fte.return_type) |rt| return returnGenericLeaf(rt);
return null;
},
else => null,
};
}
/// Declare a function as an extern stub (signature only, no body).
/// The same C SYMBOL declared more than once (two modules binding the same
/// libc function, or a rename colliding with an existing binding): an EQUAL
/// signature shares the first registration; a CONFLICTING one is diagnosed —
/// silently letting the first registration win mis-types every call through
/// the later declaration (a `-> string` view of a symbol registered `-> *u8`
/// reads the wrong shape; issue 0128). True = handled (shared or diagnosed),
/// caller must not declare again.
pub fn dedupeExternSymbol(self: *Lowering, fd: *const ast.FnDecl, sym_name: StringId, params: []const Function.Param, ret_ty: TypeId) bool {
for (self.module.functions.items, 0..) |*func, i| {
if (func.name != sym_name or !func.is_extern) continue;
var same = func.ret == ret_ty and func.params.len == params.len;
if (same) {
for (func.params, params) |a, b| {
if (a.ty != b.ty) {
same = false;
break;
}
}
}
if (same) {
self.fn_decl_fids.put(fd, FuncId.fromIndex(@intCast(i))) catch {};
return true;
}
if (self.diagnostics) |d| {
d.addFmt(.err, fd.body.span, "extern symbol '{s}' is already bound with a different signature; two views of one C symbol must declare identical types", .{self.module.types.getString(sym_name)});
}
return true;
}
return false;
}
pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8) void {
// Skip generic templates — they're monomorphized on demand, not declared as extern
if (fd.type_params.len > 0) return;
const ret_ty = self.resolveReturnType(fd);
// A `$T`-generic return with NO parameter mentioning `$T`: the fn isn't
// a template (the guard above runs on param-derived `type_params`) yet
// its return can never be bound by any call site. Declaring it would
// carry the `.unresolved` sentinel into LLVM emission and panic the
// tripwire — diagnose at the declaration instead. Named unknown types
// (`-> Bogus`) are covered by the semantic pass's "unknown type".
if (ret_ty == .unresolved) {
if (fd.return_type) |rtn| {
if (returnGenericLeaf(rtn)) |gen_name| {
if (self.diagnostics) |d| {
d.addFmt(.err, rtn.span, "generic return type '${s}' cannot be bound — '{s}' has no parameter mentioning '${s}', so no call site can infer it", .{ gen_name, name, gen_name });
}
return;
}
}
}
// Foreign declarations with a trailing variadic param map to the C
// calling convention's `...` tail. Drop the variadic param from the
// IR signature (it has no C-level slot) and set is_variadic.
const is_foreign = fd.body.data == .foreign_expr;
// Bare `extern` import: an external C symbol declared via the new linkage
// surface (empty-block placeholder body, no `foreign_expr`). It shares
// `#foreign`'s C-ABI promotion + declareExtern routing below; the optional
// `extern LIB "csym"` lib/rename axis (extern_lib/extern_name) is consumed
// in Phase 1.2. (`export` defines take the beginFunction path, not here.)
const is_extern_decl = fd.extern_export == .extern_;
var is_variadic = false;
var effective_params = fd.params;
// The C-variadic `...` tail applies to BOTH lib-less C-import spellings:
// the legacy `#foreign` (foreign_expr body) and the new `extern` keyword.
// A migrated variadic `extern` must drop the trailing slice param and set
// the flag exactly as its `#foreign` twin did (mirrored at the call site
// by `packVariadicCallArgs`).
if ((is_foreign or is_extern_decl) and fd.params.len > 0 and fd.params[fd.params.len - 1].is_variadic) {
is_variadic = true;
effective_params = fd.params[0 .. fd.params.len - 1];
}
const wants_ctx = self.funcWantsImplicitCtx(fd);
var params = std.ArrayList(Function.Param).empty;
if (wants_ctx) {
params.append(self.alloc, .{
.name = self.module.types.internString("__sx_ctx"),
.ty = self.module.types.ptrTo(.void),
}) catch unreachable;
}
for (effective_params) |p| {
const pty = self.resolveParamType(&p);
params.append(self.alloc, .{
.name = self.module.types.internString(p.name),
.ty = pty,
}) catch unreachable;
}
// `#foreign` declarations are external C symbols by definition —
// promote them to callconv(.c) when the user didn't write it
// explicitly. This keeps fn-ptr coercion type-safe: anything
// typed by name as `(args) -> ret` of a `#foreign` decl can be
// assigned to / passed as a `callconv(.c)` fn-pointer without a
// call-convention mismatch.
const cc: Function.CallingConvention = if (fd.call_conv == .c or is_foreign or is_extern_decl or fd.extern_export == .export_) .c else .default;
// Symbol-name override: `#foreign … "csym"` (foreign_expr.c_name) or the new
// `extern … "csym"` / `export … "csym"` (fd.extern_name). Declare under the C
// name and map the sx name → C name so call sites resolve to the real symbol.
// For `export` the stub is later promoted to a real definition (the body
// lowers into this C-named function via lazyLowerFunction — Phase 2.2).
const is_export_decl = fd.extern_export == .export_;
const rename_c_name: ?[]const u8 = if (is_foreign)
fd.body.data.foreign_expr.c_name
else if (is_extern_decl or is_export_decl)
fd.extern_name
else
null;
if (rename_c_name) |c_name| {
const c_name_id = self.module.types.internString(c_name);
if (self.dedupeExternSymbol(fd, c_name_id, params.items, ret_ty)) {
self.extern_name_map.put(name, c_name) catch {};
return;
}
const fid = self.builder.declareExtern(c_name_id, params.items, ret_ty);
const func = self.module.getFunctionMut(fid);
func.call_conv = cc;
func.source_file = self.current_source_file;
func.is_variadic = is_variadic;
func.has_implicit_ctx = wants_ctx;
self.extern_name_map.put(name, c_name) catch {};
self.fn_decl_fids.put(fd, fid) catch {};
return;
}
const name_id = self.module.types.internString(name);
if ((is_foreign or is_extern_decl) and self.dedupeExternSymbol(fd, name_id, params.items, ret_ty)) return;
const fid = self.builder.declareExtern(name_id, params.items, ret_ty);
const func = self.module.getFunctionMut(fid);
func.call_conv = cc;
func.source_file = self.current_source_file;
func.is_variadic = is_variadic;
func.has_implicit_ctx = wants_ctx;
self.fn_decl_fids.put(fd, fid) catch {};
}
/// 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.
/// 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.
pub 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 => {},
}
}
}
pub 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; 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.
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.
}
/// The unified non-transitive `#import` visibility predicate, parameterized
/// by `VisibilityMode`. `isNameVisible` / `isCImportVisible` are thin
/// adapters over it.
///
/// This is the lowering-side GATE: it walks `module_scopes` (the per-file
/// name set) joined over the edge set the mode selects. It is distinct from
/// `resolver.collectVisibleAuthors`, which collects raw AUTHORS over
/// `module_decls` — the single graph-walk that lives in `resolver.zig`. The
/// two read different facts (name set vs author refs) for different jobs, so
/// the gate's own iterator stays here, not in the resolver.
///
/// `module_scopes[F]` holds ONLY the names authored in F (plus its namespace
/// aliases); cross-module visibility is joined here at query time. Doing the
/// join at lookup (instead of pre-merging in `resolveImports`) lets cyclic
/// imports like std.sx ↔ allocators.sx still resolve, since the cycle's
/// skipped edge is still recorded in the graph and the partner's scope is
/// filled in by the time lowering queries it.
pub fn isVisible(self: *Lowering, name: []const u8, vis: resolver_mod.VisibilityMode) bool {
switch (vis) {
// Registration / lazy lowering paths don't police user visibility.
.lowering_internal => return true,
// Transitive visibility is ProtocolResolver.findVisibleImpls' job;
// this predicate is single-hop only.
.impl_transitive => @panic("isVisible: transitive visibility is owned by findVisibleImpls"),
.c_import_bare => {
// Foreign-C gate: only a lib-less C-import fn_decl is policed; a
// library-bound decl (resolves via the named library, not a
// module edge) or a non-C body is unconditionally visible. The
// legacy `#foreign` form (a `foreign_expr` body) and the new
// `extern` keyword (`extern_export == .extern_`, empty-block body)
// are two spellings of the same lib-less C-symbol import, so BOTH
// route to `visibleOverEdges` here — a migrated `extern` decl must
// get the identical "C function not visible" diagnostic its
// `#foreign` twin did, not the generic top-level-name wording
// (FFI-linkage Part B; example 1228).
const fd = self.program_index.fn_ast_map.get(name) orelse return true;
switch (fd.body.data) {
.foreign_expr => |fe| {
if (fe.library_ref != null) return true;
return self.visibleOverEdges(name);
},
else => {
if (fd.extern_export != .extern_) return true;
if (fd.extern_lib != null) return true;
return self.visibleOverEdges(name);
},
}
},
.user_bare_flat => return self.visibleOverEdges(name),
}
}
/// Run the per-file visibility walk over the flat-import edge set. Falls
/// open (visible) when the scoping infrastructure isn't wired (comptime
/// callers, directory imports without main_file, etc.). The caller is
/// responsible for restricting the check to names that ARE known top-level
/// decls; otherwise every local variable would be policed.
pub fn visibleOverEdges(self: *Lowering, name: []const u8) bool {
const source = self.current_source_file orelse return true;
return nameVisibleOverEdges(self.program_index.module_scopes, self.program_index.flat_import_graph, source, name);
}
/// 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. Byte-identical adapter over `isVisible`.
pub fn isCImportVisible(self: *Lowering, fn_name: []const u8) bool {
return self.isVisible(fn_name, .c_import_bare);
}
/// Non-transitive `#import` visibility check for top-level decls.
/// Byte-identical adapter over `isVisible`.
pub fn isNameVisible(self: *Lowering, name: []const u8) bool {
return self.isVisible(name, .user_bare_flat);
}
/// Lazily lower a function body on demand. Called when lowerCall can't find
/// the function and it exists in fn_ast_map.
pub fn lazyLowerFunction(self: *Lowering, name: []const u8) void {
// Already lowered?
if (self.lowered_functions.contains(name)) return;
// For sx-defined `#objc_class` methods, pin current_foreign_class
// so `*Self` substitutions in resolveTypeWithBindings find the
// state-struct type (M1.2 A.2b). The inline body-lowering path
// below re-resolves param types, so the context must be set
// BEFORE any resolveReturnType / resolveParamType call.
const saved_fc_lazy = self.current_foreign_class;
defer self.current_foreign_class = saved_fc_lazy;
if (self.lookupObjcDefinedClassForMethod(name)) |fcd| {
self.current_foreign_class = fcd;
}
// No AST? (builtins, foreign functions, or imported functions not in this file)
const fd = self.program_index.fn_ast_map.get(name) orelse return;
// Foreign declarations stay as extern stubs but need to be REGISTERED
// in the current module so callers get a real FuncId. Without this,
// a comptime-lowered function (e.g. `concat` from std.sx pulled into
// a fresh ct_module via `evalComptimeString`) emits `.call` against a
// FuncId that doesn't exist locally; the interp can't find the
// foreign target and silently no-ops instead of dispatching to libc.
if (fd.body.data == .foreign_expr or fd.extern_export == .extern_) {
if (self.resolveFuncByName(name) == null) {
self.declareFunction(fd, name);
self.lowered_functions.put(name, {}) catch {};
}
return;
}
// Builtins / #compiler bodies stay as compiler-handled — no extern stub needed.
if (fd.body.data == .builtin_expr or fd.body.data == .compiler_expr) return;
if (fd.type_params.len > 0) return; // generics handled by monomorphization (Step 3.13)
// Defer functions with type-category matches until all types are registered.
// any_to_string uses `if type == { case slice: ... }` which compiles a switch
// with type tags from resolveTypeCategoryTags. This must happen AFTER main is
// fully lowered so all types ([]i32, List__i32, etc.) are in the TypeTable.
if (!self.processing_deferred and std.mem.eql(u8, name, "any_to_string")) {
self.deferred_type_fns.append(self.alloc, name) catch {};
return;
}
// Mark as lowered before lowering (prevents infinite recursion)
self.lowered_functions.put(name, {}) catch {};
// Find the existing extern stub (from scanDecls), keyed by NAME — the
// FIRST author of a name owns this slot. A shadowed same-name author is
// not here (it has no name-keyed slot); it is lowered out-of-line into
// its OWN FuncId by `lowerRetainedSameNameAuthors`.
// A renamed `export … "csym"` fn was declared under its C symbol name
// (declareFunction's rename path), so search for the stub under that name
// and promote the body into it. `extern_name_map` only carries an entry
// when a rename was registered; a bare export / normal define keeps its sx
// name (Phase 2.2).
const search_name = self.extern_name_map.get(name) orelse name;
const name_id = self.module.types.internString(search_name);
var func_id: ?FuncId = null;
for (self.module.functions.items, 0..) |func, i| {
if (func.name == name_id) {
func_id = FuncId.fromIndex(@intCast(i));
break;
}
}
if (func_id) |fid| {
self.lowerFunctionBodyInto(fd, fid, name);
return;
}
// Function not yet declared — create it fresh via lowerFunction. A
// module-qualified alias (`ns.fn`) is registered in
// `fn_ast_map` without an eager `declareFunction`, so there's no
// `Function.source_file` to switch to. 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" (0100 F1).
// The reentry guard keeps the nested lowering transparent to the caller.
var reentry = FnBodyReentry.enter(self);
defer reentry.restore();
if (self.program_index.qualified_fn_source.get(name)) |src| {
self.setCurrentSourceFile(src);
}
self.lowerFunction(fd, name, false);
}
/// Lower `fd`'s body into the SPECIFIC `fid`, promoting its extern stub to a
/// real function. Identity-addressable: the caller passes the exact FuncId,
/// so a SHADOWED same-name author lowers into its OWN slot instead of
/// colliding on the name-keyed `resolveFuncByName` (which returns the first
/// author, the very split that trips the param-count assert). Self-
/// contained — the `FnBodyReentry` guard makes the nested lowering
/// transparent to any in-progress caller body — so it serves
/// both `lazyLowerFunction`'s name-keyed found path and the out-of-line
/// `lowerRetainedSameNameAuthors` pass.
pub fn lowerFunctionBodyInto(self: *Lowering, fd: *const ast.FnDecl, fid: FuncId, name: []const u8) void {
// objc-defined-class method context for `*Self` substitution (M1.2 A.2b);
// the resolveReturnType / resolveParamType calls below consult it.
const saved_fc = self.current_foreign_class;
defer self.current_foreign_class = saved_fc;
if (self.lookupObjcDefinedClassForMethod(name)) |fcd| {
self.current_foreign_class = fcd;
}
var reentry = FnBodyReentry.enter(self);
defer reentry.restore();
// Re-use the existing function slot — switch builder to it. Pin the
// function's OWN source BEFORE resolving the return type, so a same-name
// shadowed type in the signature resolves against THIS
// function's module rather than the caller's (which, importing two
// same-name authors, would be ambiguous). Param types below already
// resolve after this point.
self.builder.func = fid;
const func = &self.module.functions.items[@intFromEnum(fid)];
self.setCurrentSourceFile(func.source_file);
// `extern` imports are pure declarations — never promote the stub to a real
// function or lower the (empty placeholder) body. Mirrors the declare-only
// handling in lowerFunction / lazyLowerFunction.
if (fd.extern_export == .extern_) return;
const ret_ty = self.resolveReturnType(fd);
if (!func.is_extern) {
// Already promoted (e.g., via lowerComptimeDeps) — skip.
return;
}
func.is_extern = false; // promote from extern stub to real function
// `export` defines force external linkage + C ABI (Phase 2, gaps i+ii).
func.linkage = if (isExportedEntryName(name) or fd.extern_export == .export_) .external else .internal;
if (fd.call_conv == .c or fd.extern_export == .export_) func.call_conv = .c;
// Set inst_counter to param count (params occupy refs 0..N-1). IR params
// = AST params + 1 if the function carries `__sx_ctx` at slot 0.
const ctx_slots: usize = if (func.has_implicit_ctx) 1 else 0;
std.debug.assert(func.params.len == fd.params.len + ctx_slots);
self.builder.inst_counter = @intCast(func.params.len);
// Create entry block
const entry_name = self.module.types.internString("entry");
const entry = self.builder.appendBlock(entry_name, &.{});
self.builder.switchToBlock(entry);
// Create scope and bind params
var scope = Scope.init(self.alloc, null);
defer scope.deinit();
self.scope = &scope;
// The implicit `__sx_ctx` param (when present) lives at slot 0; user
// params shift by one. `current_ctx_ref` is bound to slot 0 so call-site
// lowering can prepend it to every sx-to-sx call. For OS-called entry
// points (main / JNI hooks) there's no ctx param — synthesise
// `&__sx_default_context` and bind `current_ctx_ref` to its address.
const wants_ctx = self.funcWantsImplicitCtx(fd);
const saved_ctx_ref = self.current_ctx_ref;
defer self.current_ctx_ref = saved_ctx_ref;
const user_param_base: u32 = if (wants_ctx) 1 else 0;
if (wants_ctx) self.current_ctx_ref = Ref.fromIndex(0);
for (fd.params, 0..) |p, i| {
const pty = self.resolveParamType(&p);
const slot = self.builder.alloca(pty);
const param_ref = Ref.fromIndex(@intCast(i + user_param_base));
self.builder.store(slot, param_ref);
scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true });
}
// Inbound entry points + callconv(.c) sx functions: bind current_ctx_ref
// to the static default before any user code runs.
if (!wants_ctx and self.implicit_ctx_enabled) {
if (self.program_index.global_names.get("__sx_default_context")) |dctx_gi| {
self.current_ctx_ref = self.builder.emit(.{ .global_addr = dctx_gi.id }, self.module.types.ptrTo(.void));
}
}
// Lower the function body (set target_type to return type for implicit returns)
const saved_target = self.target_type;
self.target_type = if (ret_ty != .void and ret_ty != .noreturn) ret_ty else null;
if (ret_ty != .void and ret_ty != .noreturn) {
self.lowerValueBody(fd.body, ret_ty);
} else {
// void / noreturn: no value to return — lower as statements and let
// `ensureTerminator` close the block (ret void / unreachable).
self.lowerBlock(fd.body);
self.ensureTerminator(ret_ty);
}
self.target_type = saved_target;
self.builder.finalize();
}
/// Lower a single function declaration.
pub fn lowerFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8, is_imported: bool) void {
// For sx-defined `#objc_class` methods (qualified `<Class>.<method>`),
// set `current_foreign_class` so `*Self` substitutions through
// `resolveTypeWithBindings` find the state-struct type (M1.2 A.2b).
// Save+restore — function lowering can re-enter.
const saved_fc = self.current_foreign_class;
defer self.current_foreign_class = saved_fc;
if (self.lookupObjcDefinedClassForMethod(name)) |fcd| {
self.current_foreign_class = fcd;
}
const name_id = self.module.types.internString(name);
const ret_ty = self.resolveReturnType(fd);
const wants_ctx = self.funcWantsImplicitCtx(fd);
// Build param list. `Function.init` borrows the slice (it does not
// dupe), so this storage must outlive the local — build it in the
// module's slice arena (freed at module deinit) rather than via
// `self.alloc`, which would leak (Function.deinit never frees params).
const param_alloc = self.module.slice_arena.allocator();
var params = std.ArrayList(Function.Param).empty;
if (wants_ctx) {
params.append(param_alloc, .{
.name = self.module.types.internString("__sx_ctx"),
.ty = self.module.types.ptrTo(.void),
}) catch unreachable;
}
for (fd.params) |p| {
const pty = self.resolveParamType(&p);
params.append(param_alloc, .{
.name = self.module.types.internString(p.name),
.ty = pty,
}) catch unreachable;
}
// Check if the function body is a builtin or foreign declaration (no body
// needed). `extern` imports are declare-only too (empty placeholder body).
if (fd.body.data == .builtin_expr or fd.body.data == .foreign_expr or fd.body.data == .compiler_expr or fd.extern_export == .extern_) {
// Already declared by scanDecls/declareFunction (which handles #foreign renames)
return;
}
// Skip generic functions (they have type parameters and are templates, not concrete)
if (fd.type_params.len > 0) {
const fid = self.builder.declareExtern(name_id, params.items, ret_ty);
self.module.getFunctionMut(fid).has_implicit_ctx = wants_ctx;
return;
}
// Imported functions: declare as extern (don't lower bodies from other files)
if (is_imported) {
const fid = self.builder.declareExtern(name_id, params.items, ret_ty);
self.module.getFunctionMut(fid).has_implicit_ctx = wants_ctx;
return;
}
const func_id = self.builder.beginFunction(
name_id,
params.items,
ret_ty,
);
_ = 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.
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
// loader calls — `main`, Android NativeActivity hooks — which must
// stay externally visible.
// `export` defines force external linkage (Phase 2, gap i) alongside
// the OS-called entry points.
if (isExportedEntryName(name) or fd.extern_export == .export_) {
self.builder.currentFunc().linkage = .external;
}
// Set calling convention. `export` defines promote to C ABI (gap ii).
if (fd.call_conv == .c or fd.extern_export == .export_) {
self.builder.currentFunc().call_conv = .c;
}
// Create entry block
const entry_name = self.module.types.internString("entry");
const entry = self.builder.appendBlock(entry_name, &.{});
self.builder.switchToBlock(entry);
// Create scope and bind params
var scope = Scope.init(self.alloc, self.scope);
defer scope.deinit();
self.scope = &scope;
defer self.scope = scope.parent;
// Implicit `__sx_ctx` at slot 0 when funcWantsImplicitCtx is true;
// user params shift by one. Bind `current_ctx_ref` for call-site
// forwarding inside the body.
const wants_ctx_lf = self.funcWantsImplicitCtx(fd);
const saved_ctx_ref_lf = self.current_ctx_ref;
defer self.current_ctx_ref = saved_ctx_ref_lf;
const user_param_base_lf: u32 = if (wants_ctx_lf) 1 else 0;
if (wants_ctx_lf) self.current_ctx_ref = Ref.fromIndex(0);
for (fd.params, 0..) |p, i| {
const pty = self.resolveParamType(&p);
// Allocate stack slot for param, store initial value.
// Refs 0..N-1 are reserved for function parameters by beginFunction.
const slot = self.builder.alloca(pty);
const param_ref = Ref.fromIndex(@intCast(i + user_param_base_lf));
self.builder.store(slot, param_ref);
scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true });
}
// Inbound entry points + callconv(.c) sx functions: bind
// current_ctx_ref to &__sx_default_context. See companion comment
// in `lowerFunction` for the same case.
if (!wants_ctx_lf and self.implicit_ctx_enabled) {
if (self.program_index.global_names.get("__sx_default_context")) |dctx_gi| {
self.current_ctx_ref = self.builder.emit(.{ .global_addr = dctx_gi.id }, self.module.types.ptrTo(.void));
}
}
// Lower the function body, capturing the last expression's value for implicit return
const saved_target = self.target_type;
self.target_type = if (ret_ty != .void and ret_ty != .noreturn) ret_ty else null;
if (ret_ty != .void and ret_ty != .noreturn) {
self.lowerValueBody(fd.body, ret_ty);
} else {
// void / noreturn: no value to return — lower as statements and
// let `ensureTerminator` close the block (ret void / unreachable).
self.lowerBlock(fd.body);
self.ensureTerminator(ret_ty);
}
self.target_type = saved_target;
self.builder.finalize();
}
// ── Module-const emission ───────────────────────────────────────
pub fn emitModuleConst(self: *Lowering, ci: ModuleConstInfo, author_source: ?[]const u8) Ref {
// F1: a const read from another module folds/lowers its RHS in the
// AUTHOR's visibility context, so a same-name leaf (`K :: M + 1` selected
// from `a.sx`) resolves `M` against `a.sx` — not against the reading
// module, which may flat-import a different same-name `M`. Single-author /
// own-read consts pin to the source they were already in → byte-identical.
const author_pin = self.pinConstAuthorSource(author_source);
defer author_pin.unpin();
// An integer-typed const whose initializer is a compile-time integer —
// an int literal/expression, OR an INTEGRAL float that `typedConstInitFits`
// accepted under the unified narrowing rule — materializes as its folded
// int through the SAME `program_index.foldCountI64` the count / array-dim
// path uses, so the const's emitted VALUE and its use as a COUNT come from
// one fold (`K : i64 : 4.0` → 4; `K : i64 : M + 2.0` → 4; and a float-const-
// leaf `KF : i64 : F + 1.5` → 4, which the int-only folder could not reach).
// A non-integral float never arrives (it was rejected at registration); any
// other non-foldable shape falls through to the per-kind emitters below.
if (self.isIntEx(ci.ty)) {
switch (program_index_mod.foldCountI64(ci.value, self)) {
.int => |iv| return self.builder.constInt(iv, ci.ty),
.non_integral, .not_const => {},
}
}
switch (ci.value.data) {
.int_literal => |lit| {
// If declared type is float, convert integer value to float constant
if (ci.ty == .f32 or ci.ty == .f64) {
return self.builder.constFloat(@floatFromInt(lit.value), ci.ty);
}
return self.builder.constInt(lit.value, ci.ty);
},
.float_literal => |lit| return self.builder.constFloat(lit.value, ci.ty),
.bool_literal => |lit| return self.builder.emit(.{ .const_bool = lit.value }, .bool),
.string_literal => |lit| {
const str = if (lit.is_raw) lit.raw else unescape.unescapeString(self.alloc, lit.raw) catch lit.raw;
const sid = self.module.types.internString(str);
return self.builder.constString(sid);
},
.undef_literal => return self.builder.constUndef(ci.ty),
.null_literal => return self.builder.constNull(ci.ty),
else => {
// Complex expressions (struct_literal, call, etc.) — lower on demand
const saved_target = self.target_type;
self.target_type = ci.ty;
const result = self.lowerExpr(ci.value);
self.target_type = saved_target;
return result;
},
}
}
pub fn emitPlaceholder(self: *Lowering, name: []const u8) Ref {
const sid = self.module.types.internString(name);
return self.builder.emit(.{ .placeholder = sid }, .i64);
}