diff --git a/src/ir/lower.zig b/src/ir/lower.zig index a05eca4..0e73497 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -37,6 +37,7 @@ const lower_error = @import("lower/error.zig"); const lower_comptime = @import("lower/comptime.zig"); const lower_stmt = @import("lower/stmt.zig"); const lower_control_flow = @import("lower/control_flow.zig"); +const lower_decl = @import("lower/decl.zig"); const TypeId = types.TypeId; const StringId = types.StringId; @@ -47,16 +48,6 @@ const Function = inst_mod.Function; const Module = mod_mod.Module; const Builder = mod_mod.Builder; -/// 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_"); -} /// One frame in the chain of module-const names currently being folded by the /// SOURCE-AWARE const evaluator (`Lowering.foldSourceConstInt` and its float @@ -419,7 +410,7 @@ pub const Lowering = struct { /// transparent to the caller's own lowering — notably `block_terminated`, /// which leaking back would mark the caller's trailing statements /// dead-after-terminator (issue 0100 F2). - const FnBodyReentry = struct { + pub const FnBodyReentry = struct { l: *Lowering, func: ?FuncId, block: ?BlockId, @@ -435,7 +426,7 @@ pub const Lowering = struct { pack_arg_types: ?std.StringHashMap([]const TypeId), inline_return_target: ?InlineReturnInfo, - fn enter(l: *Lowering) FnBodyReentry { + pub fn enter(l: *Lowering) FnBodyReentry { const g = FnBodyReentry{ .l = l, .func = l.builder.func, @@ -468,7 +459,7 @@ pub const Lowering = struct { return g; } - fn restore(g: FnBodyReentry) void { + pub fn restore(g: FnBodyReentry) void { const l = g.l; l.setCurrentSourceFile(g.source_file); l.scope = g.scope; @@ -501,2259 +492,6 @@ pub const Lowering = struct { // ── Public entry point ────────────────────────────────────────── - /// 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 (issue 0064). - // 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_(...)` declared in - // the bundled classes.dex by looking up the symbol - // `Java___sx_1` 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. - 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. - 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. - 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: s64 (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. - 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). - 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__impl` with the ctx param) lands in Step 4 proper. - fn funcWantsImplicitCtx(self: *const Lowering, fd: *const ast.FnDecl) bool { - if (!self.implicit_ctx_enabled) return false; - if (fd.call_conv == .c) 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. - 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. - 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); - } - 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); - } - 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. - fn scanDecls(self: *Lowering, decls: []const *const Node) void { - // Pass 0: register every numeric-literal module const (`N :: 16` and the - // typed `N : s64 : 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 (issue 0083). 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 (issue 0070). - 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 = .s64 }; - 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` - // (issue 0083). Placeholder `.s64` 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 = .s64 }; - 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 - // (issue 0105). - // - // "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 (issue 0100). The - // shadowed function stays reachable via its qualified name. - if (!self.program_index.fn_ast_map.contains(fd.name)) { - self.program_index.fn_ast_map.put(fd.name, &decl.data.fn_decl) catch {}; - self.program_index.import_flags.put(fd.name, is_imported) catch {}; - } - // Declare extern stub for all functions (bodies lowered - // lazily). 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 :: (s32) -> s32; - 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 :: []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 (issue 0083 — 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) { - // 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(s64)` 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(s64)` / - // `a.Box(s64)`): 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(s64)` 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()`. - 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 :s32: 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 (issue 0070). 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 => .s64, - .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 (issue 0070). - .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 (issue 0070). - for (decls) |decl| { - self.setCurrentSourceFile(decl.source_file); - switch (decl.data) { - .var_decl => self.registerTopLevelGlobal(&decl.data.var_decl), - .const_decl => |cd| self.registerTypedModuleConst(&cd), - else => {}, - } - } - } - - /// Register a typed module-level value constant (`AF_INET :s32: 2`). Run in - /// scanDecls pass 2 (after `resolveForwardIdentifierAliases`) so a forward - /// identifier alias in the annotation (`A :: B; B :: s32; K : A : 42;`) - /// resolves to its target rather than a fabricated empty-struct stub, which - /// would otherwise mistype the constant (issue 0070). - 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 - // `.s64`) 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 the issue-0088 - // 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 => {}, - 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]s64` → 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 `.s64` 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, s64)` yields `.widen` and would accept the bogus - /// `B : s64 : true`. - fn typedConstInitFits(self: *Lowering, value: *const Node, dst_ty: TypeId) bool { - // An INTEGER-annotated constant accepts a compile-time INTEGRAL float — - // a literal (`K : s64 : 4.0`), an int-leaf expression (`K : s64 : M + 2.0` - // → 4), or a float-const-leaf expression whose SUM is integral - // (`F : f64 : 2.5; K : s64 : 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 (issue 0088, - // attempt 2). 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). - 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 a top-level mutable global (e.g., `context : Context = ---;`). - /// Run AFTER `resolveForwardIdentifierAliases` so a forward identifier alias - /// in the type annotation (`A :: B; B :: s32; 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 (issue 0070). Globals can't be named in a type position, so - /// deferring them past type/alias registration introduces no ordering hazard. - 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 globals reference a symbol defined in libSystem etc. - // (`_NSConcreteStackBlock : *void #foreign;`). The C symbol - // name is the optional override or the sx name itself. - const sym_name = vd.foreign_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_foreign, - }); - self.putGlobal(self.current_source_file, vd.name, .{ .id = gid, .ty = var_ty }); - } - - /// Serialize a top-level global's initializer into a static `ConstantValue`. - /// Foreign globals (extern 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, issue 0071); 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. - fn globalInitValue(self: *Lowering, vd: *const ast.VarDecl, var_ty: TypeId) ?inst_mod.ConstantValue { - if (vd.is_foreign) return null; - const v = vd.value orelse return null; - return switch (v.data) { - .undef_literal => .zeroinit, - .null_literal => .null_val, - .int_literal => |il| .{ .int = il.value }, - .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 }, - .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 (issues 0071/0072). - 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 (issues 0071/0072/0080). - 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 (issue 0069). Re-resolve to a fixpoint now that every top-level name - /// has been seen, so `A :: B; B :: s32;` converges the same as the ordered - /// `B :: s32; 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 issue 0068 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). - 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 :: `s2 ``, target declared later) resolves - // to the nominal `` `s2 `` author, not the builtin `s2` 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). - 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. - fn lowerMainAndComptime(self: *Lowering, decls: []const *const Node) void { - for (decls) |decl| { - // A `#run` body lowers in its OWN module's source context (fix-0102d - // site 4): `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) { - if (isExportedEntryName(cd.name)) { - 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)) { - 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 (fix-0102b). It does NOT run during a default compile: the name path - /// stays the sole resolver, so the suite is byte-for-byte unchanged. fix-0102c - /// 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`). fix-0102a retained 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 (fix-0102c, 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 (fix-0102c F1). - 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, issue 0105). 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 (fix-0102c, R5 §C). `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 (issue 0089): 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 :: `, 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, issue 0105/0107). - 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 issue 0107 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. - 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, .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 (issue 0105). 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). - 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, .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). - 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`. - 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. - 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. - 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. - 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). - 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. - 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). - 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(s64)` 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 :: () -> s32`, 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). - 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. - 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 fix-0102b's identity- - /// addressable `lowerFunctionBodyInto`. Idempotent: `lowered_fids` tracks - /// which slots already carry a body. - 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; - } - - /// Declare a function as an extern stub (signature only, no body). - 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); - - // 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; - var is_variadic = false; - var effective_params = fd.params; - if (is_foreign 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) .c else .default; - - // For #foreign with C name override, declare under C name and map sx name → C name - if (is_foreign) { - const fe = fd.body.data.foreign_expr; - if (fe.c_name) |c_name| { - const c_name_id = self.module.types.internString(c_name); - 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.foreign_name_map.put(name, c_name) catch {}; - self.fn_decl_fids.put(fd, fid) catch {}; - return; - } - } - - const name_id = self.module.types.internString(name); - 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 (issue 0100). - /// The bare recursion in `scanDecls` still registers intra-module bare calls; - /// this adds the qualified identity the `pkg.fn(...)` resolution paths in - /// `CallResolver.plan` / `lowerCall` already prefer. - fn registerNamespaceQualifiedFns(self: *Lowering, ns_name: []const u8, own_decls: []const *Node) void { - const saved_source = self.current_source_file; - defer self.setCurrentSourceFile(saved_source); - for (own_decls) |decl| { - self.setCurrentSourceFile(decl.source_file); - switch (decl.data) { - .fn_decl => self.registerQualifiedFn(ns_name, &decl.data.fn_decl, decl.data.fn_decl.name), - .const_decl => |cd| { - if (cd.value.data == .fn_decl) { - self.registerQualifiedFn(ns_name, &cd.value.data.fn_decl, cd.name); - } - }, - else => {}, - } - } - } - - fn registerQualifiedFn(self: *Lowering, ns_name: []const u8, fd: *const ast.FnDecl, short: []const u8) void { - // Only PLAIN free functions need a qualified identity. Generic / - // comptime / pack functions (`Vector`, `print`, `any_to_string`) are - // dispatched by monomorphization off their BARE template name, not the - // plain `resolveFuncByName` / `lazyLowerFunction` path that trips the - // collision assert (issue 0100); registering a qualified alias for them - // would divert that machinery and strand a per-call type binding. - if (fd.type_params.len > 0 or hasComptimeParams(fd) or isPackFn(fd)) return; - // Foreign / builtin / #compiler bodies keep their literal name; a - // qualified alias has no distinct symbol to resolve to. - switch (fd.body.data) { - .foreign_expr, .builtin_expr, .compiler_expr => return, - else => {}, - } - const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ ns_name, short }) catch return; - if (self.program_index.fn_ast_map.contains(qualified)) return; - self.program_index.fn_ast_map.put(qualified, fd) catch {}; - self.program_index.import_flags.put(qualified, true) catch {}; - // Carry the alias's OWN declaring source file (the caller in - // `registerNamespaceQualifiedFns` pins `current_source_file` to the - // decl's source before each call). `lazyLowerFunction`'s null-FuncId - // path restores this so `ns.fn`'s body lowers in its own module's - // visibility context, not the call site's (issue 0100 F1). - if (self.current_source_file) |src| { - self.program_index.qualified_fn_source.put(qualified, src) catch {}; - } - // No eager `declareFunction` here: the extern stub's param/return types - // would be resolved now, before the forward-alias fixpoint, caching an - // `.unresolved` for any type declared later in the module. The qualified - // function is declared + lowered on demand by `lazyLowerFunction`'s - // null-FuncId path (`lowerFunction`), which runs after all types resolve. - } - - /// 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. - 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 C-import fn_decls without a library_ref - // are policed; a non-foreign body or a library-bound foreign - // decl is unconditionally visible. - const fd = self.program_index.fn_ast_map.get(name) orelse return true; - if (fd.body.data != .foreign_expr) return true; - if (fd.body.data.foreign_expr.library_ref != 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. - 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`. - 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`. - 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) { - 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 ([]s32, List__s32, 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` (fix-0102b). - const name_id = self.module.types.internString(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`, issue 0100) 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 issue 0100's param-count assert). Self- - /// contained — the `FnBodyReentry` guard makes the nested lowering - /// transparent to any in-progress caller body (issue 0100 F2) — so it serves - /// both `lazyLowerFunction`'s name-keyed found path and the out-of-line - /// `lowerRetainedSameNameAuthors` pass. - 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 (issue 0105) 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); - - 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 - func.linkage = if (isExportedEntryName(name)) .external else .internal; - if (fd.call_conv == .c) 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 `.`), - // 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) - if (fd.body.data == .builtin_expr or fd.body.data == .foreign_expr or fd.body.data == .compiler_expr) { - // 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 (issue 0100 F1). - self.builder.currentFunc().source_file = self.current_source_file; - - // Set linkage. Default for fn defs is `internal` (LLVM DCE-friendly, - // matches C `static`). isExportedEntryName lists the names the OS - // loader calls — `main`, Android NativeActivity hooks — which must - // stay externally visible. - if (isExportedEntryName(name)) { - self.builder.currentFunc().linkage = .external; - } - - // Set calling convention - if (fd.call_conv == .c) { - 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(); - } - - // ── Statement lowering ────────────────────────────────────────── - pub fn lowerExpr(self: *Lowering, node: *const Node) Ref { // Stamp this node's source span onto the instructions it emits (ERR // E3.0 — feeds DWARF line-info + comptime frame resolution). Save/ @@ -10520,7 +8258,7 @@ pub const Lowering = struct { } /// Check if a function has comptime (non-Type) value parameters. - fn hasComptimeParams(fd: *const ast.FnDecl) bool { + pub fn hasComptimeParams(fd: *const ast.FnDecl) bool { for (fd.params) |p| { if (p.is_comptime) return true; } @@ -10532,7 +8270,7 @@ pub const Lowering = struct { /// out-of-line identity-addressable slot — the bare-call disambiguation /// (fix-0102c) and the shadow-author lowering pass leave every other shape /// to the existing name-keyed dispatch. - fn isPlainFreeFn(fd: *const ast.FnDecl) bool { + pub fn isPlainFreeFn(fd: *const ast.FnDecl) bool { if (fd.type_params.len > 0) return false; return switch (fd.body.data) { .foreign_expr, .builtin_expr, .compiler_expr => false, @@ -10546,7 +8284,7 @@ pub const Lowering = struct { /// comptime VALUES into the mangled name and binds them as both /// comptime substitutions (for #insert) and runtime locals (for /// bare-name body references). - fn isPackFn(fd: *const ast.FnDecl) bool { + pub fn isPackFn(fd: *const ast.FnDecl) bool { for (fd.params) |p| { if (isPackParam(p)) return true; } @@ -10701,7 +8439,7 @@ pub const Lowering = struct { /// Construct a `TypeResolver` view over the current lowering state (borrows /// only; cheap by-value, reflects current `diagnostics` / `program_index`). - fn typeResolver(self: *Lowering) TypeResolver { + pub fn typeResolver(self: *Lowering) TypeResolver { return .{ .alloc = self.alloc, .types = &self.module.types, @@ -11106,7 +8844,7 @@ pub const Lowering = struct { /// - bare, visible author IS the canonical map author → the global template /// (byte-identical single-author path). /// - not in `struct_template_map` at all → `.not_generic`. - fn selectGenericStructHead(self: *Lowering, name: []const u8, alias: ?[]const u8, is_qualified: bool, span: ?ast.Span) HeadTemplate { + pub fn selectGenericStructHead(self: *Lowering, name: []const u8, alias: ?[]const u8, is_qualified: bool, span: ?ast.Span) HeadTemplate { if (is_qualified) { if (alias) |a| { if (self.qualifiedStructTemplate(a, name)) |tmpl| return .{ .template = tmpl }; @@ -11263,7 +9001,7 @@ pub const Lowering = struct { /// wins outright (own-wins) and is exempt; falls open when unwired / /// default-context. Diagnostic mirrors the type form (the head IS used as a type /// here). - fn headFnLeak(self: *Lowering, name: []const u8, span: ?ast.Span) bool { + pub fn headFnLeak(self: *Lowering, name: []const u8, span: ?ast.Span) bool { if (self.emitting_default_context) return false; const from = self.current_source_file orelse return false; if (self.scope) |s| if (s.lookupFn(name) != null) return false; @@ -11338,7 +9076,7 @@ pub const Lowering = struct { } /// Resolve a .call node that represents a type constructor (e.g., List(T), Vector(N, T)). - fn resolveTypeCallWithBindings(self: *Lowering, cl: *const ast.Call) TypeId { + pub fn resolveTypeCallWithBindings(self: *Lowering, cl: *const ast.Call) TypeId { // A namespaced callee (`ns.Box(..)`) is an explicit qualified reach and is // exempt from the bare-head visibility gate; only a plain identifier head // is policed (E4). @@ -11511,7 +9249,7 @@ pub const Lowering = struct { /// coincident keysets. A future writer that registers an instance's layout /// without stamping its author (a silent body-axis reopen) trips this in a /// debug `zig build test`, not in production. - fn assertInstanceMapsCoincide(self: *Lowering) void { + pub fn assertInstanceMapsCoincide(self: *Lowering) void { if (!std.debug.runtime_safety) return; var it = self.struct_instance_template.keyIterator(); while (it.next()) |k| { @@ -11694,7 +9432,7 @@ pub const Lowering = struct { /// `Complex :: ($T:Type) -> Type { return struct { value: T; count: u32; }; }` /// Walks the function body to find the returned struct/enum, resolves field types /// with the provided type bindings, and registers the result. - fn instantiateTypeFunction(self: *Lowering, alias_name: []const u8, template_name: []const u8, fd: *const ast.FnDecl, args: []const *const Node) ?TypeId { + pub fn instantiateTypeFunction(self: *Lowering, alias_name: []const u8, template_name: []const u8, fd: *const ast.FnDecl, args: []const *const Node) ?TypeId { const table = &self.module.types; // Build type bindings from params + args @@ -12040,17 +9778,17 @@ pub const Lowering = struct { @"enum": *const ast.EnumDecl, @"union": *const ast.UnionDecl, - fn key(self: ShadowTypeDecl) *const anyopaque { + pub fn key(self: ShadowTypeDecl) *const anyopaque { return switch (self) { inline else => |p| @ptrCast(p), }; } - fn name(self: ShadowTypeDecl) []const u8 { + pub fn name(self: ShadowTypeDecl) []const u8 { return switch (self) { inline else => |p| p.name, }; } - fn isGeneric(self: ShadowTypeDecl) bool { + pub fn isGeneric(self: ShadowTypeDecl) bool { return switch (self) { .@"struct" => |p| p.type_params.len > 0, else => false, @@ -12063,7 +9801,7 @@ pub const Lowering = struct { /// genuine-shadow scan enumerates all three kinds uniformly. Null when the node /// is not a struct/enum/union author. The shared infra E6b/E6c extend by adding /// their kind here. - fn topLevelTypeDecl(decl: *const Node) ?ShadowTypeDecl { + pub fn topLevelTypeDecl(decl: *const Node) ?ShadowTypeDecl { return switch (decl.data) { .struct_decl => .{ .@"struct" = &decl.data.struct_decl }, .enum_decl => .{ .@"enum" = &decl.data.enum_decl }, @@ -12079,7 +9817,7 @@ pub const Lowering = struct { } /// Dispatch a genuine-shadow reservation to the matching per-kind reserver. - fn reserveShadowSlot(self: *Lowering, td: ShadowTypeDecl) void { + pub fn reserveShadowSlot(self: *Lowering, td: ShadowTypeDecl) void { switch (td) { .@"struct" => |sd| self.reserveShadowStructSlot(sd), .@"enum" => |ed| self.reserveShadowEnumSlot(ed), @@ -12369,7 +10107,7 @@ pub const Lowering = struct { /// `.call` and `.parameterized_type_expr` const-decl alias branches and the /// qualified-head selection that precedes the bare `struct_template_map` /// fallback in each. - fn registerGenericStructAlias(self: *Lowering, alias_name: []const u8, tmpl: *const StructTemplate, args: []const *const Node) void { + pub fn registerGenericStructAlias(self: *Lowering, alias_name: []const u8, tmpl: *const StructTemplate, args: []const *const Node) void { const inst_id = self.instantiateGenericStruct(tmpl, args); const alias_name_id = self.module.types.internString(alias_name); const inst_info = self.module.types.get(inst_id); @@ -12803,7 +10541,7 @@ pub const Lowering = struct { /// under qualified names `.`. Lazy lowering /// then handles the body via the standard path; `*Self` is /// substituted to `*State` during body lowering (M1.2 A.2b). - fn registerForeignClassDecl(self: *Lowering, fcd: *const ast.ForeignClassDecl) void { + pub fn registerForeignClassDecl(self: *Lowering, fcd: *const ast.ForeignClassDecl) void { self.program_index.foreign_class_map.put(fcd.name, fcd) catch {}; if (!fcd.is_foreign and fcd.runtime == .objc_class) { if (self.module.lookupObjcDefinedClass(fcd.name) == null) { @@ -12982,7 +10720,7 @@ pub const Lowering = struct { /// ForeignClassDecl. Used by `lowerFunction` to set /// `current_foreign_class` so `*Self` resolves to the state struct /// during body lowering. - fn lookupObjcDefinedClassForMethod(self: *Lowering, name: []const u8) ?*const ast.ForeignClassDecl { + pub fn lookupObjcDefinedClassForMethod(self: *Lowering, name: []const u8) ?*const ast.ForeignClassDecl { const dot = std.mem.indexOf(u8, name, ".") orelse return null; return self.module.lookupObjcDefinedClass(name[0..dot]); } @@ -13024,7 +10762,7 @@ pub const Lowering = struct { /// scan/lower already handles bare-name registration; this only adds the /// qualified-name entry, so cross-class refs in method signatures /// (`*View` → bare lookup) still work. - fn registerNamespacedForeignClasses(self: *Lowering, ns: ast.NamespaceDecl) void { + pub fn registerNamespacedForeignClasses(self: *Lowering, ns: ast.NamespaceDecl) void { for (ns.decls) |inner| { if (inner.data == .foreign_class_decl) { const fcd = &inner.data.foreign_class_decl; @@ -13089,7 +10827,7 @@ pub const Lowering = struct { /// context call entry (Step 7). Only emitted when the program imports /// `std.sx` — without that, Context / Allocator / CAllocator aren't /// registered and the global has no purpose. - fn emitDefaultContextGlobal(self: *Lowering) void { + pub fn emitDefaultContextGlobal(self: *Lowering) void { const saved_edc = self.emitting_default_context; self.emitting_default_context = true; defer self.emitting_default_context = saved_edc; @@ -14028,62 +11766,6 @@ pub const Lowering = struct { }; } - 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 : s64 : 4.0` → 4; `K : s64 : M + 2.0` → 4; and a float-const- - // leaf `KF : s64 : 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; - }, - } - } - - fn emitPlaceholder(self: *Lowering, name: []const u8) Ref { - const sid = self.module.types.internString(name); - return self.builder.emit(.{ .placeholder = sid }, .s64); - } - /// Check if a name refers to a known type (primitive or registered struct/enum/union). /// Used to distinguish type-as-value (silent placeholder) from genuinely unresolved names. pub fn isKnownTypeName(self: *Lowering, name: []const u8) bool { @@ -14426,7 +12108,7 @@ pub const Lowering = struct { /// const-expression is described by its inferred type category, so the /// message is accurate for `N : string : M + 2` ("an integer expression") /// as well as for `N : string : 4` ("an integer literal"). - fn initializerDescription(self: *Lowering, node: *const Node) []const u8 { + pub fn initializerDescription(self: *Lowering, node: *const Node) []const u8 { return switch (node.data) { .int_literal => "an integer literal", .float_literal => "a float literal", @@ -14564,7 +12246,7 @@ pub const Lowering = struct { /// resolve correctly (M1.2 A.2b). After the bodies are lowered, /// `emitObjcDefinedClassImps` wraps each with a C-ABI trampoline /// (M1.2 A.4b.ii). - fn lowerObjcDefinedClassMethods(self: *Lowering) void { + pub fn lowerObjcDefinedClassMethods(self: *Lowering) void { for (self.module.objc_defined_class_cache.items) |entry| { const fcd = entry.decl; for (fcd.members) |m| { @@ -15824,7 +13506,7 @@ pub const Lowering = struct { return null; } - fn synthesizeJniMainStubs(self: *Lowering) void { + pub fn synthesizeJniMainStubs(self: *Lowering) void { var seen = std.StringHashMap(void).init(self.alloc); defer seen.deinit(); @@ -16088,6 +13770,63 @@ pub const Lowering = struct { pub const freshBlockWithParams = lower_control_flow.freshBlockWithParams; pub const currentBlockHasTerminator = lower_control_flow.currentBlockHasTerminator; pub const ensureTerminator = lower_control_flow.ensureTerminator; + + // --- moved to lower/decl.zig (lower_decl) --- + pub const SelectedFunc = lower_decl.SelectedFunc; + pub const BareCallee = lower_decl.BareCallee; + pub const VisibleStructAuthor = lower_decl.VisibleStructAuthor; + pub const lowerRoot = lower_decl.lowerRoot; + pub const validateMainSignature = lower_decl.validateMainSignature; + pub const checkRequiredEntryPoints = lower_decl.checkRequiredEntryPoints; + pub const injectComptimeConstants = lower_decl.injectComptimeConstants; + pub const findVariantIndex = lower_decl.findVariantIndex; + pub const lowerDeferredTypeFns = lower_decl.lowerDeferredTypeFns; + pub const lowerDecls = lower_decl.lowerDecls; + pub const detectContextDecl = lower_decl.detectContextDecl; + pub const funcWantsImplicitCtx = lower_decl.funcWantsImplicitCtx; + pub const fnPtrTypeWantsCtx = lower_decl.fnPtrTypeWantsCtx; + pub const scanDecls = lower_decl.scanDecls; + pub const registerTypedModuleConst = lower_decl.registerTypedModuleConst; + pub const typedConstInitFits = lower_decl.typedConstInitFits; + pub const constExprInitFits = lower_decl.constExprInitFits; + pub const registerTopLevelGlobal = lower_decl.registerTopLevelGlobal; + pub const globalInitValue = lower_decl.globalInitValue; + pub const diagnoseNonConstGlobal = lower_decl.diagnoseNonConstGlobal; + pub const resolveForwardIdentifierAliases = lower_decl.resolveForwardIdentifierAliases; + pub const aliasResolvedInSource = lower_decl.aliasResolvedInSource; + pub const declareFunction = lower_decl.declareFunction; + pub const registerNamespaceQualifiedFns = lower_decl.registerNamespaceQualifiedFns; + pub const registerQualifiedFn = lower_decl.registerQualifiedFn; + pub const isVisible = lower_decl.isVisible; + pub const visibleOverEdges = lower_decl.visibleOverEdges; + pub const isCImportVisible = lower_decl.isCImportVisible; + pub const isNameVisible = lower_decl.isNameVisible; + pub const lazyLowerFunction = lower_decl.lazyLowerFunction; + pub const lowerFunctionBodyInto = lower_decl.lowerFunctionBodyInto; + pub const lowerFunction = lower_decl.lowerFunction; + pub const lowerMainAndComptime = lower_decl.lowerMainAndComptime; + pub const lowerRetainedSameNameAuthors = lower_decl.lowerRetainedSameNameAuthors; + pub const selectPlainCallableAuthor = lower_decl.selectPlainCallableAuthor; + pub const selectNominalLeaf = lower_decl.selectNominalLeaf; + pub const isNamedTypeKind = lower_decl.isNamedTypeKind; + pub const namedRefTid = lower_decl.namedRefTid; + pub const nameAuthoredAsTypeAnywhere = lower_decl.nameAuthoredAsTypeAnywhere; + pub const recordLocalTypeName = lower_decl.recordLocalTypeName; + pub const localTypeInSource = lower_decl.localTypeInSource; + pub const localTypeInAnySource = lower_decl.localTypeInAnySource; + pub const resolveNominalLeaf = lower_decl.resolveNominalLeaf; + pub const fnDeclOfRaw = lower_decl.fnDeclOfRaw; + pub const structDeclOfRaw = lower_decl.structDeclOfRaw; + pub const structMethodFn = lower_decl.structMethodFn; + pub const typeFnAuthor = lower_decl.typeFnAuthor; + pub const selectedFuncId = lower_decl.selectedFuncId; + pub const bareAuthorFuncId = lower_decl.bareAuthorFuncId; + pub const putTypeAlias = lower_decl.putTypeAlias; + pub const putModuleConst = lower_decl.putModuleConst; + pub const putGlobal = lower_decl.putGlobal; + pub const dropModuleConst = lower_decl.dropModuleConst; + pub const emitModuleConst = lower_decl.emitModuleConst; + pub const emitPlaceholder = lower_decl.emitPlaceholder; }; /// JNI param/return type resolution: user-declared types pass through diff --git a/src/ir/lower/decl.zig b/src/ir/lower/decl.zig new file mode 100644 index 0000000..c965fba --- /dev/null +++ b/src/ir/lower/decl.zig @@ -0,0 +1,2376 @@ +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 parser_mod = @import("../../parser.zig"); +const interp_mod = @import("../interp.zig"); +const errors = @import("../../errors.zig"); +const jni_descriptor = @import("../jni_descriptor.zig"); +const program_index_mod = @import("../program_index.zig"); +const resolver_mod = @import("../resolver.zig"); +const imports_mod = @import("../../imports.zig"); +const ProgramIndex = program_index_mod.ProgramIndex; +const GlobalInfo = program_index_mod.GlobalInfo; +const StructTemplate = program_index_mod.StructTemplate; +const TemplateParam = program_index_mod.TemplateParam; +const ProtocolDeclInfo = program_index_mod.ProtocolDeclInfo; +const ProtocolMethodInfo = program_index_mod.ProtocolMethodInfo; +const ModuleConstInfo = program_index_mod.ModuleConstInfo; +const TypeResolver = @import("../type_resolver.zig").TypeResolver; +const ResolveEnv = @import("../type_resolver.zig").ResolveEnv; +const PackResolver = @import("../packs.zig").PackResolver; +const ExprTyper = @import("../expr_typer.zig").ExprTyper; +const CallResolver = @import("../calls.zig").CallResolver; +const GenericResolver = @import("../generics.zig").GenericResolver; +const ProtocolResolver = @import("../protocols.zig").ProtocolResolver; +const CoercionResolver = @import("../conversions.zig").CoercionResolver; +const ErrorAnalysis = @import("../error_analysis.zig").ErrorAnalysis; +const ErrorFlow = @import("../error_flow.zig").ErrorFlow; +const ObjcLowering = @import("../ffi_objc.zig").ObjcLowering; +const semantic_diagnostics = @import("../semantic_diagnostics.zig"); + +const TypeId = types.TypeId; +const StringId = types.StringId; +const Ref = inst_mod.Ref; +const BlockId = inst_mod.BlockId; +const FuncId = inst_mod.FuncId; +const Function = inst_mod.Function; +const Module = mod_mod.Module; +const Builder = mod_mod.Builder; + +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 (issue 0064). + // 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_(...)` declared in + // the bundled classes.dex by looking up the symbol + // `Java___sx_1` 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: s64 (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__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; + 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 : s64 : 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 (issue 0083). 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 (issue 0070). + 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 = .s64 }; + 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` + // (issue 0083). Placeholder `.s64` 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 = .s64 }; + 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 + // (issue 0105). + // + // "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 (issue 0100). The + // shadowed function stays reachable via its qualified name. + if (!self.program_index.fn_ast_map.contains(fd.name)) { + self.program_index.fn_ast_map.put(fd.name, &decl.data.fn_decl) catch {}; + self.program_index.import_flags.put(fd.name, is_imported) catch {}; + } + // Declare extern stub for all functions (bodies lowered + // lazily). 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 :: (s32) -> s32; + 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 :: []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 (issue 0083 — 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) { + // 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(s64)` 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(s64)` / + // `a.Box(s64)`): 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(s64)` 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()`. + 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 :s32: 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 (issue 0070). 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 => .s64, + .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 (issue 0070). + .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 (issue 0070). + for (decls) |decl| { + self.setCurrentSourceFile(decl.source_file); + switch (decl.data) { + .var_decl => self.registerTopLevelGlobal(&decl.data.var_decl), + .const_decl => |cd| self.registerTypedModuleConst(&cd), + else => {}, + } + } +} + +/// Register a typed module-level value constant (`AF_INET :s32: 2`). Run in +/// scanDecls pass 2 (after `resolveForwardIdentifierAliases`) so a forward +/// identifier alias in the annotation (`A :: B; B :: s32; K : A : 42;`) +/// resolves to its target rather than a fabricated empty-struct stub, which +/// would otherwise mistype the constant (issue 0070). +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 + // `.s64`) 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 the issue-0088 + // 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 => {}, + 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]s64` → 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 `.s64` 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, s64)` yields `.widen` and would accept the bogus +/// `B : s64 : 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 : s64 : 4.0`), an int-leaf expression (`K : s64 : M + 2.0` + // → 4), or a float-const-leaf expression whose SUM is integral + // (`F : f64 : 2.5; K : s64 : 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 (issue 0088, + // attempt 2). 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 a top-level mutable global (e.g., `context : Context = ---;`). +/// Run AFTER `resolveForwardIdentifierAliases` so a forward identifier alias +/// in the type annotation (`A :: B; B :: s32; 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 (issue 0070). 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 globals reference a symbol defined in libSystem etc. + // (`_NSConcreteStackBlock : *void #foreign;`). The C symbol + // name is the optional override or the sx name itself. + const sym_name = vd.foreign_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_foreign, + }); + self.putGlobal(self.current_source_file, vd.name, .{ .id = gid, .ty = var_ty }); +} + +/// Serialize a top-level global's initializer into a static `ConstantValue`. +/// Foreign globals (extern 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, issue 0071); 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_foreign) return null; + const v = vd.value orelse return null; + return switch (v.data) { + .undef_literal => .zeroinit, + .null_literal => .null_val, + .int_literal => |il| .{ .int = il.value }, + .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 }, + .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 (issues 0071/0072). + 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 (issues 0071/0072/0080). +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 (issue 0069). Re-resolve to a fixpoint now that every top-level name +/// has been seen, so `A :: B; B :: s32;` converges the same as the ordered +/// `B :: s32; 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 issue 0068 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 :: `s2 ``, target declared later) resolves + // to the nominal `` `s2 `` author, not the builtin `s2` 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 (fix-0102d + // site 4): `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) { + if (isExportedEntryName(cd.name)) { + 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)) { + 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 (fix-0102b). It does NOT run during a default compile: the name path +/// stays the sole resolver, so the suite is byte-for-byte unchanged. fix-0102c +/// 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`). fix-0102a retained 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 (fix-0102c, 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 (fix-0102c F1). + 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, issue 0105). 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 (fix-0102c, R5 §C). `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 (issue 0089): 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 :: `, 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, issue 0105/0107). + 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 issue 0107 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, .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 (issue 0105). 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, .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(s64)` 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 :: () -> s32`, 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 fix-0102b's 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; +} + +/// Declare a function as an extern stub (signature only, no body). +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); + + // 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; + var is_variadic = false; + var effective_params = fd.params; + if (is_foreign 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) .c else .default; + + // For #foreign with C name override, declare under C name and map sx name → C name + if (is_foreign) { + const fe = fd.body.data.foreign_expr; + if (fe.c_name) |c_name| { + const c_name_id = self.module.types.internString(c_name); + 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.foreign_name_map.put(name, c_name) catch {}; + self.fn_decl_fids.put(fd, fid) catch {}; + return; + } + } + + const name_id = self.module.types.internString(name); + 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 (issue 0100). +/// The bare recursion in `scanDecls` still registers intra-module bare calls; +/// this adds the qualified identity the `pkg.fn(...)` resolution paths in +/// `CallResolver.plan` / `lowerCall` already prefer. +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 (issue 0100); registering a qualified alias for them + // would divert that machinery and strand a per-call type binding. + if (fd.type_params.len > 0 or hasComptimeParams(fd) or isPackFn(fd)) return; + // Foreign / builtin / #compiler bodies keep their literal name; a + // qualified alias has no distinct symbol to resolve to. + switch (fd.body.data) { + .foreign_expr, .builtin_expr, .compiler_expr => return, + else => {}, + } + const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ ns_name, short }) catch return; + if (self.program_index.fn_ast_map.contains(qualified)) return; + self.program_index.fn_ast_map.put(qualified, fd) catch {}; + self.program_index.import_flags.put(qualified, true) catch {}; + // Carry the alias's OWN declaring source file (the caller in + // `registerNamespaceQualifiedFns` pins `current_source_file` to the + // decl's source before each call). `lazyLowerFunction`'s null-FuncId + // path restores this so `ns.fn`'s body lowers in its own module's + // visibility context, not the call site's (issue 0100 F1). + if (self.current_source_file) |src| { + self.program_index.qualified_fn_source.put(qualified, src) catch {}; + } + // No eager `declareFunction` here: the extern stub's param/return types + // would be resolved now, before the forward-alias fixpoint, caching an + // `.unresolved` for any type declared later in the module. The qualified + // function is declared + lowered on demand by `lazyLowerFunction`'s + // null-FuncId path (`lowerFunction`), which runs after all types resolve. +} + +/// 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 C-import fn_decls without a library_ref + // are policed; a non-foreign body or a library-bound foreign + // decl is unconditionally visible. + const fd = self.program_index.fn_ast_map.get(name) orelse return true; + if (fd.body.data != .foreign_expr) return true; + if (fd.body.data.foreign_expr.library_ref != 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) { + 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 ([]s32, List__s32, 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` (fix-0102b). + const name_id = self.module.types.internString(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`, issue 0100) 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 issue 0100's param-count assert). Self- +/// contained — the `FnBodyReentry` guard makes the nested lowering +/// transparent to any in-progress caller body (issue 0100 F2) — 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 (issue 0105) 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); + + 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 + func.linkage = if (isExportedEntryName(name)) .external else .internal; + if (fd.call_conv == .c) 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 `.`), + // 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) + if (fd.body.data == .builtin_expr or fd.body.data == .foreign_expr or fd.body.data == .compiler_expr) { + // 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 (issue 0100 F1). + self.builder.currentFunc().source_file = self.current_source_file; + + // Set linkage. Default for fn defs is `internal` (LLVM DCE-friendly, + // matches C `static`). isExportedEntryName lists the names the OS + // loader calls — `main`, Android NativeActivity hooks — which must + // stay externally visible. + if (isExportedEntryName(name)) { + self.builder.currentFunc().linkage = .external; + } + + // Set calling convention + if (fd.call_conv == .c) { + 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(); +} + +// ── Statement lowering ────────────────────────────────────────── + +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 : s64 : 4.0` → 4; `K : s64 : M + 2.0` → 4; and a float-const- + // leaf `KF : s64 : 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 }, .s64); +}