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); }