E4's pack-fn source-pin was incomplete: an imported pack function's
fixed-prefix (non-pack) parameter types were resolved in the CALLER's
module, so a param whose type is bare-visible only in the pack fn's own
module was wrongly rejected with "type 'X' is not visible" — even though
the equivalent plain fn (typed via the source-pinned call-arg path) ran
fine.
Two sites in the pack-mono path re-resolved the fixed-prefix param type
in the caller's context:
- lowerPackFnCall: the call-site arg-typing pass (to contextually type
the arg from its param) — fires first.
- monomorphizePackFn: the body parameter binding, after the caller
source was restored from the signature build.
Both now resolve via resolveParamTypeInSource(fd.body.source_file, &p),
pinning to the pack fn's defining module — matching the already-pinned
signature build, the body lowering, and the cross-module call-arg typing
sites. The call-site arg itself is still lowered AFTER, in the caller's
context (issue 0106).
Regression: examples/0544-packs-imported-pack-fn-fixed-param-source-pin
(main -> lib -> dep; `Needs` two flat hops away, never named in main).
Fails pre-fix with "type 'Needs' is not visible"; passes after. A control
plain fn in the same lib already ran, isolating the pack-mono path.
19195 lines
996 KiB
Zig
19195 lines
996 KiB
Zig
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
const ast = @import("../ast.zig");
|
|
const Node = ast.Node;
|
|
const types = @import("types.zig");
|
|
const inst_mod = @import("inst.zig");
|
|
const mod_mod = @import("module.zig");
|
|
const type_bridge = @import("type_bridge.zig");
|
|
const unescape = @import("../unescape.zig");
|
|
const 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;
|
|
|
|
/// 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_");
|
|
}
|
|
|
|
// ── Scope ───────────────────────────────────────────────────────────────
|
|
|
|
pub const Binding = struct {
|
|
ref: Ref,
|
|
ty: TypeId,
|
|
is_alloca: bool, // true if ref is a pointer that needs load
|
|
is_ref_capture: bool = false, // `for xs: (*x)` — `ref` is `*elem`; auto-deref in value positions
|
|
};
|
|
|
|
// `init` / `deinit` / `put` are pub so collaborator unit tests (e.g.
|
|
// calls.test.zig) can stand up a lexical scope and exercise the
|
|
// scope-dependent call forms (closure / fn-pointer callees) without
|
|
// driving a full function lowering.
|
|
pub const Scope = struct {
|
|
map: std.StringHashMap(Binding),
|
|
fn_names: std.StringHashMap([]const u8), // bare name → mangled name for local functions
|
|
parent: ?*Scope,
|
|
|
|
pub fn init(alloc: Allocator, parent: ?*Scope) Scope {
|
|
return .{
|
|
.map = std.StringHashMap(Binding).init(alloc),
|
|
.fn_names = std.StringHashMap([]const u8).init(alloc),
|
|
.parent = parent,
|
|
};
|
|
}
|
|
|
|
pub fn deinit(self: *Scope) void {
|
|
self.map.deinit();
|
|
self.fn_names.deinit();
|
|
}
|
|
|
|
pub fn put(self: *Scope, name: []const u8, binding: Binding) void {
|
|
self.map.put(name, binding) catch unreachable;
|
|
}
|
|
|
|
pub fn lookup(self: *const Scope, name: []const u8) ?Binding {
|
|
if (self.map.get(name)) |b| return b;
|
|
if (self.parent) |p| return p.lookup(name);
|
|
return null;
|
|
}
|
|
|
|
pub fn lookupFn(self: *const Scope, name: []const u8) ?[]const u8 {
|
|
if (self.fn_names.get(name)) |mangled| return mangled;
|
|
if (self.parent) |p| return p.lookupFn(name);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/// A pending block-scoped cleanup: `defer` (runs on every block exit) or
|
|
/// `onfail` (runs only when an error leaves the block, binding the in-flight
|
|
/// tag). Both share one declaration-ordered stack so error-exit cleanup runs
|
|
/// them interleaved in reverse order (ERR E1.7).
|
|
const CleanupEntry = struct {
|
|
body: *const Node,
|
|
is_onfail: bool,
|
|
binding: ?[]const u8 = null,
|
|
};
|
|
|
|
/// Pure non-transitive visibility walk: `name` is visible from `source` when
|
|
/// it's in `source`'s own scope or in any module reachable over one `graph`
|
|
/// edge. The core of the lowering visibility predicate, exposed so a unit test
|
|
/// can exercise the edge-walk without standing up a whole `Lowering`. Falls open
|
|
/// (true) when `scopes`/`graph` are null (scoping infra unwired).
|
|
pub fn nameVisibleOverEdges(
|
|
scopes: ?*std.StringHashMap(std.StringHashMap(void)),
|
|
graph: ?*std.StringHashMap(std.StringHashMap(void)),
|
|
source: []const u8,
|
|
name: []const u8,
|
|
) bool {
|
|
const sc = scopes orelse return true;
|
|
const own_scope = sc.get(source) orelse return true;
|
|
if (own_scope.contains(name)) return true;
|
|
const g = graph orelse return true;
|
|
const direct = g.get(source) orelse return true;
|
|
var it = direct.iterator();
|
|
while (it.next()) |kv| {
|
|
const dep = sc.get(kv.key_ptr.*) orelse continue;
|
|
if (dep.contains(name)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// ── Lowering ────────────────────────────────────────────────────────────
|
|
|
|
pub const Lowering = struct {
|
|
module: *Module,
|
|
builder: Builder,
|
|
alloc: Allocator,
|
|
scope: ?*Scope = null,
|
|
break_target: ?BlockId = null,
|
|
continue_target: ?BlockId = null,
|
|
block_counter: u32 = 0,
|
|
comptime_counter: u32 = 0,
|
|
main_file: ?[]const u8 = null, // path of the main file; imported functions are declared extern
|
|
resolved_root: ?*const Node = null, // full AST root (for building comptime modules)
|
|
comptime_param_nodes: ?std.StringHashMap(*const Node) = null, // active comptime substitutions
|
|
target_type: ?TypeId = null, // target type for struct/enum literals without explicit names
|
|
lowered_functions: std.StringHashMap(void), // tracks which functions have been fully lowered
|
|
/// Identity map: authoring `*const ast.FnDecl` → the FuncId `declareFunction`
|
|
/// created for it. The name-keyed function table (`resolveFuncByName`) returns
|
|
/// the FIRST author of a name, so two same-name authors collide there; this
|
|
/// map addresses each author's OWN slot by decl identity (fix-0102b), letting
|
|
/// a SHADOWED author lower its body into a distinct FuncId.
|
|
fn_decl_fids: std.AutoHashMap(*const ast.FnDecl, FuncId),
|
|
/// FuncId-keyed lowered tracking — the identity twin of `lowered_functions`
|
|
/// (which keys by name). A shadowed same-name author shares the winner's name
|
|
/// but not its FuncId, so name-keyed tracking can't tell them apart; this
|
|
/// records which specific FuncIds have had a real body lowered (fix-0102b).
|
|
lowered_fids: std.AutoHashMap(FuncId, void),
|
|
local_fn_counter: u32 = 0, // unique counter for mangling local function names
|
|
/// Per-declaration nominal identity bookkeeping (E2). The FIRST source to
|
|
/// register a given top-level type NAME keeps `nominal_id = 0` (structural —
|
|
/// byte-identical to pre-E2 single-author registration); a later registration
|
|
/// of the same name from a DIFFERENT source is a same-name SHADOW and gets a
|
|
/// fresh id from `next_nominal_id`, so the two authors intern to DISTINCT
|
|
/// TypeIds (closing issue 0105's last-wins collapse). `nominal_name_authors`
|
|
/// records each name's first author source to make that decision.
|
|
nominal_name_authors: std.AutoHashMap(types.StringId, []const u8),
|
|
next_nominal_id: u32 = 0,
|
|
/// Declaration-name / import / visibility facts (architecture phase A1,
|
|
/// `ProgramIndex`). Owns `import_flags`; borrows `module_scopes` /
|
|
/// `import_graph` from the compilation driver. Reached via
|
|
/// `self.program_index.<field>`; populated by scan/registration code.
|
|
program_index: ProgramIndex,
|
|
current_source_file: ?[]const u8 = null, // source file of function currently being lowered
|
|
// Implicit Context parameter machinery. When the program imports
|
|
// `std.sx` (and therefore declares `Context :: struct {...}`), every
|
|
// default-conv sx function gains a synthetic `__sx_ctx: *void` param
|
|
// at slot 0, and `current_ctx_ref` is bound to that param on each
|
|
// function-body entry. `lowerCall` / `call_indirect` prepend this ref
|
|
// to the args of every sx-to-sx call. push Context.{...} rebinds it
|
|
// to a stack-allocated Context for the lexical body. See
|
|
// `~/.claude/plans/lets-see-options-for-merry-dijkstra.md`.
|
|
implicit_ctx_enabled: bool = false,
|
|
current_ctx_ref: Ref = Ref.none,
|
|
sel_register_name_fid: ?FuncId = null, // lazily-declared `sel_registerName` extern (non-literal selector fallback)
|
|
jni_env_stack: std.ArrayList(Ref) = std.ArrayList(Ref).empty, // lexical `#jni_env(env)` Ref stack — top is current scope's env for omitted-env `#jni_call`
|
|
jni_env_stack_base: usize = 0, // index above which the currently-lowering fn's `#jni_env` scopes live; outer-fn Refs aren't valid in this fn's instruction stream
|
|
jni_env_tl_get_fid: ?FuncId = null, // extern `sx_jni_env_tl_get` (from library/vendors/sx_jni_runtime/sx_jni_env_tl.c)
|
|
jni_env_tl_set_fid: ?FuncId = null, // extern `sx_jni_env_tl_set`
|
|
needs_jni_env_tl_runtime: bool = false, // set when lowering touches the JNI env TL; signals Compilation to auto-link the runtime .c
|
|
trace_push_fid: ?FuncId = null, // extern `sx_trace_push` (ERR E3.1, from library/vendors/sx_trace_runtime/sx_trace.c)
|
|
trace_clear_fid: ?FuncId = null, // extern `sx_trace_clear`
|
|
needs_trace_runtime: bool = false, // set when lowering emits a trace push/clear; signals Compilation to auto-link sx_trace.c
|
|
chain_fail_target: ?ChainFailTarget = null, // ERR E2.4: when set, a failable `or` chain routes its TOTAL failure here (an absorbing consumer like `catch`) instead of propagating to the function
|
|
current_foreign_class: ?*const ast.ForeignClassDecl = null, // set while lowering a `#jni_main` (or any sx-defined `#jni_class`) bodied method — `super.method(args)` dispatch resolves the parent class against this fcd's `#extends`
|
|
current_foreign_method: ?ast.ForeignMethodDecl = null, // the specific method whose body is being lowered; `super.<same_name>(...)` reuses its signature
|
|
type_bindings: ?std.StringHashMap(TypeId) = null, // generic type param bindings ($T → concrete TypeId)
|
|
current_match_tags: ?[]const u64 = null, // type tags for current match arm (for runtime dispatch)
|
|
force_block_value: bool = false, // set by lowerBlockValue to extract if-else values
|
|
block_terminated: bool = false, // set when constant-folded if emits a return/br into current block
|
|
in_lambda_body: bool = false, // true while lowering a closure-literal body; sharpens the `raise`-not-failable diagnostic (ERR E5.1: tell the user to annotate `-> (T, !)`)
|
|
defer_stack: std.ArrayList(CleanupEntry) = std.ArrayList(CleanupEntry).empty, // block-scoped defer + onfail cleanup stack
|
|
func_defer_base: usize = 0, // defer stack base for current function (lowerReturn drains to this)
|
|
deferred_type_fns: std.ArrayList([]const u8) = std.ArrayList([]const u8).empty, // functions deferred until all types registered
|
|
processing_deferred: bool = false, // true when processing deferred functions (prevents re-deferral)
|
|
/// True while emitting the compiler-synthesized default-Context global
|
|
/// (`emitDefaultContextGlobal`). The built-in allocator infrastructure
|
|
/// (`CAllocator`/`Allocator`/`Context`) is resolved as compiler internals,
|
|
/// independent of the user program's import STYLE (a `std :: #import` puts
|
|
/// `CAllocator` behind a namespace edge from `main`, so the user-visibility
|
|
/// gate would reject it) — so the bare TYPE leaf falls open here (F1).
|
|
emitting_default_context: bool = false,
|
|
/// Names declared as a BLOCK-LOCAL type (a `Foo :: struct/enum/union/error_set`
|
|
/// or bare type-decl statement inside a fn / init body), keyed by the DECLARING
|
|
/// source. A local type registers into the global type table and CLOBBERS a
|
|
/// same-name top-level entry (`registerStructDecl`'s `findByName … orelse intern`
|
|
/// + `updatePreservingKey`), so after it lowers the name IS the local type
|
|
/// program-wide (single-author, pre-E2). The source-aware bare-TYPE gate consults
|
|
/// this so a legitimately block-local type resolves in ITS OWN source (never
|
|
/// mistaken for a namespaced-only leak, even when a namespaced-only import authors
|
|
/// a same-name top-level type — R2). It is keyed by source because a local is
|
|
/// visible ONLY within the source that declares it: an imported template's field
|
|
/// resolution (run in the template's source context, E3 attempt-4) must NOT bind a
|
|
/// name the CALLER declared block-local (E3 attempt-5).
|
|
local_type_names: std.StringHashMap(std.StringHashMap(void)) = std.StringHashMap(std.StringHashMap(void)).init(std.heap.page_allocator),
|
|
struct_defaults_map: std.StringHashMap([]const ?*const Node) = std.StringHashMap([]const ?*const Node).init(std.heap.page_allocator), // struct name → field defaults
|
|
struct_instance_bindings: std.StringHashMap(std.StringHashMap(TypeId)) = std.StringHashMap(std.StringHashMap(TypeId)).init(std.heap.page_allocator), // mangled struct name → type param bindings
|
|
struct_instance_template: std.StringHashMap([]const u8) = std.StringHashMap([]const u8).init(std.heap.page_allocator), // mangled struct name → template name
|
|
comptime_value_bindings: ?std.StringHashMap(i64) = null, // comptime value bindings ($N → integer value)
|
|
protocol_thunk_map: std.StringHashMap([]const FuncId) = std.StringHashMap([]const FuncId).init(std.heap.page_allocator), // "Proto\x00Type" → thunk FuncIds
|
|
protocol_vtable_type_map: std.StringHashMap(TypeId) = std.StringHashMap(TypeId).init(std.heap.page_allocator), // protocol name → vtable struct TypeId
|
|
protocol_vtable_global_map: std.StringHashMap(inst_mod.GlobalId) = std.StringHashMap(inst_mod.GlobalId).init(std.heap.page_allocator), // "Proto\x00Type" → vtable GlobalId
|
|
param_impl_map: std.StringHashMap(std.ArrayList(ParamImplEntry)) = std.StringHashMap(std.ArrayList(ParamImplEntry)).init(std.heap.page_allocator), // "Proto\x00<arg_mangled>\x00<src_mangled>" → impl entries (parameterised protocols only; list lets Phase 4/5 detect cross-module overlap)
|
|
/// Pack-variadic impl entries — separate map keyed by `"Proto\x00<arg_mangled>"`
|
|
/// (NO source suffix) so a single impl `Closure(..$args) -> $R` can be
|
|
/// matched against many concrete source shapes. Concrete impls in
|
|
/// `param_impl_map` win when both match (specificity rule).
|
|
param_impl_pack_map: std.StringHashMap(std.ArrayList(PackParamImplEntry)) = std.StringHashMap(std.ArrayList(PackParamImplEntry)).init(std.heap.page_allocator),
|
|
/// Active pack bindings during monomorphisation. Mirrors `type_bindings`
|
|
/// but for variadic pack names: `args → [T1, T2, ...]`. Read by
|
|
/// `resolveTypeWithBindings` on closure_type_expr to substitute
|
|
/// `Closure(..$args) -> $R` into a concrete closure type.
|
|
pack_bindings: ?std.StringHashMap([]const TypeId) = null,
|
|
/// Active when lowering an inlined comptime-call body. `return X;`
|
|
/// inside the body must NOT emit a `ret` into the caller's LLVM
|
|
/// function — instead it stores X into `.slot` (typed `.ret_ty`)
|
|
/// and sets `block_terminated` so the inliner can load the slot
|
|
/// once the body finishes. Without this, a body like
|
|
/// `{ return 42; }` truncates the caller's basic block mid-flight
|
|
/// and trips LLVM's "Terminator found in the middle of a basic
|
|
/// block" verifier.
|
|
inline_return_target: ?InlineReturnInfo = null,
|
|
/// Active pack-arg-node bindings during a comptime call's body lowering.
|
|
/// Maps the pack-param name (e.g. `args`) to the slice of call-site
|
|
/// argument AST nodes. `lowerIndexExpr` (and `inferExprType`) check
|
|
/// this map when the index expression's base is an identifier matching
|
|
/// a pack name AND the index is a comptime int literal — substitutes
|
|
/// with the i-th call arg's lowered value so the static type tracks
|
|
/// the call arg's real type instead of `Any`. The `[]Any` slice path
|
|
/// remains the runtime-indexed fallback for non-literal indices.
|
|
pack_arg_nodes: ?std.StringHashMap([]const *const Node) = null,
|
|
/// Active pack-arity bindings during a pack-fn mono's body lowering.
|
|
/// Maps the pack-param name (e.g. `args`) to N. `lowerFieldAccess`
|
|
/// uses this to resolve `args.len` to a compile-time constant Ref
|
|
/// when no `args` slice is in scope (the mono path doesn't
|
|
/// materialise the slice).
|
|
pack_param_count: ?std.StringHashMap(u32) = null,
|
|
/// Type-only pack binding consulted by `inferExprType` for
|
|
/// `args[<lit>]` (parallel to `pack_arg_nodes` which carries the
|
|
/// AST substitution used at lowering time). Holds the concrete
|
|
/// call-site arg types in declaration order — same data the
|
|
/// mono's pack-param signature uses. Lets generic-`$R` return
|
|
/// inference resolve `args[i]` to the correct concrete type even
|
|
/// before the mono's scope is set up.
|
|
pack_arg_types: ?std.StringHashMap([]const TypeId) = null,
|
|
/// Active during a protocol-pack mono's body lowering: pack-param name →
|
|
/// constraint protocol name (`..xs: Box` ⇒ `xs` → `"Box"`). Lets
|
|
/// `lowerFieldAccess` enforce the interface-only rule — a member access
|
|
/// `xs[i].<m>` is rejected unless `<m>` is one of the protocol's methods.
|
|
/// Null / absent for the comptime `..$args` pack (no constraint).
|
|
pack_constraint: ?std.StringHashMap([]const u8) = null,
|
|
struct_const_map: std.StringHashMap(StructConstInfo) = std.StringHashMap(StructConstInfo).init(std.heap.page_allocator), // "Struct.CONST" → value info
|
|
foreign_name_map: std.StringHashMap([]const u8) = std.StringHashMap([]const u8).init(std.heap.page_allocator), // sx name → C name for #foreign renames
|
|
target_config: ?@import("../target.zig").TargetConfig = null, // compilation target (for inline if)
|
|
comptime_constants: std.StringHashMap(ComptimeValue) = std.StringHashMap(ComptimeValue).init(std.heap.page_allocator), // compile-time known constants (e.g. OS, ARCH)
|
|
diagnostics: ?*errors.DiagnosticList = null, // error reporting with source locations
|
|
xx_reentrancy: std.AutoHashMap(u64, void) = std.AutoHashMap(u64, void).init(std.heap.page_allocator), // (src_ty, dst_ty) pairs currently being resolved through user-space Into; prevents infinite monomorphisation when a convert body re-enters the same xx
|
|
/// Whole-program-converged inferred error sets (ERR E1.4b): top-level
|
|
/// bare-`!` function name → its sorted escape-tag ids (literal raises +
|
|
/// pure-failable `try` edges, fix-pointed across the call graph). The
|
|
/// shared `!` placeholder TypeId stays empty; this side map holds the real
|
|
/// per-function sets (sidesteps the name-only error-set interning). Read by
|
|
/// `lowerTry`'s named-caller widening and the empty-inferred warning.
|
|
inferred_error_sets: std.StringHashMap([]const u32) = std.StringHashMap([]const u32).init(std.heap.page_allocator),
|
|
/// Whole-program-converged inferred error sets keyed by closure/function
|
|
/// VALUE-signature shape (ERR E5.1 sub-feature 2): every occurrence of
|
|
/// `Closure(<sig>) -> (T, !)` with a structurally identical value-signature
|
|
/// shares one node; each bare-`!` closure literal of that shape unions its
|
|
/// escape tags in. Read by `checkEscapeWidening` when a `try` operand is a
|
|
/// closure/fn-type SLOT call (no static fn name). Key = `closureShapeKey`.
|
|
shape_inferred_sets: std.StringHashMap([]const u32) = std.StringHashMap([]const u32).init(std.heap.page_allocator),
|
|
|
|
pub const ComptimeValue = union(enum) {
|
|
int_val: i64,
|
|
enum_tag: struct { ty: TypeId, tag: u32 },
|
|
};
|
|
|
|
const StructConstInfo = struct {
|
|
value: *const Node,
|
|
ty: ?TypeId, // null if no type annotation (inferred)
|
|
};
|
|
|
|
/// One impl block for a parameterised protocol (e.g. `impl Into(Block) for Closure() -> void`).
|
|
/// Stored in `param_impl_map` keyed by (protocol_name, target_args_mangled, source_mangled).
|
|
/// `defining_module` enables import-scoped visibility + cross-module duplicate diagnostics.
|
|
pub const ParamImplEntry = struct {
|
|
methods: []const *const ast.FnDecl,
|
|
source_ty: TypeId,
|
|
target_args: []const TypeId,
|
|
defining_module: []const u8,
|
|
span: ast.Span,
|
|
};
|
|
|
|
const InlineReturnInfo = struct { slot: Ref, ret_ty: TypeId, done_bb: BlockId };
|
|
|
|
/// ERR E2.4 — where a failable `or` chain's TOTAL failure routes when the
|
|
/// chain is the operand of an absorbing consumer (`catch`). `bb` is a block
|
|
/// with a single parameter typed `set` (the error tag); the chain branches
|
|
/// there with its final error instead of propagating to the function.
|
|
const ChainFailTarget = struct { bb: BlockId, set: TypeId };
|
|
|
|
/// Pack-variadic impl entry — `impl Proto(Args...) for Closure(Prefix..., ..$pack) -> $ret`.
|
|
/// Matches any concrete closure source whose first `prefix_len` param types
|
|
/// equal `source_pack_ty`'s fixed prefix; the tail binds to `pack_var_name`
|
|
/// (e.g. "args") and the source's return type binds to `ret_var_name`
|
|
/// (e.g. "R") when the impl's return is generic. `ret_var_name == null`
|
|
/// means the return type is concrete and must match exactly.
|
|
pub const PackParamImplEntry = struct {
|
|
methods: []const *const ast.FnDecl,
|
|
source_pack_ty: TypeId,
|
|
target_args: []const TypeId,
|
|
defining_module: []const u8,
|
|
span: ast.Span,
|
|
pack_var_name: []const u8,
|
|
ret_var_name: ?[]const u8,
|
|
};
|
|
|
|
/// Caller-state protection for lowering a function body re-entrantly — a
|
|
/// lazily lowered callee, a qualified `ns.fn` alias, or an out-of-line
|
|
/// same-name author. `enter` snapshots the in-progress builder / scope /
|
|
/// flag / pack / jni state and installs a fresh set for the nested body;
|
|
/// `restore` puts the caller's state back. Lowering a callee must be
|
|
/// transparent to the caller's own lowering — notably `block_terminated`,
|
|
/// which leaking back would mark the caller's trailing statements
|
|
/// dead-after-terminator (issue 0100 F2).
|
|
const FnBodyReentry = struct {
|
|
l: *Lowering,
|
|
func: ?FuncId,
|
|
block: ?BlockId,
|
|
counter: u32,
|
|
scope: ?*Scope,
|
|
defer_base: usize,
|
|
block_terminated: bool,
|
|
force_block_value: bool,
|
|
source_file: ?[]const u8,
|
|
jni_env_base: usize,
|
|
pack_arg_nodes: ?std.StringHashMap([]const *const Node),
|
|
pack_param_count: ?std.StringHashMap(u32),
|
|
pack_arg_types: ?std.StringHashMap([]const TypeId),
|
|
inline_return_target: ?InlineReturnInfo,
|
|
|
|
fn enter(l: *Lowering) FnBodyReentry {
|
|
const g = FnBodyReentry{
|
|
.l = l,
|
|
.func = l.builder.func,
|
|
.block = l.builder.current_block,
|
|
.counter = l.builder.inst_counter,
|
|
.scope = l.scope,
|
|
.defer_base = l.func_defer_base,
|
|
.block_terminated = l.block_terminated,
|
|
.force_block_value = l.force_block_value,
|
|
.source_file = l.current_source_file,
|
|
.jni_env_base = l.jni_env_stack_base,
|
|
.pack_arg_nodes = l.pack_arg_nodes,
|
|
.pack_param_count = l.pack_param_count,
|
|
.pack_arg_types = l.pack_arg_types,
|
|
.inline_return_target = l.inline_return_target,
|
|
};
|
|
// The `#jni_env` Ref stack is lexical to ONE function's instruction
|
|
// stream; move the visible base to the current top. Pack-fn mono
|
|
// state is likewise lexical to the pack-fn body — null it so a
|
|
// callee sharing a param NAME with the active pack doesn't fold the
|
|
// outer mono's arity into its own `<name>.len`.
|
|
l.jni_env_stack_base = l.jni_env_stack.items.len;
|
|
l.pack_arg_nodes = null;
|
|
l.pack_param_count = null;
|
|
l.pack_arg_types = null;
|
|
l.inline_return_target = null;
|
|
l.func_defer_base = l.defer_stack.items.len;
|
|
l.block_terminated = false;
|
|
l.force_block_value = false;
|
|
return g;
|
|
}
|
|
|
|
fn restore(g: FnBodyReentry) void {
|
|
const l = g.l;
|
|
l.setCurrentSourceFile(g.source_file);
|
|
l.scope = g.scope;
|
|
l.func_defer_base = g.defer_base;
|
|
l.block_terminated = g.block_terminated;
|
|
l.force_block_value = g.force_block_value;
|
|
l.builder.func = g.func;
|
|
l.builder.current_block = g.block;
|
|
l.builder.inst_counter = g.counter;
|
|
l.jni_env_stack_base = g.jni_env_base;
|
|
l.pack_arg_nodes = g.pack_arg_nodes;
|
|
l.pack_param_count = g.pack_param_count;
|
|
l.pack_arg_types = g.pack_arg_types;
|
|
l.inline_return_target = g.inline_return_target;
|
|
}
|
|
};
|
|
|
|
pub fn init(module: *Module) Lowering {
|
|
return .{
|
|
.module = module,
|
|
.builder = Builder.init(module),
|
|
.alloc = module.alloc,
|
|
.lowered_functions = std.StringHashMap(void).init(module.alloc),
|
|
.fn_decl_fids = std.AutoHashMap(*const ast.FnDecl, FuncId).init(module.alloc),
|
|
.lowered_fids = std.AutoHashMap(FuncId, void).init(module.alloc),
|
|
.nominal_name_authors = std.AutoHashMap(types.StringId, []const u8).init(module.alloc),
|
|
.program_index = ProgramIndex.init(module.alloc),
|
|
};
|
|
}
|
|
|
|
// ── Public entry point ──────────────────────────────────────────
|
|
|
|
/// Lower all top-level declarations from a root node.
|
|
/// Pass 1: Scan all declarations (register ASTs, types, extern stubs).
|
|
/// Pass 2: Lower only `main` (everything else is lowered lazily on demand).
|
|
pub fn lowerRoot(self: *Lowering, root: *const Node) void {
|
|
const decls = switch (root.data) {
|
|
.root => |r| r.decls,
|
|
else => return,
|
|
};
|
|
// Pass 0: pre-scan for `Context :: struct {...}`. If the program
|
|
// imports `std.sx` it has Context, and every default-conv sx
|
|
// function gets the implicit `__sx_ctx` param. Otherwise the
|
|
// implicit-ctx machinery stays fully disabled — programs that
|
|
// call only libc directly keep their bare C ABI.
|
|
self.implicit_ctx_enabled = detectContextDecl(decls);
|
|
self.module.has_implicit_ctx = self.implicit_ctx_enabled;
|
|
// Pass 1: scan — register all function ASTs, struct types, extern stubs
|
|
self.scanDecls(decls);
|
|
// Pass 1b: inject compile-time constants (OS, ARCH, POINTER_SIZE) from target config
|
|
self.injectComptimeConstants();
|
|
// Pass 1c: emit the process-wide default Context global, statically
|
|
// initialised to a CAllocator-backed Allocator value. Used by FFI
|
|
// wrappers in Step 4 and by the interp's `callWithDefaultContext`
|
|
// entry. Only fires when the program imports `std.sx` (so Context +
|
|
// Allocator + CAllocator are all registered).
|
|
self.emitDefaultContextGlobal();
|
|
// Pass 1d: converge inferred (`bare !`) error sets across the whole
|
|
// program (ERR E1.4b). Runs before body lowering so `lowerTry`'s
|
|
// named-caller widening sees each bare-`!` callee's converged set; also
|
|
// emits the empty-inferred warning.
|
|
self.convergeInferredErrorSets();
|
|
// Pass 1d': converge inferred (`bare !`) error sets per closure/fn-type
|
|
// SHAPE (ERR E5.1 sub-feature 2). Runs after the name-keyed pass so a
|
|
// closure's `try named_fn()` edge resolves against the converged
|
|
// top-level sets; before body lowering so `try slot(x)` widening sees
|
|
// the full per-shape union.
|
|
self.convergeClosureShapeSets();
|
|
// Pass 1e: error-flow checks (ERR E1.8 value-slot liveness + E1.7
|
|
// cleanup-body absorption) over the main file's functions. Runs after
|
|
// the error-set convergence passes (so failable callees resolve) and
|
|
// before body lowering — purely a diagnostic pass; `core.zig` halts on
|
|
// any error before codegen.
|
|
self.errorFlow().checkErrorFlow(decls);
|
|
// Pass 1f: reject identifiers used in a type position that name no
|
|
// declared type / primitive / in-scope generic param (issue 0064).
|
|
// Runs after scanning (so every real type name is registered) and
|
|
// before body lowering, so the diagnostic halts via `core.zig`
|
|
// `hasErrors()` before the empty-struct stub can reach codegen. Owned by
|
|
// `semantic_diagnostics.UnknownTypeChecker` (A2.4); built only when
|
|
// diagnostics are active, querying ProgramIndex + TypeResolver.
|
|
if (self.diagnostics) |diags| {
|
|
const checker = semantic_diagnostics.UnknownTypeChecker{
|
|
.alloc = self.alloc,
|
|
.diagnostics = diags,
|
|
.types = &self.module.types,
|
|
.index = &self.program_index,
|
|
.main_file = self.main_file,
|
|
};
|
|
checker.run(decls);
|
|
}
|
|
// Pass 2: lower main (and comptime side-effects)
|
|
self.lowerMainAndComptime(decls);
|
|
// Pass 3: lower deferred functions (any_to_string etc.) now that all types are registered
|
|
self.lowerDeferredTypeFns();
|
|
// Pass 4: target-specific entry-point sanity checks
|
|
self.checkRequiredEntryPoints();
|
|
// Pass 4a: validate main's signature (ERR E4.2 entry-point gate).
|
|
self.validateMainSignature();
|
|
// Pass 4b: eagerly lower bodied methods on sx-defined `#objc_class`
|
|
// declarations. The Obj-C runtime calls these via IMP pointers
|
|
// registered in M1.2 A.4 — no sx-side call path drives lazy
|
|
// lowering, so we trigger it here. Mirrors the JNI eager-lower
|
|
// pattern in Pass 5.
|
|
self.lowerObjcDefinedClassMethods();
|
|
// Pass 5: synthesize JNI-mangled exports for `#jni_main` bodied methods.
|
|
// Android's JNI runtime resolves `private native sx_<m>(...)` declared in
|
|
// the bundled classes.dex by looking up the symbol
|
|
// `Java_<pkg-mangled>_<Class>_sx_1<m-mangled>` in the loaded .so. Each
|
|
// bodied method on a `#jni_main #jni_class` decl becomes an exported
|
|
// C-ABI fn with that name; the JNIEnv* / jobject params are prepended,
|
|
// then the user-declared params (with type-erased pointers since JNI
|
|
// doesn't carry sx-side types across the binding).
|
|
self.synthesizeJniMainStubs();
|
|
}
|
|
|
|
/// ERR E4.2: the entry-point signature gate. `main` must take no parameters
|
|
/// and have a SINGLE-slot return: void (`()` / `-> ()` / `-> void`), an
|
|
/// integer (POSIX exit code, truncated to u8), or `-> !` / `-> !Named` (the
|
|
/// error tag rides the single return register). The multi-slot
|
|
/// `-> (T, !)` tuple return is NOT yet supported — the JIT calls main as
|
|
/// `() -> i32`, so a 2-slot `{value, error}` return ABI-mismatches and
|
|
/// segfaults; that shape lands with the E4.2 entry-point wrapper. Any other
|
|
/// shape (`-> string`, `-> f64`, a non-failable tuple, …) is a clean
|
|
/// diagnostic rather than a silent miscompile.
|
|
fn validateMainSignature(self: *Lowering) void {
|
|
const fd = self.program_index.fn_ast_map.get("main") orelse return;
|
|
|
|
if (fd.params.len != 0) {
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, fd.params[0].name_span, "main: parameters must be empty; return type must be void, an integer, or `!`", .{});
|
|
}
|
|
return;
|
|
}
|
|
|
|
const rt = self.resolveReturnType(fd);
|
|
// Single-slot returns the JIT's `() -> i32` ABI handles directly:
|
|
// void / integer, and a pure failable `-> !` (a bare u32 error tag).
|
|
if (rt == .void or self.isIntEx(rt)) return;
|
|
if (self.errorChannelOf(rt)) |chan| {
|
|
if (rt == chan) {
|
|
// pure `-> !` / `-> !Named`. The emitted entry-point wrapper
|
|
// (emit_llvm `emitFailableMainRet`) calls `sx_trace_report_unhandled`
|
|
// on an escaping error, so the AOT path must auto-link the trace
|
|
// runtime even when the body emits no other push/clear.
|
|
self.needs_trace_runtime = true;
|
|
return;
|
|
}
|
|
// `-> (T, !)` — value-carrying failable. Accepted only for a single
|
|
// **integer** value slot (`{int, error_set}`): the wrapper extracts
|
|
// the value + tag from the returned tuple, exits `value as u8` on
|
|
// success / reports + exits 1 on error. Multi-value `-> (T1, T2, !)`
|
|
// or a non-integer value slot stays rejected — there's no single
|
|
// integer exit code to map it to.
|
|
const ti = self.module.types.get(rt);
|
|
if (ti == .tuple and ti.tuple.fields.len == 2 and self.isIntEx(ti.tuple.fields[0])) {
|
|
self.needs_trace_runtime = true;
|
|
return;
|
|
}
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, if (fd.return_type) |rtn| rtn.span else null, "a value-carrying failable `main` must be `-> (int, !)` (one integer value slot); got '{s}'. Use `-> !` (no value), `-> (int, !)`, or a non-failable integer return", .{self.formatTypeName(rt)});
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, if (fd.return_type) |rtn| rtn.span else null, "main: return type must be void, an integer, or `!`; got '{s}'", .{self.formatTypeName(rt)});
|
|
}
|
|
}
|
|
|
|
// ERR E1.7 / E1.8 — path-sensitive error-flow diagnostics (Pass 1e) live in
|
|
// `error_flow.zig` (`ErrorFlow`, a `*Lowering` facade). `lowerRoot` calls
|
|
// `self.errorFlow().checkErrorFlow(decls)`.
|
|
|
|
/// On Android, the OS loads the .so via a Java-side Activity declared
|
|
/// with `#jni_main #jni_class("...")`. The Java class drives the
|
|
/// lifecycle (onCreate / onPause / etc.) and sx provides the native
|
|
/// delegates bound via JNI name mangling. Without a `#jni_main` decl
|
|
/// there's no entry point — the .so would load but Android has nothing
|
|
/// to call into.
|
|
fn checkRequiredEntryPoints(self: *Lowering) void {
|
|
const tc = self.target_config orelse return;
|
|
if (!tc.isAndroid()) return;
|
|
|
|
var it = self.program_index.foreign_class_map.iterator();
|
|
while (it.next()) |entry| {
|
|
const fcd = entry.value_ptr.*;
|
|
if (fcd.is_main and !fcd.is_foreign and fcd.runtime == .jni_class) return;
|
|
}
|
|
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, null,
|
|
"target is Android but no `#jni_main` Activity declared. " ++
|
|
"The OS launches a Java-side Activity that delegates lifecycle " ++
|
|
"callbacks into sx — declare one like:\n\n" ++
|
|
" Bundle :: #foreign #jni_class(\"android/os/Bundle\") {{ }}\n\n" ++
|
|
" MyApp :: #jni_main #jni_class(\"co/example/MyApp\") {{\n" ++
|
|
" onCreate :: (self: *Self, b: *Bundle) {{ /* ... */ }}\n" ++
|
|
" }}", .{});
|
|
}
|
|
}
|
|
|
|
/// Inject compile-time constants from target_config into comptime_constants.
|
|
/// Called after scanDecls so that enum types (OperatingSystem, Architecture) are registered.
|
|
fn injectComptimeConstants(self: *Lowering) void {
|
|
const tc = self.target_config orelse return;
|
|
|
|
// OS: OperatingSystem enum { macos; linux; windows; wasm; unknown; }
|
|
const os_name_id = self.module.types.internString("OperatingSystem");
|
|
if (self.module.types.findByName(os_name_id)) |os_ty| {
|
|
const os_info = self.module.types.get(os_ty);
|
|
if (os_info == .@"enum") {
|
|
const tag: u32 = if (tc.isWasm())
|
|
self.findVariantIndex(os_info.@"enum".variants, "wasm")
|
|
else if (tc.isWindows())
|
|
self.findVariantIndex(os_info.@"enum".variants, "windows")
|
|
else if (tc.isAndroid())
|
|
self.findVariantIndex(os_info.@"enum".variants, "android")
|
|
else if (tc.isLinux())
|
|
self.findVariantIndex(os_info.@"enum".variants, "linux")
|
|
else if (tc.isIOS())
|
|
self.findVariantIndex(os_info.@"enum".variants, "ios")
|
|
else if (tc.isMacOS())
|
|
self.findVariantIndex(os_info.@"enum".variants, "macos")
|
|
else
|
|
self.findVariantIndex(os_info.@"enum".variants, "unknown");
|
|
self.comptime_constants.put("OS", .{ .enum_tag = .{ .ty = os_ty, .tag = tag } }) catch {};
|
|
}
|
|
}
|
|
|
|
// ARCH: Architecture enum { aarch64; x86_64; wasm32; wasm64; unknown; }
|
|
const arch_name_id = self.module.types.internString("Architecture");
|
|
if (self.module.types.findByName(arch_name_id)) |arch_ty| {
|
|
const arch_info = self.module.types.get(arch_ty);
|
|
if (arch_info == .@"enum") {
|
|
const tag: u32 = if (tc.isWasm32())
|
|
self.findVariantIndex(arch_info.@"enum".variants, "wasm32")
|
|
else if (tc.isWasm64())
|
|
self.findVariantIndex(arch_info.@"enum".variants, "wasm64")
|
|
else if (tc.isAarch64())
|
|
self.findVariantIndex(arch_info.@"enum".variants, "aarch64")
|
|
else if (tc.isX86_64())
|
|
self.findVariantIndex(arch_info.@"enum".variants, "x86_64")
|
|
else
|
|
self.findVariantIndex(arch_info.@"enum".variants, "unknown");
|
|
self.comptime_constants.put("ARCH", .{ .enum_tag = .{ .ty = arch_ty, .tag = tag } }) catch {};
|
|
}
|
|
}
|
|
|
|
// POINTER_SIZE: s64 (4 for wasm32, 8 for wasm64 and other 64-bit targets)
|
|
const ptr_size: i64 = if (tc.isWasm32()) 4 else 8;
|
|
self.comptime_constants.put("POINTER_SIZE", .{ .int_val = ptr_size }) catch {};
|
|
}
|
|
|
|
fn findVariantIndex(self: *Lowering, variants: []const types.StringId, name: []const u8) u32 {
|
|
const name_id = self.module.types.internString(name);
|
|
for (variants, 0..) |v, i| {
|
|
if (v == name_id) return @intCast(i);
|
|
}
|
|
return 0; // fallback to first variant
|
|
}
|
|
|
|
/// Lower functions that were deferred because they use type-category matching.
|
|
/// At this point, main is fully lowered and all types are in the TypeTable.
|
|
fn lowerDeferredTypeFns(self: *Lowering) void {
|
|
if (self.deferred_type_fns.items.len == 0) return;
|
|
self.processing_deferred = true;
|
|
for (self.deferred_type_fns.items) |name| {
|
|
self.lazyLowerFunction(name);
|
|
}
|
|
self.processing_deferred = false;
|
|
}
|
|
|
|
/// Lower a list of top-level declarations (used by irComptimeEval — non-lazy path).
|
|
/// This preserves the old behavior for comptime evaluation contexts.
|
|
pub fn lowerDecls(self: *Lowering, decls: []const *const Node) void {
|
|
for (decls) |decl| {
|
|
self.setCurrentSourceFile(decl.source_file);
|
|
const is_imported = if (self.main_file) |mf|
|
|
(if (decl.source_file) |sf| !std.mem.eql(u8, sf, mf) else false)
|
|
else
|
|
false;
|
|
switch (decl.data) {
|
|
.fn_decl => |fd| {
|
|
self.program_index.fn_ast_map.put(fd.name, &decl.data.fn_decl) catch {};
|
|
self.lowerFunction(&fd, fd.name, is_imported);
|
|
},
|
|
.const_decl => |cd| {
|
|
if (cd.value.data == .fn_decl) {
|
|
self.program_index.fn_ast_map.put(cd.name, &cd.value.data.fn_decl) catch {};
|
|
self.lowerFunction(&cd.value.data.fn_decl, cd.name, is_imported);
|
|
} else if (cd.value.data == .struct_decl) {
|
|
self.registerStructDecl(&cd.value.data.struct_decl, decl.source_file);
|
|
} else if (cd.value.data == .enum_decl) {
|
|
_ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
|
} else if (cd.value.data == .union_decl) {
|
|
_ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
|
} 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 => {
|
|
_ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
|
},
|
|
.union_decl => {
|
|
_ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
|
},
|
|
.error_set_decl => {
|
|
self.registerErrorSetDecl(decl);
|
|
},
|
|
.protocol_decl => {
|
|
self.registerProtocolDecl(&decl.data.protocol_decl);
|
|
},
|
|
.impl_block => {
|
|
self.protocolResolver().registerImplBlock(&decl.data.impl_block, is_imported, decl);
|
|
},
|
|
.foreign_class_decl => {
|
|
self.registerForeignClassDecl(&decl.data.foreign_class_decl);
|
|
},
|
|
.namespace_decl => |ns| {
|
|
self.registerNamespacedForeignClasses(ns);
|
|
if (self.main_file != null) {
|
|
self.registerNamespaceQualifiedFns(ns.name, ns.own_decls);
|
|
self.lowerDecls(ns.decls);
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Detect whether `Context :: struct {...}` is declared anywhere in the
|
|
/// program. Used to gate the implicit `__sx_ctx` param machinery: when
|
|
/// `std.sx` is in the dep graph, `Context` is declared and every sx
|
|
/// function gets the implicit param. Otherwise the program runs with a
|
|
/// bare C ABI (no global Context, no implicit param, no FFI wrappers).
|
|
fn detectContextDecl(decls: []const *const Node) bool {
|
|
for (decls) |decl| {
|
|
const found = switch (decl.data) {
|
|
.struct_decl => |sd| std.mem.eql(u8, sd.name, "Context"),
|
|
.const_decl => |cd|
|
|
std.mem.eql(u8, cd.name, "Context") and cd.value.data == .struct_decl,
|
|
.namespace_decl => |ns| detectContextDecl(ns.decls),
|
|
else => false,
|
|
};
|
|
if (found) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Returns true if a sx function declaration should receive the
|
|
/// implicit `__sx_ctx` parameter. False for foreign-libc bindings,
|
|
/// #builtin / #compiler bodies, and C-conv functions (which keep
|
|
/// their literal C ABI). Also false for OS-called entry points
|
|
/// (`isExportedEntryName`): main and JNI hooks are invoked by the
|
|
/// dyld / JVM with no `__sx_ctx` arg, so the visible signature must
|
|
/// not include one. Their bodies are still sx code — they
|
|
/// synthesise `&__sx_default_context` at entry and use it as their
|
|
/// own `current_ctx_ref`. Full FFI-wrapper split (a separate
|
|
/// `__sx_<name>_impl` with the ctx param) lands in Step 4 proper.
|
|
fn funcWantsImplicitCtx(self: *const Lowering, fd: *const ast.FnDecl) bool {
|
|
if (!self.implicit_ctx_enabled) return false;
|
|
if (fd.call_conv == .c) return false;
|
|
return switch (fd.body.data) {
|
|
.foreign_expr, .builtin_expr, .compiler_expr => false,
|
|
else => !isExportedEntryName(fd.name),
|
|
};
|
|
}
|
|
|
|
/// Returns true if a fn-pointer of the given type carries an implicit
|
|
/// `__sx_ctx` at LLVM slot 0. Default-conv sx fn-pointers do; C-conv
|
|
/// (and any non-function type) does not.
|
|
fn fnPtrTypeWantsCtx(self: *const Lowering, ty: TypeId) bool {
|
|
if (!self.implicit_ctx_enabled) return false;
|
|
if (ty.isBuiltin()) return false;
|
|
const ti = self.module.types.get(ty);
|
|
if (ti != .function) return false;
|
|
return ti.function.call_conv != .c;
|
|
}
|
|
|
|
// ── Unified declaration-fact writers (R5 §#4) ──
|
|
// The SOLE writers of the three semantic maps — global
|
|
// `type_alias_map` / `module_const_map` / `global_names` AND their
|
|
// source-partitioned analogues (`*_by_source`). Invariant: the global and
|
|
// by-source write for a name are inseparable — a write-site that mirrors
|
|
// one without the other lets a ns-only author miss `*_by_source` and leak
|
|
// past the source-aware bare-TYPE gate. No raw `.put`/`.remove` to the
|
|
// three maps exists outside these helpers (grep-checkable — mirrors the
|
|
// no-raw-`TypeTable.update` discipline). The global map stays the only
|
|
// READER for now; the per-source cache feeds the gate. A null source
|
|
// (unreachable for a scanned top-level decl post-import-resolution) falls
|
|
// back to the main file; if even that is absent only the by-source write is
|
|
// skipped — the global map is always written.
|
|
fn putTypeAlias(self: *Lowering, source: ?[]const u8, name: []const u8, tid: TypeId) void {
|
|
self.program_index.type_alias_map.put(name, tid) catch {};
|
|
if (source orelse self.main_file) |src| self.program_index.putTypeAliasBySource(src, name, tid);
|
|
}
|
|
fn putModuleConst(self: *Lowering, source: ?[]const u8, name: []const u8, info: program_index_mod.ModuleConstInfo) void {
|
|
self.program_index.module_const_map.put(name, info) catch {};
|
|
if (source orelse self.main_file) |src| self.program_index.putModuleConstBySource(src, name, info);
|
|
}
|
|
fn putGlobal(self: *Lowering, source: ?[]const u8, name: []const u8, info: program_index_mod.GlobalInfo) void {
|
|
self.program_index.global_names.put(name, info) catch {};
|
|
if (source orelse self.main_file) |src| self.program_index.putGlobalBySource(src, name, info);
|
|
}
|
|
fn dropModuleConst(self: *Lowering, source: ?[]const u8, name: []const u8) void {
|
|
_ = self.program_index.module_const_map.remove(name);
|
|
if (source orelse self.main_file) |src| self.program_index.removeModuleConstBySource(src, name);
|
|
}
|
|
|
|
/// Pass 1: Scan declarations — register ASTs and extern stubs, but don't lower bodies.
|
|
fn scanDecls(self: *Lowering, decls: []const *const Node) void {
|
|
// Pass 0: register every numeric-literal module const (`N :: 16` and the
|
|
// typed `N : s64 : 16`, plus float-valued `N :: 4.0` / `N : f64 : 4.0`)
|
|
// BEFORE any type alias is resolved below. A type alias whose dimension is
|
|
// a named const (`Arr :: [N]T`) resolves its dimension eagerly here, on
|
|
// the stateless registration path; that path can only read
|
|
// `module_const_map`. Untyped consts would otherwise be registered only in
|
|
// declaration order (pass 1) and typed ones only after the alias fixpoint
|
|
// (pass 2) — so an alias declared before its const, or any alias over a
|
|
// typed const, saw an empty table and miscompiled the dimension to length
|
|
// 0 (issue 0083). A float-valued const resolves to a dimension only when
|
|
// its value is integral (`floatToIntExact`); pre-registering it keeps the
|
|
// forward-alias float path identical to the int path. The dimension only
|
|
// needs the value, so a placeholder type is fine; pass 2 overwrites typed
|
|
// consts with the resolved annotation type (issue 0070).
|
|
for (decls) |decl| {
|
|
if (decl.data != .const_decl) continue;
|
|
const cd = decl.data.const_decl;
|
|
switch (cd.value.data) {
|
|
.int_literal => {
|
|
const info = program_index_mod.ModuleConstInfo{ .value = cd.value, .ty = .s64 };
|
|
self.putModuleConst(decl.source_file, cd.name, info);
|
|
},
|
|
.float_literal => {
|
|
const info = program_index_mod.ModuleConstInfo{ .value = cd.value, .ty = .f64 };
|
|
self.putModuleConst(decl.source_file, cd.name, info);
|
|
},
|
|
// A const whose RHS is an integer EXPRESSION over other consts
|
|
// (`M :: 2; N :: M + 1`) is itself a usable count: register it so
|
|
// `moduleConstInt` can fold the RHS through `evalConstIntExpr`
|
|
// (issue 0083). Placeholder `.s64` type — the count consumers read
|
|
// only the value; if the expression doesn't fold (references a
|
|
// non-const), `moduleConstInt` yields null and the use diagnoses.
|
|
.binary_op, .unary_op => {
|
|
const info = program_index_mod.ModuleConstInfo{ .value = cd.value, .ty = .s64 };
|
|
self.putModuleConst(decl.source_file, cd.name, info);
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
// Pass 0b: reserve every GENUINE same-name STRUCT shadow's DISTINCT nominal
|
|
// slot BEFORE the registration loop resolves any fields (E2/F1). A field
|
|
// 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 struct decls in THIS scan author the name (so it
|
|
// needs ≥2 distinct nominal TypeIds). 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.
|
|
var shadow_first = std.AutoHashMap(types.StringId, *const anyopaque).init(self.alloc);
|
|
defer shadow_first.deinit();
|
|
var genuine_shadows = std.AutoHashMap(types.StringId, void).init(self.alloc);
|
|
defer genuine_shadows.deinit();
|
|
for (decls) |decl| {
|
|
const sd = topLevelStructDecl(decl) orelse continue;
|
|
if (sd.type_params.len > 0) continue;
|
|
const nm = self.module.types.internString(sd.name);
|
|
const key: *const anyopaque = @ptrCast(sd);
|
|
const gop = shadow_first.getOrPut(nm) catch continue;
|
|
if (gop.found_existing) {
|
|
if (gop.value_ptr.* != key) genuine_shadows.put(nm, {}) catch {};
|
|
} else gop.value_ptr.* = key;
|
|
}
|
|
for (decls) |decl| {
|
|
const sd = topLevelStructDecl(decl) orelse continue;
|
|
const nm = self.module.types.internString(sd.name);
|
|
if (!genuine_shadows.contains(nm)) continue;
|
|
self.setCurrentSourceFile(decl.source_file);
|
|
self.reserveShadowStructSlot(sd);
|
|
}
|
|
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
|
|
// `module_fns` — 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) {
|
|
// Register enum/tagged-union types in the type table
|
|
_ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
|
} else if (cd.value.data == .union_decl) {
|
|
// Register plain union types in the type table
|
|
_ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
|
} 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 :: [<computed>]T`, whose
|
|
// dimension is not a compile-time integer constant. Surface
|
|
// it as a clean diagnostic so the build aborts here rather
|
|
// than letting `.unresolved` reach codegen and `@panic` in
|
|
// sizeOf (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 => "",
|
|
};
|
|
if (callee_name.len > 0) {
|
|
if (self.program_index.struct_template_map.getPtr(callee_name)) |tmpl| {
|
|
const inst_id = self.instantiateGenericStruct(tmpl, call_data.args);
|
|
// Register under the alias name
|
|
const alias_name_id = self.module.types.internString(cd.name);
|
|
const inst_info = self.module.types.get(inst_id);
|
|
if (inst_info == .@"struct") {
|
|
const alias_info: types.TypeInfo = .{ .@"struct" = .{
|
|
.name = alias_name_id,
|
|
.fields = inst_info.@"struct".fields,
|
|
} };
|
|
const alias_id = if (self.module.types.findByName(alias_name_id)) |existing| existing else self.module.types.intern(alias_info);
|
|
self.module.types.updatePreservingKey(alias_id, alias_info);
|
|
// A generic-struct instantiation alias IS a type
|
|
// author: route it through the unified writer so it
|
|
// lands in `type_aliases_by_source` and the bare-TYPE
|
|
// gate treats it like any other alias (a ns-only
|
|
// `Secret :: Box(s32)` is rejected, a flat one
|
|
// resolves to the same TypeId `findByName` would).
|
|
self.putTypeAlias(self.current_source_file, cd.name, alias_id);
|
|
}
|
|
} else 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 (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;
|
|
if (self.program_index.struct_template_map.getPtr(base_name)) |tmpl| {
|
|
const inst_id = self.instantiateGenericStruct(tmpl, pt.args);
|
|
const alias_name_id = self.module.types.internString(cd.name);
|
|
const inst_info = self.module.types.get(inst_id);
|
|
if (inst_info == .@"struct") {
|
|
const alias_info: types.TypeInfo = .{ .@"struct" = .{
|
|
.name = alias_name_id,
|
|
.fields = inst_info.@"struct".fields,
|
|
} };
|
|
const alias_id = if (self.module.types.findByName(alias_name_id)) |existing| existing else self.module.types.intern(alias_info);
|
|
self.module.types.updatePreservingKey(alias_id, alias_info);
|
|
// Same as the `.call` generic-struct branch: a
|
|
// parameterized-struct alias is a type author and
|
|
// must reach `type_aliases_by_source` so it gates.
|
|
self.putTypeAlias(self.current_source_file, cd.name, alias_id);
|
|
}
|
|
} else {
|
|
// Builtin parameterised type (Vector(N, T) etc) —
|
|
// resolve via type_bridge and register the result
|
|
// under the alias name so `Vec4` in expression
|
|
// position can `const_type(<vector tid>)`.
|
|
const result_ty = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
|
if (result_ty != .void and result_ty != .unresolved) {
|
|
self.putTypeAlias(self.current_source_file, cd.name, result_ty);
|
|
}
|
|
}
|
|
}
|
|
// comptime_expr handled in Pass 2
|
|
|
|
// Typed value constants (`AF_INET :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 => {
|
|
// Register enum/tagged-union types in the type table
|
|
_ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
|
},
|
|
.union_decl => {
|
|
// Register plain union types in the type table
|
|
_ = type_bridge.resolveAstType(decl, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
|
},
|
|
.error_set_decl => {
|
|
self.registerErrorSetDecl(decl);
|
|
},
|
|
.protocol_decl => {
|
|
self.registerProtocolDecl(&decl.data.protocol_decl);
|
|
},
|
|
.impl_block => {
|
|
self.protocolResolver().registerImplBlock(&decl.data.impl_block, is_imported, decl);
|
|
},
|
|
.foreign_class_decl => {
|
|
self.registerForeignClassDecl(&decl.data.foreign_class_decl);
|
|
},
|
|
.namespace_decl => |ns| {
|
|
self.registerNamespacedForeignClasses(ns);
|
|
if (self.main_file != null) {
|
|
self.scanDecls(ns.decls);
|
|
self.registerNamespaceQualifiedFns(ns.name, ns.own_decls);
|
|
}
|
|
},
|
|
.ufcs_alias => |ua| {
|
|
self.program_index.ufcs_alias_map.put(ua.name, ua.target) catch {};
|
|
},
|
|
// Top-level globals are registered in a second pass (below),
|
|
// after the forward-alias fixpoint, so a forward identifier
|
|
// alias used as a global's type annotation resolves (issue 0070).
|
|
.var_decl => {},
|
|
else => {},
|
|
}
|
|
}
|
|
self.resolveForwardIdentifierAliases(decls);
|
|
// Pass 2: registrations that resolve a top-level type annotation run
|
|
// after the alias fixpoint, so a forward identifier alias used as the
|
|
// annotation resolves to its target (issue 0070).
|
|
for (decls) |decl| {
|
|
self.setCurrentSourceFile(decl.source_file);
|
|
switch (decl.data) {
|
|
.var_decl => self.registerTopLevelGlobal(&decl.data.var_decl),
|
|
.const_decl => |cd| self.registerTypedModuleConst(&cd),
|
|
else => {},
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Register a typed module-level value constant (`AF_INET :s32: 2`). Run in
|
|
/// scanDecls pass 2 (after `resolveForwardIdentifierAliases`) so a forward
|
|
/// identifier alias in the annotation (`A :: B; B :: s32; K : A : 42;`)
|
|
/// resolves to its target rather than a fabricated empty-struct stub, which
|
|
/// would otherwise mistype the constant (issue 0070).
|
|
fn registerTypedModuleConst(self: *Lowering, cd: *const ast.ConstDecl) void {
|
|
const ta = cd.type_annotation orelse return;
|
|
// Only initializer shapes that pass 0 (binary_op / unary_op → placeholder
|
|
// `.s64`) or the literal path register as a USABLE module const need
|
|
// reconciling against the annotation. Every other shape (call,
|
|
// struct/array literal, bare identifier) is never registered as a
|
|
// foldable / emittable const, so it cannot manifest the issue-0088
|
|
// wrong-type fold/emit; a use-site diagnostic covers it.
|
|
switch (cd.value.data) {
|
|
.int_literal, .float_literal, .bool_literal, .string_literal, .undef_literal, .null_literal, .binary_op, .unary_op => {},
|
|
else => return,
|
|
}
|
|
const ty = self.resolveType(ta);
|
|
// An unresolvable annotation is already diagnosed by the type resolver;
|
|
// don't pile a bogus type-mismatch on top, and don't leave the pass-0
|
|
// placeholder behind as a usable const.
|
|
if (ty == .unresolved) {
|
|
self.dropModuleConst(self.current_source_file, cd.name);
|
|
return;
|
|
}
|
|
// Validate the initializer against the explicit annotation BY TYPE, so a
|
|
// const-EXPRESSION initializer (`N : string : M + 2`) is checked exactly
|
|
// like a literal rather than skipped. A mismatch is a type error, not a
|
|
// silently-accepted const — registering it would let `emitModuleConst`
|
|
// stamp the value with the wrong IR type (an int emitted as a `string`
|
|
// const → a bogus pointer that segfaults at the use site) and let the
|
|
// count path fold it (`[N]s64` → 4). Issue 0088.
|
|
if (!self.typedConstInitFits(cd.value, ty)) {
|
|
// A non-integral compile-time float into an integer const is the
|
|
// same implicit-narrowing failure as a typed local/field/param —
|
|
// report it with the unified wording (integral floats now FOLD here,
|
|
// so the old generic "initializer is a float literal/expression"
|
|
// message is stale). Every other mismatch keeps the generic wording.
|
|
if (self.isIntEx(ty) and isFloat(self.inferExprType(cd.value))) {
|
|
if (program_index_mod.evalConstFloatExpr(cd.value, self)) |fv| {
|
|
self.diagNonIntegralNarrow(cd.value.span, fv, ty);
|
|
self.dropModuleConst(self.current_source_file, cd.name);
|
|
return;
|
|
}
|
|
}
|
|
if (self.diagnostics) |d| {
|
|
d.addFmt(.err, cd.value.span, "type mismatch: constant '{s}' is declared '{s}' but its initializer is {s}", .{
|
|
cd.name, self.formatTypeName(ty), self.initializerDescription(cd.value),
|
|
});
|
|
}
|
|
// Evict the pass-0 placeholder (`N : string : 4` and
|
|
// `N : string : M + 2` are both pre-registered as `.s64` in scanDecls
|
|
// pass 0); leaving it would let a count use still fold `N`.
|
|
self.dropModuleConst(self.current_source_file, cd.name);
|
|
return;
|
|
}
|
|
// Reconcile the registration with the resolved annotation (pass 0 stored
|
|
// a literal/expression placeholder type), so the const folds and emits at
|
|
// its declared type — the same `put` the literal path always did.
|
|
const info = program_index_mod.ModuleConstInfo{ .value = cd.value, .ty = ty };
|
|
self.putModuleConst(self.current_source_file, cd.name, info);
|
|
}
|
|
|
|
/// True iff a literal initializer of `value`'s kind is faithfully
|
|
/// representable at the declared `dst_ty` — the precondition
|
|
/// `emitModuleConst` relies on when it materialises the constant. The arms
|
|
/// match `emitModuleConst`'s arms exactly, using the same type-kind
|
|
/// predicates (`isIntEx` / `isFloat` / the `module.types.get` tag) the rest
|
|
/// of lowering uses.
|
|
///
|
|
/// Deliberately NOT routed through `coercionResolver().classify`
|
|
/// (conversions.zig): that planner judges RUNTIME value coercions and is
|
|
/// unsound as a compile-time literal-representability oracle here — a `null`
|
|
/// literal's natural type is `.void`, so `classify(.void, *T)` yields `.none`
|
|
/// and would reject the valid `P : *void : null`; `bool` is 1 bit wide, so
|
|
/// `classify(.bool, s64)` yields `.widen` and would accept the bogus
|
|
/// `B : s64 : true`.
|
|
fn typedConstInitFits(self: *Lowering, value: *const Node, dst_ty: TypeId) bool {
|
|
// An INTEGER-annotated constant accepts a compile-time INTEGRAL float —
|
|
// a literal (`K : s64 : 4.0`), an int-leaf expression (`K : s64 : M + 2.0`
|
|
// → 4), or a float-const-leaf expression whose SUM is integral
|
|
// (`F : f64 : 2.5; K : s64 : F + 1.5` → 4). Integrality is judged on the
|
|
// FLOAT fold (`evalConstFloatExpr` + `floatToIntExact`) — the SAME facility
|
|
// the typed-local path (`foldComptimeFloatInit`) uses — not the int-only
|
|
// folder, which folds leaf-by-leaf in `i64` and so misses an integral SUM
|
|
// built from a non-integral float leaf. A non-integral fold (`1.5`,
|
|
// `M + 0.5`, `F + 0.25`) yields null here and falls through to the
|
|
// rejecting checks below, where `registerTypedModuleConst` emits the
|
|
// unified narrowing diagnostic.
|
|
if (self.isIntEx(dst_ty)) {
|
|
switch (value.data) {
|
|
.float_literal, .binary_op, .unary_op => {
|
|
if (program_index_mod.evalConstFloatExpr(value, self)) |fv| {
|
|
if (program_index_mod.floatToIntExact(fv) != null) return true;
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
return switch (value.data) {
|
|
// `---` zero-inits at any type.
|
|
.undef_literal => true,
|
|
// Integer literal → any integer (incl. custom widths) or float
|
|
// (`WIDTH : f32 : 800`).
|
|
.int_literal => self.isIntEx(dst_ty) or isFloat(dst_ty),
|
|
// Float literal → a float type only (the float arm emits `constFloat`).
|
|
.float_literal => isFloat(dst_ty),
|
|
.bool_literal => dst_ty == .bool,
|
|
.string_literal => dst_ty == .string,
|
|
// `null` → a pointer or optional.
|
|
.null_literal => !dst_ty.isBuiltin() and switch (self.module.types.get(dst_ty)) {
|
|
.pointer, .many_pointer, .optional => true,
|
|
else => false,
|
|
},
|
|
// Const-EXPRESSION initializer (binary_op / unary_op — the only
|
|
// non-literal kinds the caller admits): validate by the initializer's
|
|
// INFERRED type so coverage is type-based, not a per-node-kind
|
|
// allowlist where an unenumerated kind silently escapes (issue 0088,
|
|
// attempt 2). The integer/float fit mirrors the literal arms above.
|
|
else => self.constExprInitFits(self.inferExprType(value), dst_ty),
|
|
};
|
|
}
|
|
|
|
/// True iff a const-expression initializer of inferred type `init_ty` is
|
|
/// faithfully representable at the declared `dst_ty`. Type-based so it covers
|
|
/// every const-expression shape (binary_op, unary_op, …) through one check
|
|
/// rather than per-node-kind arms. The integer/float arms mirror the
|
|
/// int/float literal arms of `typedConstInitFits` (an integer expression fits
|
|
/// an integer or float annotation; a float expression fits a float).
|
|
fn constExprInitFits(self: *Lowering, init_ty: TypeId, dst_ty: TypeId) bool {
|
|
// An initializer whose type we couldn't infer is left for the use-site /
|
|
// emission diagnostic rather than rejected here (no over-rejection).
|
|
if (init_ty == .unresolved) return true;
|
|
if (self.isIntEx(init_ty)) return self.isIntEx(dst_ty) or isFloat(dst_ty);
|
|
if (isFloat(init_ty)) return isFloat(dst_ty);
|
|
if (init_ty == .bool) return dst_ty == .bool;
|
|
if (init_ty == .string) return dst_ty == .string;
|
|
// Any other concrete initializer type must match the annotation exactly.
|
|
return init_ty == dst_ty;
|
|
}
|
|
|
|
/// Register a top-level mutable global (e.g., `context : Context = ---;`).
|
|
/// Run AFTER `resolveForwardIdentifierAliases` so a forward identifier alias
|
|
/// in the type annotation (`A :: B; B :: s32; g : A = 7;`) resolves to its
|
|
/// target instead of a fabricated empty-struct stub, which would otherwise
|
|
/// give the global a type that mismatches its initializer at LLVM
|
|
/// verification (issue 0070). Globals can't be named in a type position, so
|
|
/// deferring them past type/alias registration introduces no ordering hazard.
|
|
fn registerTopLevelGlobal(self: *Lowering, vd: *const ast.VarDecl) void {
|
|
// Use self.resolveType so type aliases like `Handle :: u32;` resolve
|
|
// to their target type (not a synthetic empty struct). When the
|
|
// user omitted the annotation, infer from the initializer
|
|
// expression; foreign globals with no annotation are diagnosed
|
|
// because their type can't be inferred without an initializer.
|
|
const var_ty: TypeId = if (vd.type_annotation) |ta|
|
|
self.resolveType(ta)
|
|
else if (vd.value) |val|
|
|
self.inferExprType(val)
|
|
else blk: {
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, null, "top-level var '{s}' has no type annotation and no initializer to infer from", .{vd.name});
|
|
break :blk .void;
|
|
};
|
|
// Foreign globals reference a symbol defined in libSystem etc.
|
|
// (`_NSConcreteStackBlock : *void #foreign;`). The C symbol
|
|
// name is the optional override or the sx name itself.
|
|
const sym_name = vd.foreign_name orelse vd.name;
|
|
const name_id = self.module.types.internString(sym_name);
|
|
const init_val = self.globalInitValue(vd, var_ty);
|
|
const gid = self.module.addGlobal(.{
|
|
.name = name_id,
|
|
.ty = var_ty,
|
|
.init_val = init_val,
|
|
.is_const = false,
|
|
.is_extern = vd.is_foreign,
|
|
});
|
|
self.putGlobal(self.current_source_file, vd.name, .{ .id = gid, .ty = var_ty });
|
|
}
|
|
|
|
/// Serialize a top-level global's initializer into a static `ConstantValue`.
|
|
/// Foreign globals (extern symbol) and value-less declarations carry no
|
|
/// payload — they default to zero/extern at link, which is correct. An
|
|
/// identifier initializer that names a module constant is materialized from
|
|
/// the recorded constant (`K : A : 42; g : A = K;` → 42, issue 0071); a
|
|
/// global initialized from an identifier that resolves to no usable constant
|
|
/// is rejected with a diagnostic rather than silently zero-initialized — a
|
|
/// global has no run site for a dynamic initializer.
|
|
fn globalInitValue(self: *Lowering, vd: *const ast.VarDecl, var_ty: TypeId) ?inst_mod.ConstantValue {
|
|
if (vd.is_foreign) return null;
|
|
const v = vd.value orelse return null;
|
|
return switch (v.data) {
|
|
.undef_literal => .zeroinit,
|
|
.null_literal => .null_val,
|
|
.int_literal => |il| .{ .int = il.value },
|
|
.bool_literal => |bl| .{ .boolean = bl.value },
|
|
// A float initializer at an integer-typed global follows the
|
|
// implicit narrowing rule (integral folds, non-integral errors).
|
|
.float_literal => |fl| blk: {
|
|
if (self.isIntEx(var_ty)) {
|
|
if (program_index_mod.floatToIntExact(fl.value)) |iv| break :blk inst_mod.ConstantValue{ .int = iv };
|
|
self.diagNonIntegralNarrow(v.span, fl.value, var_ty);
|
|
break :blk null;
|
|
}
|
|
break :blk inst_mod.ConstantValue{ .float = fl.value };
|
|
},
|
|
.string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) },
|
|
.array_literal => |al| self.constArrayLiteral(al.elements, var_ty) orelse self.diagnoseNonConstGlobal(vd, v),
|
|
.struct_literal => |sl| self.constStructLiteral(&sl, var_ty) orelse self.diagnoseNonConstGlobal(vd, v),
|
|
.identifier => |id| blk: {
|
|
// A global initialized from a module constant copies the
|
|
// constant's recorded value (typed module consts land in
|
|
// `module_const_map` via `registerTypedModuleConst`, run in the
|
|
// same pass-2 before this).
|
|
if (self.program_index.module_const_map.get(id.name)) |ci| {
|
|
if (self.constExprValue(ci.value, var_ty)) |cv| break :blk cv;
|
|
}
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, v.span, "global '{s}' must be initialized by a compile-time constant; '{s}' is not a usable constant here", .{ vd.name, id.name });
|
|
break :blk null;
|
|
},
|
|
// An enum-literal global (`chosen : Color = .green;`) serializes to
|
|
// the variant's tag value against the destination enum type (issue
|
|
// 0082). The compiler-injected `OS`/`ARCH` globals flow through here
|
|
// too; their runtime reads resolve via `comptime_constants`, so the
|
|
// serialized tag only affects the static initializer.
|
|
.enum_literal => |el| self.constEnumLiteral(&el, var_ty, v.span),
|
|
// Any other initializer shape (`.field_access` on a const, a call, an
|
|
// arithmetic expression, …) is not a static constant the compiler can
|
|
// evaluate here. Diagnose loudly rather than emit a null payload that
|
|
// silently zero-initializes the global (issues 0071/0072).
|
|
else => blk: {
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, v.span, "global '{s}' must be initialized by a compile-time constant", .{vd.name});
|
|
break :blk null;
|
|
},
|
|
};
|
|
}
|
|
|
|
/// A global aggregate initializer (array/struct literal) that does not fully
|
|
/// reduce to a compile-time constant is rejected loudly. Without this the
|
|
/// `null` payload would fall through to a zero-initialized global, silently
|
|
/// dropping the declared fields (issues 0071/0072/0080).
|
|
fn diagnoseNonConstGlobal(self: *Lowering, vd: *const ast.VarDecl, v: *const Node) ?inst_mod.ConstantValue {
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, v.span, "global '{s}' must be initialized by a compile-time constant", .{vd.name});
|
|
return null;
|
|
}
|
|
|
|
/// Resolve identifier-RHS type aliases whose target is declared LATER in the
|
|
/// file. The forward scan above only registers an alias (`A :: B`) when `B`
|
|
/// is already resolved as a type author; a forward target isn't yet present,
|
|
/// so `A` is left unregistered and its uses get falsely flagged as an unknown
|
|
/// type (issue 0069). Re-resolve to a fixpoint now that every top-level name
|
|
/// has been seen, so `A :: B; B :: s32;` converges the same as the ordered
|
|
/// `B :: s32; A :: B;`. A value const is never an `.identifier` node
|
|
/// (`NotAType :: 123` is an int literal), and an alias whose target is a value
|
|
/// const stays unresolved, so neither this pass nor issue 0068 can register a
|
|
/// non-type name.
|
|
///
|
|
/// SOURCE-AWARE (R5 §4, E1.5). The target `B` is resolved AS SEEN FROM `A`'s
|
|
/// OWN source via the source-aware nominal leaf (`selectNominalLeaf` over
|
|
/// `type_aliases_by_source` / `moduleTypeAuthor` — E1), NEVER the global
|
|
/// `type_alias_map` / global `findByName`. The "already resolved" guard is
|
|
/// likewise per-source. When a same-name `B` is authored by a *different*
|
|
/// source (e.g. a namespaced import polluting the global alias map last-wins),
|
|
/// a global fixpoint would bind `A` to the wrong `B` and re-open 0105 one
|
|
/// layer down once E2 registers shadows; resolving against `A`'s source binds
|
|
/// the local `B`. The `.pending` outcome (B is itself a not-yet-resolved
|
|
/// forward alias) routes BACK into this fixpoint — `A` is skipped this round
|
|
/// and converges on a later iteration. `.undeclared` (no type author) and
|
|
/// `.not_visible` (a namespaced-only type, not bare-aliasable) leave `A`
|
|
/// unwritten; its uses surface the stub / diagnostic, never a silent global
|
|
/// leak. The write stays on the unified `putTypeAlias` helper (E1 no-drift
|
|
/// invariant — only the helper touches the maps).
|
|
fn resolveForwardIdentifierAliases(self: *Lowering, decls: []const *const Node) void {
|
|
var progressed = true;
|
|
while (progressed) {
|
|
progressed = false;
|
|
for (decls) |decl| {
|
|
const cd = switch (decl.data) {
|
|
.const_decl => |c| c,
|
|
else => continue,
|
|
};
|
|
if (cd.value.data != .identifier) continue;
|
|
const src = decl.source_file orelse self.main_file orelse continue;
|
|
if (self.aliasResolvedInSource(src, cd.name)) continue;
|
|
const rhs = cd.value.data.identifier;
|
|
// Pass the backtick raw flag so a forward alias whose RHS is a raw
|
|
// identifier (`` RawAlias :: `s2 ``, target declared later) resolves
|
|
// to the nominal `` `s2 `` author, not the builtin `s2` spelling.
|
|
switch (self.selectNominalLeaf(rhs.name, src, rhs.is_raw)) {
|
|
.resolved => |tid| {
|
|
self.putTypeAlias(decl.source_file, cd.name, tid);
|
|
progressed = true;
|
|
},
|
|
// B not yet a resolved type author from this source: a forward
|
|
// alias still pending (re-tried next round), a forward / not-
|
|
// yet-registered named author, an undeclared name, a
|
|
// namespaced-only type that is not bare-aliasable, or an
|
|
// ambiguous same-name shadow (≥2 flat authors). Leave A
|
|
// unwritten — no global last-wins leak; the ambiguity surfaces
|
|
// where A is used.
|
|
.pending, .forward, .undeclared, .not_visible, .ambiguous => {},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// TRUE iff `name` is already recorded as a type alias FROM `src` — the
|
|
/// per-source analogue of `type_alias_map.contains`, so the forward-alias
|
|
/// fixpoint resolves a same-name alias in each source independently (E1.5).
|
|
fn aliasResolvedInSource(self: *Lowering, src: []const u8, name: []const u8) bool {
|
|
if (self.program_index.type_aliases_by_source.get(src)) |inner| return inner.contains(name);
|
|
return false;
|
|
}
|
|
|
|
/// Try to convert an array literal's elements into a compile-time
|
|
/// ConstantValue.aggregate. `array_ty` is the array's resolved TypeId; its
|
|
/// element type drives type-aware serialization of struct-literal and
|
|
/// nested-array elements. Returns null if `array_ty` is not an array type or
|
|
/// any element is not a compile-time constant.
|
|
fn constArrayLiteral(self: *Lowering, elements: []const *const Node, array_ty: TypeId) ?inst_mod.ConstantValue {
|
|
if (array_ty.isBuiltin()) return null;
|
|
const elem_ty: TypeId = switch (self.module.types.get(array_ty)) {
|
|
.array => |a| a.element,
|
|
else => return null,
|
|
};
|
|
const vals = self.alloc.alloc(inst_mod.ConstantValue, elements.len) catch return null;
|
|
for (elements, 0..) |elem, i| {
|
|
vals[i] = self.constExprValue(elem, elem_ty) orelse return null;
|
|
}
|
|
return .{ .aggregate = vals };
|
|
}
|
|
|
|
/// Try to convert a single AST expression into a compile-time ConstantValue.
|
|
/// `expected_ty` is the destination element/field type — it lets aggregate
|
|
/// leaves (struct literals, nested arrays) serialize with the correct shape
|
|
/// rather than collapsing to null (issue 0080). Returns null if the
|
|
/// expression is not constant-foldable here.
|
|
fn constExprValue(self: *Lowering, expr: *const Node, expected_ty: TypeId) ?inst_mod.ConstantValue {
|
|
return switch (expr.data) {
|
|
.int_literal => |il| .{ .int = il.value },
|
|
.bool_literal => |bl| .{ .boolean = bl.value },
|
|
// A float into an INTEGER destination follows the implicit
|
|
// narrowing rule: an integral float folds to its int, a
|
|
// non-integral one is a compile error (not a silent bit-coerce).
|
|
.float_literal => |fl| blk: {
|
|
if (self.isIntEx(expected_ty)) {
|
|
if (program_index_mod.floatToIntExact(fl.value)) |iv| break :blk inst_mod.ConstantValue{ .int = iv };
|
|
self.diagNonIntegralNarrow(expr.span, fl.value, expected_ty);
|
|
break :blk null;
|
|
}
|
|
break :blk inst_mod.ConstantValue{ .float = fl.value };
|
|
},
|
|
.string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) },
|
|
.undef_literal => .zeroinit,
|
|
// A `null` in a pointer (or optional-pointer) field is a
|
|
// compile-time constant: the zero pointer. Without this arm the
|
|
// aggregate is wrongly rejected as non-constant (issue 0081).
|
|
.null_literal => .null_val,
|
|
.unary_op => |uo| switch (uo.op) {
|
|
.negate => switch (uo.operand.data) {
|
|
.int_literal => |il| .{ .int = -il.value },
|
|
.float_literal => |fl| .{ .float = -fl.value },
|
|
else => null,
|
|
},
|
|
else => null,
|
|
},
|
|
.array_literal => |al| self.constArrayLiteral(al.elements, expected_ty),
|
|
.struct_literal => |sl| self.constStructLiteral(&sl, expected_ty),
|
|
// An enum tag as an aggregate leaf (`[2]Color = .[.green, .blue]`, or
|
|
// an enum field inside a global struct) serializes to its tag int
|
|
// against the leaf's declared enum type (issue 0082).
|
|
.enum_literal => |el| self.constEnumLiteral(&el, expected_ty, expr.span),
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
/// Serialize an enum-literal initializer (`.Variant`) into a static
|
|
/// `ConstantValue.int` holding the variant's tag value, resolved against the
|
|
/// destination enum type `ty`. The tag respects explicit variant values
|
|
/// (`enum { a; b :: 5; }`); the enum's backing width is applied by the
|
|
/// const emitters via the destination type's LLVM type. Plain enums only —
|
|
/// a tagged-union or non-enum destination is diagnosed loudly rather than
|
|
/// silently zero-initialized (issue 0082).
|
|
fn constEnumLiteral(self: *Lowering, el: *const ast.EnumLiteral, ty: TypeId, span: ast.Span) ?inst_mod.ConstantValue {
|
|
if (!ty.isBuiltin()) {
|
|
const info = self.module.types.get(ty);
|
|
if (info == .@"enum") {
|
|
const e = info.@"enum";
|
|
const name_id = self.module.types.internString(el.name);
|
|
for (e.variants, 0..) |variant, i| {
|
|
if (variant != name_id) continue;
|
|
if (e.explicit_values) |vals| {
|
|
if (i < vals.len) return .{ .int = vals[i] };
|
|
}
|
|
return .{ .int = @intCast(i) };
|
|
}
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, span, "'.{s}' is not a variant of enum '{s}'", .{ el.name, self.module.types.getString(e.name) });
|
|
return null;
|
|
}
|
|
}
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, span, "enum-literal global initializer '.{s}' is only supported for a plain enum destination type", .{el.name});
|
|
return null;
|
|
}
|
|
|
|
/// Try to convert a struct literal into a compile-time ConstantValue.aggregate of the
|
|
/// struct's fields in declaration order, filling missing fields from the struct's
|
|
/// field defaults. Returns null if any value is not constant-foldable.
|
|
fn constStructLiteral(self: *Lowering, sl: *const ast.StructLiteral, ty: TypeId) ?inst_mod.ConstantValue {
|
|
if (ty.isBuiltin()) return null;
|
|
const ti = self.module.types.get(ty);
|
|
if (ti != .@"struct") return null;
|
|
const struct_fields = ti.@"struct".fields;
|
|
const struct_name = self.module.types.getString(ti.@"struct".name);
|
|
const field_defaults: []const ?*const Node = self.struct_defaults_map.get(struct_name) orelse &.{};
|
|
|
|
const has_names = sl.field_inits.len > 0 and sl.field_inits[0].name != null;
|
|
|
|
const vals = self.alloc.alloc(inst_mod.ConstantValue, struct_fields.len) catch return null;
|
|
for (struct_fields, 0..) |sf, fi| {
|
|
const sf_name = self.module.types.getString(sf.name);
|
|
const init_expr: ?*const Node = blk: {
|
|
if (has_names) {
|
|
for (sl.field_inits) |init_pair| {
|
|
if (init_pair.name) |n| {
|
|
if (std.mem.eql(u8, n, sf_name)) break :blk init_pair.value;
|
|
}
|
|
}
|
|
} else if (fi < sl.field_inits.len) {
|
|
break :blk sl.field_inits[fi].value;
|
|
}
|
|
if (fi < field_defaults.len) break :blk field_defaults[fi];
|
|
break :blk null;
|
|
};
|
|
if (init_expr) |e| {
|
|
vals[fi] = self.constExprValue(e, sf.ty) orelse return null;
|
|
} else {
|
|
vals[fi] = .zeroinit;
|
|
}
|
|
}
|
|
return .{ .aggregate = vals };
|
|
}
|
|
|
|
/// Pass 2: Lower main function body and comptime side-effects.
|
|
fn lowerMainAndComptime(self: *Lowering, decls: []const *const Node) void {
|
|
for (decls) |decl| {
|
|
// A `#run` body lowers in its OWN module's source context (fix-0102d
|
|
// site 4): `NAME :: #run f()` written in an imported module must
|
|
// resolve a bare `f` from that module's flat imports, not the main
|
|
// file's. Without this, `selectPlainCallableAuthor` runs with the main
|
|
// file's perspective and reports a genuine per-source author as
|
|
// ambiguous. Mirrors `scanDecls` / `lowerDecls`, which already set
|
|
// the source file per decl.
|
|
self.setCurrentSourceFile(decl.source_file);
|
|
switch (decl.data) {
|
|
.const_decl => |cd| {
|
|
if (cd.value.data == .fn_decl) {
|
|
if (isExportedEntryName(cd.name)) {
|
|
self.lazyLowerFunction(cd.name);
|
|
}
|
|
} else if (cd.value.data == .comptime_expr) {
|
|
self.lowerComptimeGlobal(cd.name, cd.value.data.comptime_expr.expr, cd.type_annotation);
|
|
}
|
|
},
|
|
.fn_decl => |fd| {
|
|
if (isExportedEntryName(fd.name)) {
|
|
self.lazyLowerFunction(fd.name);
|
|
}
|
|
},
|
|
.comptime_expr => |ct| {
|
|
self.lowerComptimeSideEffect(ct.expr);
|
|
},
|
|
.namespace_decl => |ns| {
|
|
if (self.main_file != null) {
|
|
self.lowerMainAndComptime(ns.decls);
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Lower every SHADOWED same-name function author into its OWN FuncId with a
|
|
/// real (non-extern) body — the identity-addressable lowering PATH this step
|
|
/// adds (fix-0102b). It does NOT run during a default compile: the name path
|
|
/// stays the sole resolver, so the suite is byte-for-byte unchanged. fix-0102c
|
|
/// invokes it as part of routing bare flat calls to the right author; until
|
|
/// then it is exercised by the lower-test regression that asserts two distinct
|
|
/// non-extern bodies for a same-name collision.
|
|
///
|
|
/// The first-wins flat/directory merge keeps exactly one author per name in
|
|
/// the merged decl list; `scanDecls` declares that WINNER (lowered on demand
|
|
/// through the name-keyed `lazyLowerFunction`). fix-0102a retained every
|
|
/// dropped same-name author in `module_fns` (path → name → `*FnDecl`) without
|
|
/// touching resolution; this walks that index 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_fns` 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_fns = self.program_index.module_fns 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_fns.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.iterator();
|
|
while (fn_it.next()) |fn_entry| {
|
|
const name = fn_entry.key_ptr.*;
|
|
const fd = fn_entry.value_ptr.*;
|
|
|
|
// 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_fns` + `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 `module_fns`' fn-only view by filtering each author through
|
|
/// `fnDeclOfRaw` (a `const`-wrapped fn unwraps to its inner fn — the exact
|
|
/// `*FnDecl` `module_fns` stored; 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 the fn-only
|
|
// `module_fns` walk did.
|
|
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 :: <type>`, a `const_decl` whose value resolved to a
|
|
// type, recorded in E0's `type_aliases_by_source`). Both kinds are gated
|
|
// identically: `moduleTypeAuthor` is the SINGLE source of truth, so a
|
|
// namespaced-only alias leaks no more than a namespaced-only named type,
|
|
// and a flat-visible alias is never poisoned by an invisible same-name
|
|
// named type (and vice-versa) — R4. A same-name flat VALUE/FUNCTION is
|
|
// NOT a type author (R1); a value-const (`N :: 7`) lives in
|
|
// `module_consts_by_source`, never in `type_aliases_by_source`, so it is
|
|
// correctly excluded too.
|
|
//
|
|
// The TYPE reachability here is SINGLE-HOP — `from`'s own author plus its
|
|
// DIRECT flat-import edges (`flatTypeAuthorCount`), the same non-transitive
|
|
// set the bare VALUE / FUNCTION / CONST leaves use (E4, consistent with
|
|
// 0706). A library template's INTERNAL type refs (`List.append`'s
|
|
// `alloc: Allocator`) still resolve because every instantiation kind
|
|
// (generic struct / fn / pack fn / param protocol / type fn) is
|
|
// source-pinned to the template's defining module, so the query
|
|
// originates THERE — where the type is a direct flat import — not at the
|
|
// cross-module call site.
|
|
const name_id = table.internString(name);
|
|
const registered = table.findByName(name_id);
|
|
|
|
// Compiler-synthesized default-Context emission resolves the built-in
|
|
// allocator types as infrastructure — fall open (the gate is for USER bare
|
|
// references, not compiler internals).
|
|
if (self.emitting_default_context) {
|
|
if (registered) |existing| return .{ .resolved = existing };
|
|
}
|
|
// Import facts unwired (registration / comptime host with no module_decls
|
|
// or flat graph): there is no querying context to gate against — preserve
|
|
// the legacy resolution (registered → existing; else forward-alias /
|
|
// undeclared).
|
|
if (self.program_index.module_decls == null or self.program_index.flat_import_graph == null) {
|
|
if (registered) |existing| return .{ .resolved = existing };
|
|
return self.forwardAliasOrUndeclared(name, from);
|
|
}
|
|
|
|
// 1. A flat-import-visible TYPE author (named type OR alias) — resolve to
|
|
// its PER-SOURCE declared TypeId (E2). The author KIND decides
|
|
// resolution: an ALIAS resolves to its `type_aliases_by_source` target;
|
|
// a NAMED type resolves to its per-decl `type_decl_tids` nominal
|
|
// identity — so two same-name structs authored in different sources
|
|
// return their OWN distinct TypeIds instead of collapsing last-wins
|
|
// (issue 0105). A named author not registered yet (a forward / self
|
|
// reference like `next: *ArenaChunk` resolved mid-registration) yields
|
|
// `.undeclared` → the legacy empty-struct stub, reconciled by
|
|
// `internNamedTypeDecl` adopting that stub when the type registers.
|
|
//
|
|
// The querying source's OWN author wins outright (own-wins, 0105 case
|
|
// 3); otherwise the single-hop direct flat-import set is searched, and
|
|
// ≥2 DISTINCT flat-visible authors → `.ambiguous` (0105 case 4). Single-
|
|
// author keeps ≤1 author across that set, so this stays byte-identical
|
|
// to the legacy leaf.
|
|
if (self.moduleTypeAuthor(from, name)) |author| switch (author) {
|
|
.alias => |tid| return .{ .resolved = tid },
|
|
.named => |ref| {
|
|
if (self.namedRefTid(ref, name)) |tid| return .{ .resolved = tid };
|
|
// The author exists but its slot is not interned yet (self /
|
|
// forward / mutual reference resolved mid-registration) — a
|
|
// forward stub the type adopts when it registers, NOT undeclared.
|
|
return .forward;
|
|
},
|
|
};
|
|
switch (self.flatTypeAuthorCount(name, from)) {
|
|
.none => {},
|
|
.one => |tid| return .{ .resolved = tid },
|
|
.ambiguous => return .ambiguous,
|
|
// A flat author exists but is not registered as a findByName-able type
|
|
// yet (a forward reference, or a foreign / lazily-registered class) →
|
|
// the legacy empty-struct stub, NOT a namespaced-only leak (arm 3).
|
|
.unregistered => return .forward,
|
|
}
|
|
|
|
// 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 (R2): 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 (resolved in the
|
|
// template's source context, E3 attempt-4) naming a type the CALLER
|
|
// declared block-local — the local is NOT visible here: route to the
|
|
// undeclared path so the leak surfaces, never the `registered` catch-all
|
|
// (arm 4) which would resolve the globally-registered cross-source local.
|
|
if (self.localTypeInSource(from, name)) {
|
|
if (registered) |existing| return .{ .resolved = existing };
|
|
} else if (self.localTypeInAnySource(name)) {
|
|
return self.forwardAliasOrUndeclared(name, from);
|
|
}
|
|
|
|
// 3. Authored as a TYPE (named OR alias) in some module, but NOT flat-
|
|
// import-reachable from `from` and NOT shadowed by a local → reachable
|
|
// only over a namespace edge → leak. Return `.not_visible`;
|
|
// `resolveNominalLeaf` surfaces the diagnostic and the `.unresolved`
|
|
// sentinel (qualify it `ns.Type`, Phase F).
|
|
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 (findByName hit, no module_decls
|
|
// author) resolves ungated. Otherwise a forward identifier alias
|
|
// (visible const author, target not resolved yet → `.pending`, back to
|
|
// the fixpoint) or `.undeclared`.
|
|
if (registered) |existing| return .{ .resolved = existing };
|
|
return self.forwardAliasOrUndeclared(name, from);
|
|
}
|
|
|
|
/// The forward-alias / undeclared tail of `selectNominalLeaf`: a bare nominal
|
|
/// name that is neither a flat-visible type author, a local, nor a leak.
|
|
/// Selects the single-hop const author (E1: `collectVisibleAuthors` returns
|
|
/// ≤1) and, if its alias target is not yet in `type_aliases_by_source`,
|
|
/// returns `.pending` so the forward-alias fixpoint re-resolves it (source-
|
|
/// aware in E1.5). A resolved flat-visible alias is already returned by
|
|
/// `moduleTypeAuthor` (arm 1) above, so the `inner.get` here only catches a
|
|
/// const author reachable via `collectVisibleAuthors` whose target landed
|
|
/// between the two reads — the fixpoint path is the common outcome.
|
|
fn forwardAliasOrUndeclared(self: *Lowering, name: []const u8, from: []const u8) TypeHeadResolution {
|
|
var res = self.resolver();
|
|
const set = res.collectVisibleAuthors(name, from, .user_bare_flat);
|
|
defer if (set.flat.len > 0) self.alloc.free(set.flat);
|
|
if (constAuthor(set)) |author| {
|
|
if (self.program_index.type_aliases_by_source.get(author.source)) |inner| {
|
|
if (inner.get(name)) |alias_ty| return .{ .resolved = alias_ty };
|
|
}
|
|
return .pending;
|
|
}
|
|
return .undeclared;
|
|
}
|
|
|
|
/// The single `const_decl` (alias-or-value const) author of a collected
|
|
/// `AuthorSet`. E1 is single-author (`collectVisibleAuthors` returns ≤1), so
|
|
/// own-then-flat picks the one author. E2 adds shadow ambiguity.
|
|
fn constAuthor(set: resolver_mod.AuthorSet) ?resolver_mod.RawAuthor {
|
|
if (set.own) |o| if (o.raw == .const_decl) return o;
|
|
for (set.flat) |fa| if (fa.raw == .const_decl) return fa;
|
|
return null;
|
|
}
|
|
|
|
/// 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 as a type author NOT here but via `type_aliases_by_source`
|
|
/// (E0's source-keyed cache) in `moduleTypeAuthor`, so the two type-author
|
|
/// kinds — named type and alias — gate identically (R4).
|
|
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,
|
|
};
|
|
}
|
|
|
|
/// A module's authorship of a bare type `name`: an ALIAS (carrying the
|
|
/// resolved target `TypeId` from `type_aliases_by_source`) or a NAMED type
|
|
/// (struct/enum/union/error-set/protocol/foreign class — carrying its
|
|
/// `RawDeclRef` so the use site can resolve its PER-DECL nominal TypeId via
|
|
/// `type_decl_tids`, decoupled from registration timing so a not-yet-registered
|
|
/// forward / self reference is still recognised as an author).
|
|
const FlatTypeAuthor = union(enum) {
|
|
alias: TypeId,
|
|
named: resolver_mod.RawDeclRef,
|
|
};
|
|
|
|
/// How module `path` authors `name` AS A TYPE, or null if it does not. A type
|
|
/// author is EITHER a type ALIAS (`Name :: <type>`, recorded in E0's
|
|
/// `type_aliases_by_source` — checked first via the source-keyed cache) OR a
|
|
/// NAMED type (recognised by its `module_decls` decl KIND; the `RawDeclRef` is
|
|
/// carried so the use site resolves its per-decl nominal TypeId). A same-name
|
|
/// VALUE/FUNCTION is NOT a type author (R1); a value-const (`N :: 7`) lives in
|
|
/// `module_consts_by_source`, never `type_aliases_by_source`, so it returns
|
|
/// null too. THE per-module "is `name` a type author here?" predicate — the
|
|
/// single source of truth for the visibility walk (R4).
|
|
fn moduleTypeAuthor(self: *Lowering, path: []const u8, name: []const u8) ?FlatTypeAuthor {
|
|
if (self.program_index.type_aliases_by_source.get(path)) |inner| {
|
|
if (inner.get(name)) |tid| return .{ .alias = tid };
|
|
}
|
|
const decls = self.program_index.module_decls orelse return null;
|
|
const m = decls.get(path) orelse return null;
|
|
const ref = m.names.get(name) orelse return null;
|
|
if (!isNamedTypeKind(ref)) return null;
|
|
return .{ .named = ref };
|
|
}
|
|
|
|
/// 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 / union / error-set / protocol / foreign-class keep the
|
|
/// legacy `findByName` resolution (same-name shadows of those kinds are a
|
|
/// later, orthogonal phase outside 0105's struct/alias scope).
|
|
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, .union_decl, .error_set_decl, .protocol_decl, .foreign_class_decl => table.findByName(table.internString(name)),
|
|
.fn_decl, .const_decl, .namespace_decl => null,
|
|
};
|
|
}
|
|
|
|
/// The per-source TypeId module `path` authors for bare `name`, or null. The
|
|
/// alias-or-named resolution behind the ambiguity walk: an ALIAS yields its
|
|
/// source-keyed target; a NAMED type yields its per-decl nominal TypeId (null
|
|
/// while still unregistered, so it does not count toward ambiguity mid-scan).
|
|
fn moduleTypeAuthorTid(self: *Lowering, path: []const u8, name: []const u8) ?TypeId {
|
|
return switch (self.moduleTypeAuthor(path, name) orelse return null) {
|
|
.alias => |tid| tid,
|
|
.named => |ref| self.namedRefTid(ref, name),
|
|
};
|
|
}
|
|
|
|
/// What bare `name`'s type authors look like across the SINGLE-HOP flat-import
|
|
/// set of `from` — its DIRECT bare `#import` edges only, NOT the transitive
|
|
/// closure (E4: consistent with the bare VALUE/FUNCTION/CONST leaves and
|
|
/// example 0706; the interim transitive closure E1 shipped is gone). The
|
|
/// querying source's OWN author is consulted by `selectNominalLeaf` first
|
|
/// (own-wins), so this surveys only the cross-module direct-flat authors:
|
|
/// - `.ambiguous` — ≥2 DISTINCT resolved TypeIds (issue 0105 case 4);
|
|
/// - `.one` — exactly one distinct resolved TypeId;
|
|
/// - `.unregistered` — ≥1 flat author found but none resolves to a TypeId
|
|
/// yet (a forward reference, or a foreign/lazily-registered author with no
|
|
/// `findByName` slot) → the caller yields the legacy stub, NOT a leak;
|
|
/// - `.none` — no flat author at all → the caller proceeds to the
|
|
/// local / leak / forward-alias arms.
|
|
/// Distinctness is BY TypeId: each distinct author holds a distinct
|
|
/// `nominal_id` TypeId, while a diamond import of the SAME module yields the
|
|
/// same TypeId, so byte-identical de-dup falls out. A library template's
|
|
/// INTERNAL bare-TYPE refs (a 2-flat-hop type like `List(T).append`'s
|
|
/// `alloc: Allocator`) stay resolvable because instantiation is source-pinned
|
|
/// to the template's defining module (E4 #1), so the query originates THERE —
|
|
/// where the type is a direct flat import — not at the cross-module call site.
|
|
/// The walk lives in `lower.zig`, NOT `resolver.zig` — the single-graph-walk
|
|
/// invariant (one `flat_import_graph` iterator in `resolver.zig`) is untouched.
|
|
const FlatTypeAuthorCount = union(enum) { none, one: TypeId, ambiguous, unregistered };
|
|
fn flatTypeAuthorCount(self: *Lowering, name: []const u8, from: []const u8) FlatTypeAuthorCount {
|
|
const graph = self.program_index.flat_import_graph orelse return .none;
|
|
const direct = graph.get(from) orelse return .none;
|
|
var found: ?TypeId = null;
|
|
var saw_author = false;
|
|
var it = direct.iterator();
|
|
while (it.next()) |kv| {
|
|
const dep = kv.key_ptr.*;
|
|
if (self.moduleTypeAuthor(dep, name) != null) {
|
|
saw_author = true;
|
|
if (self.moduleTypeAuthorTid(dep, name)) |tid| {
|
|
if (found) |f| {
|
|
if (tid != f) return .ambiguous;
|
|
} else found = tid;
|
|
}
|
|
}
|
|
}
|
|
if (found) |t| return .{ .one = t };
|
|
return if (saw_author) .unregistered else .none;
|
|
}
|
|
|
|
/// TRUE iff `name` is authored as a TYPE — a NAMED type OR a type ALIAS — in
|
|
/// ANY module's raw facts. The leak detector: a name that is a type author
|
|
/// somewhere but not flat-visible from the querying module is reachable only
|
|
/// over a namespace edge. Both kinds are checked (R4): named types via
|
|
/// `module_decls`, aliases via E0's `type_aliases_by_source`. Distinguishes a
|
|
/// real cross-module TYPE author from a LOCAL type / generic-param /
|
|
/// fabricated empty-struct stub (findByName-registered but authored in no
|
|
/// module) and from a same-name VALUE/FUNCTION author (not a type). Unwired
|
|
/// facts → false (nothing to gate; resolve ungated).
|
|
fn nameAuthoredAsTypeAnywhere(self: *Lowering, name: []const u8) bool {
|
|
if (self.program_index.module_decls) |decls| {
|
|
var it = decls.valueIterator();
|
|
while (it.next()) |m| {
|
|
if (m.names.get(name)) |ref| if (isNamedTypeKind(ref)) return true;
|
|
}
|
|
}
|
|
var ait = self.program_index.type_aliases_by_source.valueIterator();
|
|
while (ait.next()) |inner| {
|
|
if (inner.contains(name)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Record a name declared as a BLOCK-LOCAL type so the bare-TYPE gate never
|
|
/// mistakes it for a namespaced-only leak (see `local_type_names`). Keyed by the
|
|
/// declaring source (the function being lowered) so the local is visible only
|
|
/// within that source.
|
|
fn recordLocalTypeName(self: *Lowering, name: []const u8) void {
|
|
const src = self.current_source_file orelse self.main_file orelse return;
|
|
const gop = self.local_type_names.getOrPut(src) catch return;
|
|
if (!gop.found_existing) gop.value_ptr.* = std.StringHashMap(void).init(std.heap.page_allocator);
|
|
gop.value_ptr.put(name, {}) catch {};
|
|
}
|
|
|
|
/// TRUE iff `name` is a block-local type declared in `source`.
|
|
fn localTypeInSource(self: *Lowering, source: []const u8, name: []const u8) bool {
|
|
if (self.local_type_names.get(source)) |inner| return inner.contains(name);
|
|
return false;
|
|
}
|
|
|
|
/// TRUE iff `name` is a block-local type declared in ANY source. A name that is a
|
|
/// local SOMEWHERE but not in the querying source is a cross-source local — not
|
|
/// visible from the querying source.
|
|
fn localTypeInAnySource(self: *Lowering, name: []const u8) bool {
|
|
var it = self.local_type_names.valueIterator();
|
|
while (it.next()) |inner| if (inner.contains(name)) return true;
|
|
return false;
|
|
}
|
|
|
|
/// Resolve the bare TYPE leaf to a `TypeId` for `resolveTypeWithBindings`.
|
|
/// Routes through the source-aware `selectNominalLeaf`. `.pending` (forward
|
|
/// alias) and `.forward` (a real author not interned yet — self / forward /
|
|
/// foreign reference) keep the empty-struct stub, which the type ADOPTS on
|
|
/// registration (`internNamedTypeDecl`). `.undeclared` (NO author anywhere)
|
|
/// is genuinely-undeclared: in a NON-main module — which the
|
|
/// `UnknownTypeChecker` trusts and never walks — the leaf is the only guard,
|
|
/// so it emits "unknown type" and poisons with `.unresolved` (never a silent
|
|
/// 0-field struct). In the MAIN file the checker owns the diagnostic (and a
|
|
/// valid unbound generic leaf legitimately reaches here), so the leaf keeps
|
|
/// the legacy stub. `.not_visible` / `.ambiguous` surface their own loud
|
|
/// diagnostic + `.unresolved`. When the source context is unwired
|
|
/// (`current_source_file` null — comptime / registration callers), there is no
|
|
/// querying module to collect from, so fall open to the legacy namer.
|
|
fn resolveNominalLeaf(self: *Lowering, name: []const u8, raw: bool, span: ?ast.Span) TypeId {
|
|
const from = self.current_source_file orelse
|
|
return self.typeResolver().resolveName(name, raw);
|
|
return switch (self.selectNominalLeaf(name, from, raw)) {
|
|
.resolved => |t| t,
|
|
// A forward alias (`.pending`) or a forward / not-yet-interned named
|
|
// author (`.forward`) — keep the empty-struct stub the type adopts
|
|
// when it registers. A raw or non-raw bare name both land the same
|
|
// stub here.
|
|
.pending, .forward => self.module.types.intern(.{ .@"struct" = .{
|
|
.name = self.module.types.internString(name),
|
|
.fields = &.{},
|
|
} }),
|
|
// Genuinely undeclared: no type / alias / const author anywhere.
|
|
.undeclared => {
|
|
// The MAIN file is the `UnknownTypeChecker`'s domain — it emits
|
|
// the canonical "unknown type" (with scope context + value-param
|
|
// hints) and `hasErrors` halts before the stub reaches codegen,
|
|
// and a valid unbound generic leaf (`-> T` on a template) also
|
|
// lands here — so keep the legacy stub and do NOT double-report.
|
|
// A NON-main (imported / library) module is checker-trusted, so
|
|
// this leaf is the sole guard: emit + poison with `.unresolved`.
|
|
const is_main = if (self.main_file) |mf| std.mem.eql(u8, from, mf) else true;
|
|
if (is_main) return self.module.types.intern(.{ .@"struct" = .{
|
|
.name = self.module.types.internString(name),
|
|
.fields = &.{},
|
|
} });
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, span, "unknown type '{s}'", .{name});
|
|
return .unresolved;
|
|
},
|
|
// Registered, but reachable only through a namespaced import: emit the
|
|
// diagnostic at the reference and poison the result so no downstream
|
|
// check (field access, size) trusts a leaked / mis-sized type.
|
|
// `.unresolved` is poison-suppressed, so there is no secondary
|
|
// "field not found" cascade.
|
|
.not_visible => {
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, span, "type '{s}' is not visible; #import the module that declares it", .{name});
|
|
return .unresolved;
|
|
},
|
|
// ≥2 distinct same-name type authors flat-visible, none own (issue
|
|
// 0105 case 4): a genuine collision the source can't disambiguate.
|
|
// Emit a loud diagnostic and poison — never a silent first-/last-wins.
|
|
.ambiguous => {
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, span, "type '{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{name});
|
|
return .unresolved;
|
|
},
|
|
};
|
|
}
|
|
|
|
/// The `*FnDecl` a raw author wraps, or null when the author is not a
|
|
/// function — `imports.fnDeclOf` over a `RawDeclRef` so the collector's
|
|
/// all-domain authors reproduce `module_fns`' fn-only view (a `const`-wrapped
|
|
/// fn unwraps to its inner fn, the same pointer `module_fns` holds; every
|
|
/// other domain → null).
|
|
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,
|
|
};
|
|
}
|
|
|
|
/// Materialize (lower-on-demand) the FuncId for a selected bare-call author,
|
|
/// caching into `sf.materialized`. Shadow-only: the winner owns the
|
|
/// name-keyed slot and lowers through the lazy path, so
|
|
/// `selectPlainCallableAuthor` returns `.none` for it and this is never asked
|
|
/// to lower the winner (0102d). `name` is the call name (== the author's
|
|
/// registered name); `sf.source` pins the author's own visibility context.
|
|
fn selectedFuncId(self: *Lowering, sf: *SelectedFunc, name: []const u8) FuncId {
|
|
if (sf.materialized) |fid| return fid;
|
|
const fid = self.bareAuthorFuncId(sf.decl, name, sf.source);
|
|
sf.materialized = fid;
|
|
return fid;
|
|
}
|
|
|
|
/// The FuncId for a resolved bare-call author, ensuring its body is lowered.
|
|
/// Only ever called for a SHADOW (an author that is not the name-keyed
|
|
/// winner): the winner owns the name-keyed slot and lowers through the
|
|
/// normal lazy path, so `selectPlainCallableAuthor` returns `.none` for it. A shadow
|
|
/// is declared a fresh same-name FuncId in its OWN module's visibility
|
|
/// context and its body lowered into that slot via fix-0102b's identity-
|
|
/// addressable `lowerFunctionBodyInto`. Idempotent: `lowered_fids` tracks
|
|
/// which slots already carry a body.
|
|
fn bareAuthorFuncId(self: *Lowering, fd: *const ast.FnDecl, name: []const u8, path: []const u8) FuncId {
|
|
if (self.fn_decl_fids.get(fd)) |fid| {
|
|
if (!self.lowered_fids.contains(fid)) {
|
|
self.lowered_fids.put(fid, {}) catch {};
|
|
self.lowerFunctionBodyInto(fd, fid, name);
|
|
}
|
|
return fid;
|
|
}
|
|
const saved_src = self.current_source_file;
|
|
self.setCurrentSourceFile(path);
|
|
self.declareFunction(fd, name);
|
|
self.setCurrentSourceFile(saved_src);
|
|
const fid = self.fn_decl_fids.get(fd).?;
|
|
self.lowered_fids.put(fid, {}) catch {};
|
|
self.lowerFunctionBodyInto(fd, fid, name);
|
|
return fid;
|
|
}
|
|
|
|
/// Declare a function as an extern stub (signature only, no body).
|
|
pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8) void {
|
|
// Skip generic templates — they're monomorphized on demand, not declared as extern
|
|
if (fd.type_params.len > 0) return;
|
|
|
|
const ret_ty = self.resolveReturnType(fd);
|
|
|
|
// Foreign declarations with a trailing variadic param map to the C
|
|
// calling convention's `...` tail. Drop the variadic param from the
|
|
// IR signature (it has no C-level slot) and set is_variadic.
|
|
const is_foreign = fd.body.data == .foreign_expr;
|
|
var is_variadic = false;
|
|
var effective_params = fd.params;
|
|
if (is_foreign and fd.params.len > 0 and fd.params[fd.params.len - 1].is_variadic) {
|
|
is_variadic = true;
|
|
effective_params = fd.params[0 .. fd.params.len - 1];
|
|
}
|
|
|
|
const wants_ctx = self.funcWantsImplicitCtx(fd);
|
|
var params = std.ArrayList(Function.Param).empty;
|
|
if (wants_ctx) {
|
|
params.append(self.alloc, .{
|
|
.name = self.module.types.internString("__sx_ctx"),
|
|
.ty = self.module.types.ptrTo(.void),
|
|
}) catch unreachable;
|
|
}
|
|
for (effective_params) |p| {
|
|
const pty = self.resolveParamType(&p);
|
|
params.append(self.alloc, .{
|
|
.name = self.module.types.internString(p.name),
|
|
.ty = pty,
|
|
}) catch unreachable;
|
|
}
|
|
|
|
// `#foreign` declarations are external C symbols by definition —
|
|
// promote them to callconv(.c) when the user didn't write it
|
|
// explicitly. This keeps fn-ptr coercion type-safe: anything
|
|
// typed by name as `(args) -> ret` of a `#foreign` decl can be
|
|
// assigned to / passed as a `callconv(.c)` fn-pointer without a
|
|
// call-convention mismatch.
|
|
const cc: Function.CallingConvention = if (fd.call_conv == .c or is_foreign) .c else .default;
|
|
|
|
// For #foreign with C name override, declare under C name and map sx name → C name
|
|
if (is_foreign) {
|
|
const fe = fd.body.data.foreign_expr;
|
|
if (fe.c_name) |c_name| {
|
|
const c_name_id = self.module.types.internString(c_name);
|
|
const fid = self.builder.declareExtern(c_name_id, params.items, ret_ty);
|
|
const func = self.module.getFunctionMut(fid);
|
|
func.call_conv = cc;
|
|
func.source_file = self.current_source_file;
|
|
func.is_variadic = is_variadic;
|
|
func.has_implicit_ctx = wants_ctx;
|
|
self.foreign_name_map.put(name, c_name) catch {};
|
|
self.fn_decl_fids.put(fd, fid) catch {};
|
|
return;
|
|
}
|
|
}
|
|
|
|
const name_id = self.module.types.internString(name);
|
|
const fid = self.builder.declareExtern(name_id, params.items, ret_ty);
|
|
const func = self.module.getFunctionMut(fid);
|
|
func.call_conv = cc;
|
|
func.source_file = self.current_source_file;
|
|
func.is_variadic = is_variadic;
|
|
func.has_implicit_ctx = wants_ctx;
|
|
self.fn_decl_fids.put(fd, fid) catch {};
|
|
}
|
|
|
|
/// Register a namespaced import's OWN functions under their module-qualified
|
|
/// name (`ns.fn`), giving each a UNIQUE FuncId in the function table. Two
|
|
/// modules each exporting a top-level `parse` otherwise collide in the
|
|
/// bare-name `fn_ast_map` / function table (last-wins) while `resolveFuncByName`
|
|
/// picks the first declared, so `lazyLowerFunction` lowers one signature
|
|
/// against the other's body and trips its param-count assert (issue 0100).
|
|
/// The bare recursion in `scanDecls` still registers intra-module bare calls;
|
|
/// this adds the qualified identity the `pkg.fn(...)` resolution paths in
|
|
/// `CallResolver.plan` / `lowerCall` already prefer.
|
|
fn registerNamespaceQualifiedFns(self: *Lowering, ns_name: []const u8, own_decls: []const *Node) void {
|
|
const saved_source = self.current_source_file;
|
|
defer self.setCurrentSourceFile(saved_source);
|
|
for (own_decls) |decl| {
|
|
self.setCurrentSourceFile(decl.source_file);
|
|
switch (decl.data) {
|
|
.fn_decl => self.registerQualifiedFn(ns_name, &decl.data.fn_decl, decl.data.fn_decl.name),
|
|
.const_decl => |cd| {
|
|
if (cd.value.data == .fn_decl) {
|
|
self.registerQualifiedFn(ns_name, &cd.value.data.fn_decl, cd.name);
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn registerQualifiedFn(self: *Lowering, ns_name: []const u8, fd: *const ast.FnDecl, short: []const u8) void {
|
|
// Only PLAIN free functions need a qualified identity. Generic /
|
|
// comptime / pack functions (`Vector`, `print`, `any_to_string`) are
|
|
// dispatched by monomorphization off their BARE template name, not the
|
|
// plain `resolveFuncByName` / `lazyLowerFunction` path that trips the
|
|
// collision assert (issue 0100); registering a qualified alias for them
|
|
// would divert that machinery and strand a per-call type binding.
|
|
if (fd.type_params.len > 0 or hasComptimeParams(fd) or isPackFn(fd)) return;
|
|
// Foreign / builtin / #compiler bodies keep their literal name; a
|
|
// qualified alias has no distinct symbol to resolve to.
|
|
switch (fd.body.data) {
|
|
.foreign_expr, .builtin_expr, .compiler_expr => return,
|
|
else => {},
|
|
}
|
|
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ ns_name, short }) catch return;
|
|
if (self.program_index.fn_ast_map.contains(qualified)) return;
|
|
self.program_index.fn_ast_map.put(qualified, fd) catch {};
|
|
self.program_index.import_flags.put(qualified, true) catch {};
|
|
// Carry the alias's OWN declaring source file (the caller in
|
|
// `registerNamespaceQualifiedFns` pins `current_source_file` to the
|
|
// decl's source before each call). `lazyLowerFunction`'s null-FuncId
|
|
// path restores this so `ns.fn`'s body lowers in its own module's
|
|
// visibility context, not the call site's (issue 0100 F1).
|
|
if (self.current_source_file) |src| {
|
|
self.program_index.qualified_fn_source.put(qualified, src) catch {};
|
|
}
|
|
// No eager `declareFunction` here: the extern stub's param/return types
|
|
// would be resolved now, before the forward-alias fixpoint, caching an
|
|
// `.unresolved` for any type declared later in the module. The qualified
|
|
// function is declared + lowered on demand by `lazyLowerFunction`'s
|
|
// null-FuncId path (`lowerFunction`), which runs after all types resolve.
|
|
}
|
|
|
|
/// The unified non-transitive `#import` visibility predicate, parameterized
|
|
/// by `VisibilityMode`. `isNameVisible` / `isCImportVisible` are thin
|
|
/// adapters over it.
|
|
///
|
|
/// This is the lowering-side GATE: it walks `module_scopes` (the per-file
|
|
/// name set) joined over the edge set the mode selects. It is distinct from
|
|
/// `resolver.collectVisibleAuthors`, which collects raw AUTHORS over
|
|
/// `module_decls` — the single graph-walk that lives in `resolver.zig`. The
|
|
/// two read different facts (name set vs author refs) for different jobs, so
|
|
/// the gate's own iterator stays here, not in the resolver.
|
|
///
|
|
/// `module_scopes[F]` holds ONLY the names authored in F (plus its namespace
|
|
/// aliases); cross-module visibility is joined here at query time. Doing the
|
|
/// join at lookup (instead of pre-merging in `resolveImports`) lets cyclic
|
|
/// imports like std.sx ↔ allocators.sx still resolve, since the cycle's
|
|
/// skipped edge is still recorded in the graph and the partner's scope is
|
|
/// filled in by the time lowering queries it.
|
|
fn isVisible(self: *Lowering, name: []const u8, vis: resolver_mod.VisibilityMode) bool {
|
|
switch (vis) {
|
|
// Registration / lazy lowering paths don't police user visibility.
|
|
.lowering_internal => return true,
|
|
// Transitive visibility is ProtocolResolver.findVisibleImpls' job;
|
|
// this predicate is single-hop only.
|
|
.impl_transitive => @panic("isVisible: transitive visibility is owned by findVisibleImpls"),
|
|
.c_import_bare => {
|
|
// Foreign-C gate: only C-import fn_decls without a library_ref
|
|
// are policed; a non-foreign body or a library-bound foreign
|
|
// decl is unconditionally visible.
|
|
const fd = self.program_index.fn_ast_map.get(name) orelse return true;
|
|
if (fd.body.data != .foreign_expr) return true;
|
|
if (fd.body.data.foreign_expr.library_ref != null) return true;
|
|
return self.visibleOverEdges(name, .flat);
|
|
},
|
|
.user_bare_flat => return self.visibleOverEdges(name, .flat),
|
|
.legacy_direct_any => return self.visibleOverEdges(name, .all),
|
|
}
|
|
}
|
|
|
|
const VisEdgeSet = enum { flat, all };
|
|
|
|
/// Resolve the mode's edge set and run the per-file visibility walk. Falls
|
|
/// open (visible) when the scoping infrastructure isn't wired (comptime
|
|
/// callers, directory imports without main_file, etc.). The caller is
|
|
/// responsible for restricting the check to names that ARE known top-level
|
|
/// decls; otherwise every local variable would be policed.
|
|
fn visibleOverEdges(self: *Lowering, name: []const u8, edges: VisEdgeSet) bool {
|
|
const source = self.current_source_file orelse return true;
|
|
const graph = switch (edges) {
|
|
.flat => self.program_index.flat_import_graph,
|
|
.all => self.program_index.import_graph,
|
|
};
|
|
return nameVisibleOverEdges(self.program_index.module_scopes, graph, source, name);
|
|
}
|
|
|
|
/// Check if a C-imported function is visible from the current source file.
|
|
/// Returns true for non-C functions (always visible) or if no scoping info
|
|
/// available. Byte-identical adapter over `isVisible`.
|
|
fn isCImportVisible(self: *Lowering, fn_name: []const u8) bool {
|
|
return self.isVisible(fn_name, .c_import_bare);
|
|
}
|
|
|
|
/// Non-transitive `#import` visibility check for top-level decls.
|
|
/// Byte-identical adapter over `isVisible`.
|
|
fn isNameVisible(self: *Lowering, name: []const u8) bool {
|
|
return self.isVisible(name, .user_bare_flat);
|
|
}
|
|
|
|
/// Lazily lower a function body on demand. Called when lowerCall can't find
|
|
/// the function and it exists in fn_ast_map.
|
|
fn lazyLowerFunction(self: *Lowering, name: []const u8) void {
|
|
// Already lowered?
|
|
if (self.lowered_functions.contains(name)) return;
|
|
|
|
// For sx-defined `#objc_class` methods, pin current_foreign_class
|
|
// so `*Self` substitutions in resolveTypeWithBindings find the
|
|
// state-struct type (M1.2 A.2b). The inline body-lowering path
|
|
// below re-resolves param types, so the context must be set
|
|
// BEFORE any resolveReturnType / resolveParamType call.
|
|
const saved_fc_lazy = self.current_foreign_class;
|
|
defer self.current_foreign_class = saved_fc_lazy;
|
|
if (self.lookupObjcDefinedClassForMethod(name)) |fcd| {
|
|
self.current_foreign_class = fcd;
|
|
}
|
|
// No AST? (builtins, foreign functions, or imported functions not in this file)
|
|
const fd = self.program_index.fn_ast_map.get(name) orelse return;
|
|
// Foreign declarations stay as extern stubs but need to be REGISTERED
|
|
// in the current module so callers get a real FuncId. Without this,
|
|
// a comptime-lowered function (e.g. `concat` from std.sx pulled into
|
|
// a fresh ct_module via `evalComptimeString`) emits `.call` against a
|
|
// FuncId that doesn't exist locally; the interp can't find the
|
|
// foreign target and silently no-ops instead of dispatching to libc.
|
|
if (fd.body.data == .foreign_expr) {
|
|
if (self.resolveFuncByName(name) == null) {
|
|
self.declareFunction(fd, name);
|
|
self.lowered_functions.put(name, {}) catch {};
|
|
}
|
|
return;
|
|
}
|
|
// Builtins / #compiler bodies stay as compiler-handled — no extern stub needed.
|
|
if (fd.body.data == .builtin_expr or fd.body.data == .compiler_expr) return;
|
|
if (fd.type_params.len > 0) return; // generics handled by monomorphization (Step 3.13)
|
|
|
|
// Defer functions with type-category matches until all types are registered.
|
|
// any_to_string uses `if type == { case slice: ... }` which compiles a switch
|
|
// with type tags from resolveTypeCategoryTags. This must happen AFTER main is
|
|
// fully lowered so all types ([]s32, List__s32, etc.) are in the TypeTable.
|
|
if (!self.processing_deferred and std.mem.eql(u8, name, "any_to_string")) {
|
|
self.deferred_type_fns.append(self.alloc, name) catch {};
|
|
return;
|
|
}
|
|
|
|
// Mark as lowered before lowering (prevents infinite recursion)
|
|
self.lowered_functions.put(name, {}) catch {};
|
|
|
|
// Find the existing extern stub (from scanDecls), keyed by NAME — the
|
|
// FIRST author of a name owns this slot. A shadowed same-name author is
|
|
// not here (it has no name-keyed slot); it is lowered out-of-line into
|
|
// its OWN FuncId by `lowerRetainedSameNameAuthors` (fix-0102b).
|
|
const name_id = self.module.types.internString(name);
|
|
var func_id: ?FuncId = null;
|
|
for (self.module.functions.items, 0..) |func, i| {
|
|
if (func.name == name_id) {
|
|
func_id = FuncId.fromIndex(@intCast(i));
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (func_id) |fid| {
|
|
self.lowerFunctionBodyInto(fd, fid, name);
|
|
return;
|
|
}
|
|
|
|
// Function not yet declared — create it fresh via lowerFunction. A
|
|
// module-qualified alias (`ns.fn`, issue 0100) is registered in
|
|
// `fn_ast_map` without an eager `declareFunction`, so there's no
|
|
// `Function.source_file` to switch to. Restore the alias's OWN declaring
|
|
// source before lowering its body, otherwise it lowers in the caller's
|
|
// visibility context and an own-import callee (`foo` calling `helper`
|
|
// from `foo`'s module's flat import) is reported "not visible" (0100 F1).
|
|
// The reentry guard keeps the nested lowering transparent to the caller.
|
|
var reentry = FnBodyReentry.enter(self);
|
|
defer reentry.restore();
|
|
if (self.program_index.qualified_fn_source.get(name)) |src| {
|
|
self.setCurrentSourceFile(src);
|
|
}
|
|
self.lowerFunction(fd, name, false);
|
|
}
|
|
|
|
/// Lower `fd`'s body into the SPECIFIC `fid`, promoting its extern stub to a
|
|
/// real function. Identity-addressable: the caller passes the exact FuncId,
|
|
/// so a SHADOWED same-name author lowers into its OWN slot instead of
|
|
/// colliding on the name-keyed `resolveFuncByName` (which returns the first
|
|
/// author, the very split that trips issue 0100's param-count assert). Self-
|
|
/// contained — the `FnBodyReentry` guard makes the nested lowering
|
|
/// transparent to any in-progress caller body (issue 0100 F2) — so it serves
|
|
/// both `lazyLowerFunction`'s name-keyed found path and the out-of-line
|
|
/// `lowerRetainedSameNameAuthors` pass.
|
|
fn lowerFunctionBodyInto(self: *Lowering, fd: *const ast.FnDecl, fid: FuncId, name: []const u8) void {
|
|
// objc-defined-class method context for `*Self` substitution (M1.2 A.2b);
|
|
// the resolveReturnType / resolveParamType calls below consult it.
|
|
const saved_fc = self.current_foreign_class;
|
|
defer self.current_foreign_class = saved_fc;
|
|
if (self.lookupObjcDefinedClassForMethod(name)) |fcd| {
|
|
self.current_foreign_class = fcd;
|
|
}
|
|
|
|
var reentry = FnBodyReentry.enter(self);
|
|
defer reentry.restore();
|
|
|
|
// Re-use the existing function slot — switch builder to it. Pin the
|
|
// function's OWN source BEFORE resolving the return type, so a same-name
|
|
// shadowed type in the signature (issue 0105) resolves against THIS
|
|
// function's module rather than the caller's (which, importing two
|
|
// same-name authors, would be ambiguous). Param types below already
|
|
// resolve after this point.
|
|
self.builder.func = fid;
|
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
|
self.setCurrentSourceFile(func.source_file);
|
|
|
|
const ret_ty = self.resolveReturnType(fd);
|
|
|
|
if (!func.is_extern) {
|
|
// Already promoted (e.g., via lowerComptimeDeps) — skip.
|
|
return;
|
|
}
|
|
func.is_extern = false; // promote from extern stub to real function
|
|
func.linkage = if (isExportedEntryName(name)) .external else .internal;
|
|
if (fd.call_conv == .c) func.call_conv = .c;
|
|
// Set inst_counter to param count (params occupy refs 0..N-1). IR params
|
|
// = AST params + 1 if the function carries `__sx_ctx` at slot 0.
|
|
const ctx_slots: usize = if (func.has_implicit_ctx) 1 else 0;
|
|
std.debug.assert(func.params.len == fd.params.len + ctx_slots);
|
|
self.builder.inst_counter = @intCast(func.params.len);
|
|
|
|
// Create entry block
|
|
const entry_name = self.module.types.internString("entry");
|
|
const entry = self.builder.appendBlock(entry_name, &.{});
|
|
self.builder.switchToBlock(entry);
|
|
|
|
// Create scope and bind params
|
|
var scope = Scope.init(self.alloc, null);
|
|
defer scope.deinit();
|
|
self.scope = &scope;
|
|
|
|
// The implicit `__sx_ctx` param (when present) lives at slot 0; user
|
|
// params shift by one. `current_ctx_ref` is bound to slot 0 so call-site
|
|
// lowering can prepend it to every sx-to-sx call. For OS-called entry
|
|
// points (main / JNI hooks) there's no ctx param — synthesise
|
|
// `&__sx_default_context` and bind `current_ctx_ref` to its address.
|
|
const wants_ctx = self.funcWantsImplicitCtx(fd);
|
|
const saved_ctx_ref = self.current_ctx_ref;
|
|
defer self.current_ctx_ref = saved_ctx_ref;
|
|
const user_param_base: u32 = if (wants_ctx) 1 else 0;
|
|
if (wants_ctx) self.current_ctx_ref = Ref.fromIndex(0);
|
|
|
|
for (fd.params, 0..) |p, i| {
|
|
const pty = self.resolveParamType(&p);
|
|
const slot = self.builder.alloca(pty);
|
|
const param_ref = Ref.fromIndex(@intCast(i + user_param_base));
|
|
self.builder.store(slot, param_ref);
|
|
scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true });
|
|
}
|
|
|
|
// Inbound entry points + callconv(.c) sx functions: bind current_ctx_ref
|
|
// to the static default before any user code runs.
|
|
if (!wants_ctx and self.implicit_ctx_enabled) {
|
|
if (self.program_index.global_names.get("__sx_default_context")) |dctx_gi| {
|
|
self.current_ctx_ref = self.builder.emit(.{ .global_addr = dctx_gi.id }, self.module.types.ptrTo(.void));
|
|
}
|
|
}
|
|
|
|
// Lower the function body (set target_type to return type for implicit returns)
|
|
const saved_target = self.target_type;
|
|
self.target_type = if (ret_ty != .void and ret_ty != .noreturn) ret_ty else null;
|
|
if (ret_ty != .void and ret_ty != .noreturn) {
|
|
self.lowerValueBody(fd.body, ret_ty);
|
|
} else {
|
|
// void / noreturn: no value to return — lower as statements and let
|
|
// `ensureTerminator` close the block (ret void / unreachable).
|
|
self.lowerBlock(fd.body);
|
|
self.ensureTerminator(ret_ty);
|
|
}
|
|
self.target_type = saved_target;
|
|
|
|
self.builder.finalize();
|
|
}
|
|
|
|
/// Lower a single function declaration.
|
|
pub fn lowerFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8, is_imported: bool) void {
|
|
// For sx-defined `#objc_class` methods (qualified `<Class>.<method>`),
|
|
// set `current_foreign_class` so `*Self` substitutions through
|
|
// `resolveTypeWithBindings` find the state-struct type (M1.2 A.2b).
|
|
// Save+restore — function lowering can re-enter.
|
|
const saved_fc = self.current_foreign_class;
|
|
defer self.current_foreign_class = saved_fc;
|
|
if (self.lookupObjcDefinedClassForMethod(name)) |fcd| {
|
|
self.current_foreign_class = fcd;
|
|
}
|
|
|
|
const name_id = self.module.types.internString(name);
|
|
const ret_ty = self.resolveReturnType(fd);
|
|
|
|
const wants_ctx = self.funcWantsImplicitCtx(fd);
|
|
|
|
// Build param list. `Function.init` borrows the slice (it does not
|
|
// dupe), so this storage must outlive the local — build it in the
|
|
// module's slice arena (freed at module deinit) rather than via
|
|
// `self.alloc`, which would leak (Function.deinit never frees params).
|
|
const param_alloc = self.module.slice_arena.allocator();
|
|
var params = std.ArrayList(Function.Param).empty;
|
|
if (wants_ctx) {
|
|
params.append(param_alloc, .{
|
|
.name = self.module.types.internString("__sx_ctx"),
|
|
.ty = self.module.types.ptrTo(.void),
|
|
}) catch unreachable;
|
|
}
|
|
for (fd.params) |p| {
|
|
const pty = self.resolveParamType(&p);
|
|
params.append(param_alloc, .{
|
|
.name = self.module.types.internString(p.name),
|
|
.ty = pty,
|
|
}) catch unreachable;
|
|
}
|
|
|
|
// Check if the function body is a builtin or foreign declaration (no body needed)
|
|
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 ──────────────────────────────────────────
|
|
|
|
fn lowerBlock(self: *Lowering, node: *const Node) void {
|
|
switch (node.data) {
|
|
.block => |blk| {
|
|
// Create a child scope for block-level variable shadowing
|
|
var block_scope = Scope.init(self.alloc, self.scope);
|
|
const saved_scope = self.scope;
|
|
self.scope = &block_scope;
|
|
const saved_defer_len = self.defer_stack.items.len;
|
|
defer {
|
|
self.emitBlockDefers(saved_defer_len);
|
|
self.scope = saved_scope;
|
|
block_scope.deinit();
|
|
}
|
|
for (blk.stmts) |stmt| {
|
|
if (self.block_terminated) break;
|
|
self.lowerStmt(stmt);
|
|
// A bare `return`/`raise` mid-block terminates the current
|
|
// basic block but deliberately does NOT set `block_terminated`
|
|
// (that flag would leak past an `if cond { return }` merge
|
|
// block, skipping its trailing statements — see lowerReturn).
|
|
// Stop here so dead statements after the terminator aren't
|
|
// emitted into an already-closed block (invalid LLVM IR).
|
|
if (self.currentBlockHasTerminator()) break;
|
|
}
|
|
},
|
|
else => {
|
|
// Single expression as body (arrow functions)
|
|
self.lowerStmt(node);
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Lower an `inline if` branch — block body emits statements, expression returns value.
|
|
fn lowerInlineBranch(self: *Lowering, node: *const Node) Ref {
|
|
if (node.data == .block) {
|
|
self.lowerBlock(node);
|
|
// A `return` inside the branch terminates the current LLVM block; propagate
|
|
// that up so the enclosing block lowering stops emitting fall-through.
|
|
if (self.currentBlockHasTerminator()) {
|
|
self.block_terminated = true;
|
|
return .none;
|
|
}
|
|
return self.builder.constInt(0, .void);
|
|
}
|
|
return self.lowerExpr(node);
|
|
}
|
|
|
|
/// Lower a block and return the last expression's value (for implicit returns).
|
|
fn lowerBlockValue(self: *Lowering, node: *const Node) ?Ref {
|
|
// Set force_block_value so nested if-else expressions produce values
|
|
const saved = self.force_block_value;
|
|
self.force_block_value = true;
|
|
defer self.force_block_value = saved;
|
|
|
|
switch (node.data) {
|
|
.block => |blk| {
|
|
if (blk.stmts.len == 0) return null;
|
|
// Create a child scope for block-level variable shadowing
|
|
var block_scope = Scope.init(self.alloc, self.scope);
|
|
const saved_scope = self.scope;
|
|
self.scope = &block_scope;
|
|
const saved_defer_len = self.defer_stack.items.len;
|
|
defer {
|
|
self.emitBlockDefers(saved_defer_len);
|
|
self.scope = saved_scope;
|
|
block_scope.deinit();
|
|
}
|
|
// A block whose last statement is `;`-terminated (or not an
|
|
// expression) discards its value: lower every statement as a
|
|
// statement and yield nothing.
|
|
if (!blk.produces_value) {
|
|
self.force_block_value = false;
|
|
for (blk.stmts) |stmt| {
|
|
if (self.block_terminated) return null;
|
|
self.lowerStmt(stmt);
|
|
if (self.currentBlockHasTerminator()) return null;
|
|
}
|
|
return null;
|
|
}
|
|
// Lower all statements except the last normally
|
|
self.force_block_value = false; // don't force for non-last statements
|
|
for (blk.stmts[0 .. blk.stmts.len - 1]) |stmt| {
|
|
if (self.block_terminated) return null;
|
|
self.lowerStmt(stmt);
|
|
// A bare `return`/`raise` mid-block closes the current basic
|
|
// block (without setting `block_terminated`); the remaining
|
|
// statements — including the value-expr — are dead.
|
|
if (self.currentBlockHasTerminator()) return null;
|
|
}
|
|
if (self.block_terminated) return null;
|
|
// Last statement (no trailing `;`): its value is the block's.
|
|
self.force_block_value = true;
|
|
const last = blk.stmts[blk.stmts.len - 1];
|
|
return self.tryLowerAsExpr(last);
|
|
},
|
|
else => {
|
|
// Single expression as body (arrow functions)
|
|
return self.tryLowerAsExpr(node);
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Lower a value-returning function body and emit the implicit return.
|
|
/// Emits a hard error when the body yields no value — its last statement is
|
|
/// `;`-terminated (value discarded) or void — and the body doesn't already
|
|
/// terminate via `return`/`raise`. Replaces the old silent default-return.
|
|
fn lowerValueBody(self: *Lowering, body: *const Node, ret_ty: TypeId) void {
|
|
const body_val = self.lowerBlockValue(body);
|
|
if (self.currentBlockHasTerminator()) return;
|
|
if (body_val) |val| {
|
|
const val_ty = self.builder.getRefType(val);
|
|
if (val_ty != .void) {
|
|
const coerced = self.coerceToType(val, val_ty, ret_ty);
|
|
self.builder.ret(coerced, ret_ty);
|
|
return;
|
|
}
|
|
}
|
|
// A PURE-failable function (`-> !` / `-> !Named`, whose entire return IS
|
|
// the error channel) carries no success value — a void body is a normal
|
|
// success exit, not a missing value. `ensureTerminator` emits the
|
|
// error-slot-zero success return.
|
|
if (self.errorChannelOf(ret_ty)) |chan| {
|
|
if (chan == ret_ty) {
|
|
self.ensureTerminator(ret_ty);
|
|
return;
|
|
}
|
|
}
|
|
if (self.diagnostics) |diags| {
|
|
if (body.data == .block and body.data.block.discarded_semi != null) {
|
|
diags.addFmt(.err, body.data.block.discarded_semi.?, "function returns '{s}' but the last expression's value is discarded by this `;` — drop the `;` to return it (or use an explicit `return`)", .{self.formatTypeName(ret_ty)});
|
|
} else {
|
|
const span = blk: {
|
|
if (body.data == .block) {
|
|
const stmts = body.data.block.stmts;
|
|
if (stmts.len > 0) break :blk stmts[stmts.len - 1].span;
|
|
}
|
|
break :blk body.span;
|
|
};
|
|
diags.addFmt(.err, span, "function returns '{s}' but its body produces no value — end it with a trailing expression (no `;`) or an explicit `return`", .{self.formatTypeName(ret_ty)});
|
|
}
|
|
}
|
|
self.ensureTerminator(ret_ty);
|
|
}
|
|
|
|
/// Try to lower a node as an expression, returning its value.
|
|
/// Statement nodes are lowered as statements (returning null).
|
|
fn tryLowerAsExpr(self: *Lowering, node: *const Node) ?Ref {
|
|
return switch (node.data) {
|
|
.var_decl, .const_decl, .fn_decl, .return_stmt, .raise_stmt, .assignment, .defer_stmt, .push_stmt, .multi_assign, .destructure_decl => {
|
|
self.lowerStmt(node);
|
|
return null;
|
|
},
|
|
else => self.lowerExpr(node),
|
|
};
|
|
}
|
|
|
|
fn lowerStmt(self: *Lowering, node: *const Node) void {
|
|
// Stamp this statement's span onto its instructions (ERR E3.0); see
|
|
// `lowerExpr`.
|
|
const saved_span = self.builder.current_span;
|
|
defer self.builder.current_span = saved_span;
|
|
if (node.span.start != 0 or node.span.end != 0) self.builder.current_span = .{ .start = node.span.start, .end = node.span.end };
|
|
switch (node.data) {
|
|
.var_decl => |vd| self.lowerVarDecl(&vd),
|
|
.const_decl => |cd| self.lowerConstDecl(&cd),
|
|
.fn_decl => |fd| self.lowerLocalFnDecl(&fd),
|
|
.return_stmt => |rs| self.lowerReturn(&rs),
|
|
.raise_stmt => |rs| self.lowerRaise(&rs, node.span),
|
|
.assignment => |asgn| self.lowerAssignment(&asgn),
|
|
.defer_stmt => |ds| self.lowerDefer(&ds),
|
|
.onfail_stmt => |ofs| self.lowerOnFail(&ofs, node.span),
|
|
.push_stmt => |ps| self.lowerPush(&ps),
|
|
.multi_assign => |ma| self.lowerMultiAssign(&ma),
|
|
.destructure_decl => |dd| self.lowerDestructureDecl(&dd),
|
|
.insert_expr => |ins| self.lowerInsertExpr(ins.expr),
|
|
.block => self.lowerBlock(node),
|
|
.jni_env_block => |eb| {
|
|
// Compile-time stack push for lexical-direct env resolution
|
|
// (2.16b — `#jni_call` in the same fn picks up env from
|
|
// jni_env_stack directly, no TL read).
|
|
//
|
|
// Runtime TL save/set/restore (2.16c) for cross-function
|
|
// helpers: callees in OTHER fns invoked from inside the
|
|
// body read the slot via `sx_jni_env_tl_get`. Storage
|
|
// lives in a separately-linked C helper (see
|
|
// library/vendors/sx_jni_runtime/sx_jni_env_tl.c) so the
|
|
// JIT doesn't need orc_rt for TLS.
|
|
const env_ref = self.lowerExpr(eb.env);
|
|
const fids = self.getJniEnvTlFids();
|
|
const ptr_ty = self.module.types.ptrTo(.void);
|
|
const saved_tl = self.builder.emit(.{ .call = .{ .callee = fids.get, .args = &.{} } }, ptr_ty);
|
|
const set_args = self.alloc.dupe(Ref, &.{env_ref}) catch unreachable;
|
|
_ = self.builder.emit(.{ .call = .{ .callee = fids.set, .args = set_args } }, .void);
|
|
self.jni_env_stack.append(self.alloc, env_ref) catch unreachable;
|
|
self.lowerBlock(eb.body);
|
|
_ = self.jni_env_stack.pop();
|
|
const restore_args = self.alloc.dupe(Ref, &.{saved_tl}) catch unreachable;
|
|
_ = self.builder.emit(.{ .call = .{ .callee = fids.set, .args = restore_args } }, .void);
|
|
},
|
|
// Block-local type declarations
|
|
.struct_decl => |sd| {
|
|
self.recordLocalTypeName(sd.name);
|
|
self.registerStructDecl(&node.data.struct_decl, node.source_file orelse self.current_source_file);
|
|
},
|
|
.enum_decl, .union_decl => {
|
|
if (node.data.declName()) |dn| self.recordLocalTypeName(dn);
|
|
_ = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
|
},
|
|
.error_set_decl => {
|
|
if (node.data.declName()) |dn| self.recordLocalTypeName(dn);
|
|
self.registerErrorSetDecl(node);
|
|
},
|
|
.ufcs_alias => |ua| {
|
|
self.program_index.ufcs_alias_map.put(ua.name, ua.target) catch {};
|
|
},
|
|
// Expression statement
|
|
else => {
|
|
_ = self.lowerExpr(node);
|
|
},
|
|
}
|
|
}
|
|
|
|
fn lowerVarDecl(self: *Lowering, vd: *const ast.VarDecl) void {
|
|
if (vd.value) |val| {
|
|
if (val.data == .identifier and self.isPackName(val.data.identifier.name)) {
|
|
const ph = self.diagPackAsValue(val.data.identifier.name, val.span, .storage);
|
|
// Bind the name to the placeholder so later uses don't cascade
|
|
// into a second "unresolved" error after this one.
|
|
if (self.scope) |scope| {
|
|
scope.put(vd.name, .{ .ref = ph, .ty = .unresolved, .is_alloca = false });
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
if (vd.type_annotation) |ta| {
|
|
// Explicit type annotation — resolve type first, then lower value
|
|
const ty = self.resolveType(ta);
|
|
const slot = self.builder.alloca(ty);
|
|
if (vd.value) |val| {
|
|
// = --- (undef_literal) on tuple types: zero-initialize
|
|
if (val.data == .undef_literal and !ty.isBuiltin()) {
|
|
const ti = self.module.types.get(ty);
|
|
if (ti == .tuple) {
|
|
var field_vals = std.ArrayList(Ref).empty;
|
|
defer field_vals.deinit(self.alloc);
|
|
for (ti.tuple.fields) |f| {
|
|
field_vals.append(self.alloc, self.builder.constInt(0, f)) catch unreachable;
|
|
}
|
|
const zero = self.builder.emit(.{
|
|
.tuple_init = .{ .fields = self.alloc.dupe(Ref, field_vals.items) catch unreachable },
|
|
}, ty);
|
|
self.builder.store(slot, zero);
|
|
if (self.scope) |scope| {
|
|
scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true });
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
// A compile-time float initializer narrowing into an integer
|
|
// local follows the unified rule (integral folds, non-integral
|
|
// errors); a runtime float / `xx` cast falls through to the
|
|
// normal lower+coerce below.
|
|
if (self.foldComptimeFloatInit(val, ty)) |folded| {
|
|
self.builder.store(slot, folded);
|
|
if (self.scope) |scope| {
|
|
scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true });
|
|
}
|
|
return;
|
|
}
|
|
const saved_target = self.target_type;
|
|
const saved_fbv = self.force_block_value;
|
|
self.target_type = ty;
|
|
self.force_block_value = true;
|
|
var ref = self.lowerExpr(val);
|
|
self.target_type = saved_target;
|
|
self.force_block_value = saved_fbv;
|
|
// If target is optional and value isn't null, wrap with optional_wrap
|
|
if (!ty.isBuiltin()) {
|
|
const ty_info = self.module.types.get(ty);
|
|
if (ty_info == .optional and val.data != .null_literal) {
|
|
ref = self.builder.optionalWrap(ref, ty);
|
|
} else if (ty_info == .slice) {
|
|
// Array → slice promotion: if value is an array, convert to slice
|
|
const ref_ty = self.builder.getRefType(ref);
|
|
if (!ref_ty.isBuiltin()) {
|
|
const ref_info = self.module.types.get(ref_ty);
|
|
if (ref_info == .array) {
|
|
ref = self.builder.emit(.{ .array_to_slice = .{ .operand = ref } }, ty);
|
|
}
|
|
}
|
|
} else if (self.getProtocolInfo(ty) != null) {
|
|
// Auto type erasure: concrete → protocol
|
|
const ref_ty = self.builder.getRefType(ref);
|
|
if (ref_ty != ty) {
|
|
ref = self.buildProtocolErasure(ref, val, ref_ty, ty);
|
|
}
|
|
}
|
|
}
|
|
// Coerce value to match target type (e.g. u8 → s64 widening)
|
|
{
|
|
const ref_ty = self.builder.getRefType(ref);
|
|
if (ref_ty != ty and ref_ty != .void and ty != .void) {
|
|
ref = self.coerceToType(ref, ref_ty, ty);
|
|
}
|
|
}
|
|
self.builder.store(slot, ref);
|
|
} else {
|
|
// No value: zero-initialize or apply struct defaults
|
|
const zero = self.buildDefaultValue(ty);
|
|
self.builder.store(slot, zero);
|
|
}
|
|
if (self.scope) |scope| {
|
|
scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true });
|
|
}
|
|
} else if (vd.value) |val| {
|
|
// No type annotation — lower expr first, then get type from result.
|
|
// This is critical for generic calls where the return type is only
|
|
// known after monomorphization.
|
|
const saved_fbv = self.force_block_value;
|
|
self.force_block_value = true;
|
|
const ref = self.lowerExpr(val);
|
|
self.force_block_value = saved_fbv;
|
|
const ty = self.builder.getRefType(ref);
|
|
const slot = self.builder.alloca(ty);
|
|
self.builder.store(slot, ref);
|
|
if (self.scope) |scope| {
|
|
scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true });
|
|
}
|
|
} else {
|
|
const ty = TypeId.s64;
|
|
const slot = self.builder.alloca(ty);
|
|
self.builder.store(slot, self.zeroValue(ty));
|
|
if (self.scope) |scope| {
|
|
scope.put(vd.name, .{ .ref = slot, .ty = ty, .is_alloca = true });
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Handle a bare fn_decl node as a local function declaration.
|
|
/// The parser produces `fn_decl` (not `const_decl`) for `name :: (params) -> T { body }`.
|
|
fn lowerLocalFnDecl(self: *Lowering, fd: *const ast.FnDecl) void {
|
|
// Use mangled name for local functions to support block-scoped shadowing
|
|
const name = if (self.scope) |scope| blk: {
|
|
const mangled = std.fmt.allocPrint(self.alloc, "{s}__{d}", .{ fd.name, self.local_fn_counter }) catch fd.name;
|
|
self.local_fn_counter += 1;
|
|
scope.fn_names.put(fd.name, mangled) catch {};
|
|
break :blk mangled;
|
|
} else fd.name;
|
|
self.program_index.fn_ast_map.put(name, fd) catch {};
|
|
self.lazyLowerFunction(name);
|
|
}
|
|
|
|
fn lowerConstDecl(self: *Lowering, cd: *const ast.ConstDecl) void {
|
|
// Handle local function declarations: fx :: (s:s3) -> s3 { ... }
|
|
if (cd.value.data == .fn_decl) {
|
|
const fd = &cd.value.data.fn_decl;
|
|
// Use mangled name for local functions to support block-scoped shadowing
|
|
const name = if (self.scope != null) blk: {
|
|
const mangled = std.fmt.allocPrint(self.alloc, "{s}__{d}", .{ cd.name, self.local_fn_counter }) catch cd.name;
|
|
self.local_fn_counter += 1;
|
|
// Register the bare→mangled mapping in the current scope
|
|
if (self.scope) |scope| {
|
|
scope.fn_names.put(cd.name, mangled) catch {};
|
|
}
|
|
break :blk mangled;
|
|
} else cd.name;
|
|
// Register in fn_ast_map so it can be resolved by lowerCall
|
|
self.program_index.fn_ast_map.put(name, fd) catch {};
|
|
// Lower the function body (saves/restores builder state)
|
|
self.lazyLowerFunction(name);
|
|
return;
|
|
}
|
|
|
|
// Handle local type declarations: MyType :: struct/union/enum { ... }
|
|
if (cd.value.data == .struct_decl) {
|
|
self.recordLocalTypeName(cd.name);
|
|
self.registerStructDecl(&cd.value.data.struct_decl, self.current_source_file);
|
|
return;
|
|
}
|
|
if (cd.value.data == .enum_decl or cd.value.data == .union_decl) {
|
|
self.recordLocalTypeName(cd.name);
|
|
_ = type_bridge.resolveAstType(cd.value, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
|
return;
|
|
}
|
|
|
|
const ref = self.lowerExpr(cd.value);
|
|
// If there's an explicit type annotation, use it. Otherwise, infer from the expression.
|
|
const ty = if (cd.type_annotation) |ta|
|
|
self.resolveType(ta)
|
|
else
|
|
self.builder.getRefType(ref);
|
|
|
|
if (self.scope) |scope| {
|
|
scope.put(cd.name, .{ .ref = ref, .ty = ty, .is_alloca = false });
|
|
}
|
|
}
|
|
|
|
fn lowerReturn(self: *Lowering, rs: *const ast.ReturnStmt) void {
|
|
if (rs.value) |val| {
|
|
if (val.data == .identifier and self.isPackName(val.data.identifier.name)) {
|
|
_ = self.diagPackAsValue(val.data.identifier.name, val.span, .return_value);
|
|
return;
|
|
}
|
|
}
|
|
// Set target_type to function return type so null_literal etc. get the right type.
|
|
// When inlining a comptime body, the *inlined* fn's declared return type wins
|
|
// over the caller's — otherwise `return 42` inside a `-> s64` body lowered into
|
|
// a `-> s32` caller would coerce 42 to s32 before storing into the s64 slot.
|
|
const old_target = self.target_type;
|
|
const ret_ty_for_target: TypeId = if (self.inline_return_target) |iri|
|
|
iri.ret_ty
|
|
else if (self.builder.func) |fid|
|
|
self.module.functions.items[@intFromEnum(fid)].ret
|
|
else
|
|
TypeId.s64;
|
|
// A value-carrying failable (`-> (T..., !)`) returns its VALUE part and
|
|
// the success error slot (0) is appended by lowerFailableSuccessReturn.
|
|
// Resolve a BARE returned value against that value type, NOT the failable
|
|
// tuple: a bare enum literal `.variant` resolves its tag against
|
|
// `target_type`, and against the tuple it matches no variant (tag 0) and
|
|
// is stamped with the tuple type — which the success-return path then
|
|
// mistakes for a forwarded full tuple, dropping the appended `0` slot.
|
|
// An explicit full failable tuple return (`return (v..., e)`) keeps the
|
|
// full-tuple target so its trailing error element resolves against the
|
|
// error set; it is then forwarded as-is. Applies to the inlined
|
|
// comptime-body return path too (iri.ret_ty is the failable tuple there).
|
|
const target_for_value = self.failableReturnTarget(ret_ty_for_target, rs.value);
|
|
if (target_for_value != .void) self.target_type = target_for_value;
|
|
// Evaluate return value first (before defers)
|
|
const ret_val = if (rs.value) |val| self.lowerExpr(val) else null;
|
|
self.target_type = old_target;
|
|
|
|
// Inlined-comptime-body return: store into the slot the inliner
|
|
// gave us and branch to the inliner's "return-done" basic block.
|
|
// The branch is the basic block's terminator — so subsequent
|
|
// dead code in the same block trips the LLVM verifier (the
|
|
// SAME behaviour as a regular `return X;` followed by code).
|
|
//
|
|
// We DO NOT set `block_terminated = true`: that flag would
|
|
// leak past structured control flow (e.g. an `if cond { return
|
|
// X; }` whose merge block continues to subsequent statements)
|
|
// and incorrectly skip the trailing statements. CFG-level
|
|
// termination is what we actually want — let the basic-block
|
|
// terminator do its job.
|
|
if (self.inline_return_target) |iri| {
|
|
if (ret_val) |ref| {
|
|
// Value-carrying failable inlined body: append the success error
|
|
// slot (0) exactly like the real-return path below.
|
|
// lowerFailableSuccessReturn routes through emitTupleRet, which
|
|
// stores into iri.slot and branches to iri.done_bb for an inline
|
|
// target. Defers first, so the returned SSA value is materialized
|
|
// before they run (matching the real-return ordering).
|
|
if (!iri.ret_ty.isBuiltin() and
|
|
self.module.types.get(iri.ret_ty) == .tuple and
|
|
self.errorChannelOf(iri.ret_ty) != null)
|
|
{
|
|
self.emitBlockDefers(self.func_defer_base);
|
|
self.lowerFailableSuccessReturn(ref, iri.ret_ty, rs.value.?.span);
|
|
return;
|
|
}
|
|
const val_ty = self.builder.getRefType(ref);
|
|
const coerced = if (val_ty != iri.ret_ty)
|
|
self.coerceToType(ref, val_ty, iri.ret_ty)
|
|
else
|
|
ref;
|
|
self.builder.store(iri.slot, coerced);
|
|
}
|
|
// Drain block-scoped defers up to the inlined-body base so
|
|
// they fire on this return path the same as a real fn return.
|
|
self.emitBlockDefers(self.func_defer_base);
|
|
self.builder.br(iri.done_bb, &.{});
|
|
return;
|
|
}
|
|
|
|
// Emit ALL pending defers for THIS function in LIFO order before the return
|
|
self.emitBlockDefers(self.func_defer_base);
|
|
|
|
if (ret_val) |ref| {
|
|
const ret_ty = if (self.builder.func) |fid|
|
|
self.module.functions.items[@intFromEnum(fid)].ret
|
|
else
|
|
TypeId.s64;
|
|
if (ret_ty == .void) {
|
|
// Void function — just return void (the value expression was evaluated for side effects)
|
|
self.builder.retVoid();
|
|
} else if (!ret_ty.isBuiltin() and self.module.types.get(ret_ty) == .tuple and self.errorChannelOf(ret_ty) != null) {
|
|
// Value-carrying failable `-> (T..., !)`: the user returns the
|
|
// value part; the compiler appends the success error slot (0).
|
|
self.lowerFailableSuccessReturn(ref, ret_ty, rs.value.?.span);
|
|
} else {
|
|
// Coerce return value to match function return type (e.g., ?s32 → s32)
|
|
const val_ty = self.builder.getRefType(ref);
|
|
const coerced = self.coerceToType(ref, val_ty, ret_ty);
|
|
self.builder.ret(coerced, ret_ty);
|
|
}
|
|
} else {
|
|
// A bare `return;` in a pure failable function (`-> !` / `-> !Named`,
|
|
// whose return type IS the error set) is the success exit — the
|
|
// error slot carries 0 ("no error"). Everything else is a void return.
|
|
const ret_ty = if (self.builder.func) |fid|
|
|
self.module.functions.items[@intFromEnum(fid)].ret
|
|
else
|
|
TypeId.void;
|
|
if (!ret_ty.isBuiltin() and self.module.types.get(ret_ty) == .error_set) {
|
|
self.builder.ret(self.builder.constInt(0, ret_ty), ret_ty);
|
|
} else {
|
|
self.builder.retVoid();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn lowerAssignment(self: *Lowering, asgn: *const ast.Assignment) void {
|
|
// Set target_type from LHS for RHS lowering (enum literals, struct literals, etc.)
|
|
const old_target = self.target_type;
|
|
if (asgn.target.data == .identifier) {
|
|
var found_local = false;
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(asgn.target.data.identifier.name)) |binding| {
|
|
self.target_type = binding.ty;
|
|
found_local = true;
|
|
}
|
|
}
|
|
if (!found_local) {
|
|
if (self.program_index.global_names.get(asgn.target.data.identifier.name)) |gi| {
|
|
self.target_type = gi.ty;
|
|
}
|
|
}
|
|
} else if (asgn.target.data == .index_expr) {
|
|
// For array[i] = val, set target_type to the element type
|
|
const elem_ty = self.getElementType(self.inferExprType(asgn.target.data.index_expr.object));
|
|
if (elem_ty != .void) self.target_type = elem_ty;
|
|
} else if (asgn.target.data == .field_access) {
|
|
// For obj.field = val, set target_type to the field's type so RHS
|
|
// sub-expressions (enum/struct literals, branch arms, xx casts) can
|
|
// resolve against it. Skipped for forms that would forward the type
|
|
// unchanged into method-call arg slots (`resolveCallParamTypes` can't
|
|
// override target_type per-arg).
|
|
const needs_target = switch (asgn.value.data) {
|
|
.enum_literal, .struct_literal, .tuple_literal, .if_expr, .match_expr, .block, .unary_op, .binary_op => true,
|
|
.call => |vc| vc.callee.data == .enum_literal,
|
|
else => false,
|
|
};
|
|
if (needs_target) {
|
|
const fa = asgn.target.data.field_access;
|
|
const obj_ty_raw = self.inferExprType(fa.object);
|
|
const obj_ty = if (!obj_ty_raw.isBuiltin()) blk: {
|
|
const pinfo = self.module.types.get(obj_ty_raw);
|
|
break :blk if (pinfo == .pointer) pinfo.pointer.pointee else obj_ty_raw;
|
|
} else obj_ty_raw;
|
|
if (!obj_ty.isBuiltin()) {
|
|
const field_name_id = self.module.types.internString(fa.field);
|
|
const struct_fields = self.getStructFields(obj_ty);
|
|
for (struct_fields) |f| {
|
|
if (f.name == field_name_id) {
|
|
self.target_type = f.ty;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const val = self.lowerExpr(asgn.value);
|
|
self.target_type = old_target;
|
|
|
|
switch (asgn.target.data) {
|
|
.identifier => |id| {
|
|
var handled = false;
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(id.name)) |binding| {
|
|
if (binding.is_alloca) {
|
|
handled = true;
|
|
if (asgn.op == .assign) {
|
|
// Coerce value to match binding type (e.g., f32 → ?f32, concrete → protocol)
|
|
var store_val = val;
|
|
const val_ty = self.builder.getRefType(val);
|
|
if (val_ty != binding.ty and val_ty != .void and binding.ty != .void) {
|
|
store_val = self.coerceToType(val, val_ty, binding.ty);
|
|
}
|
|
self.builder.store(binding.ref, store_val);
|
|
} else {
|
|
// Compound assignment: load, op, store
|
|
const loaded = self.builder.load(binding.ref, binding.ty);
|
|
const result = self.emitCompoundOp(loaded, val, asgn.op, binding.ty);
|
|
self.builder.store(binding.ref, result);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Fallback: global variable assignment
|
|
if (!handled) {
|
|
if (self.program_index.global_names.get(id.name)) |gi| {
|
|
if (asgn.op == .assign) {
|
|
const val_ty = self.builder.getRefType(val);
|
|
const store_val = if (val_ty != gi.ty and val_ty != .void and gi.ty != .void)
|
|
self.coerceToType(val, val_ty, gi.ty)
|
|
else
|
|
val;
|
|
self.builder.emitVoid(.{ .global_set = .{ .global = gi.id, .value = store_val } }, .void);
|
|
} else {
|
|
// Compound assignment: load current value, apply op, store back
|
|
const loaded = self.builder.emit(.{ .global_get = gi.id }, gi.ty);
|
|
const result = self.emitCompoundOp(loaded, val, asgn.op, gi.ty);
|
|
self.builder.emitVoid(.{ .global_set = .{ .global = gi.id, .value = result } }, .void);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
.field_access => |fa| {
|
|
// M2.2 — `obj.field = val` for an Obj-C `#property` field
|
|
// dispatches via objc_msgSend `setField:`. Skip struct-
|
|
// pointer / GEP entirely; receivers are opaque Obj-C ids.
|
|
// Compound ops on properties are deferred (need load-via-
|
|
// getter + op + store-via-setter — Month 4 ARC territory).
|
|
if (asgn.op == .assign) {
|
|
if (self.lookupObjcPropertyOnPointer(fa.object, fa.field)) |prop| {
|
|
self.lowerObjcPropertySetter(fa.object, prop, val);
|
|
return;
|
|
}
|
|
}
|
|
// M1.2 A.3 — `self.field [op]= val` on a sx-defined Obj-C
|
|
// class instance field (NOT a #property): write through
|
|
// the __sx_state ivar. Handles plain assignment AND
|
|
// compound ops (+=, -=, etc.) via storeOrCompound.
|
|
if (self.lookupObjcDefinedStateFieldOnPointer(fa.object, fa.field)) |info| {
|
|
const obj_ref = self.lowerExpr(fa.object);
|
|
const state_ptr = self.lowerObjcDefinedStateForObj(obj_ref, info.fcd) orelse return;
|
|
const ptr_void = self.module.types.ptrTo(.void);
|
|
const field_addr = self.builder.emit(.{ .struct_gep = .{
|
|
.base = state_ptr,
|
|
.field_index = info.field_idx,
|
|
.base_type = info.state_ty,
|
|
} }, ptr_void);
|
|
self.storeOrCompound(field_addr, val, asgn.op, info.field_ty);
|
|
return;
|
|
}
|
|
|
|
var obj_ptr = self.lowerExprAsPtr(fa.object);
|
|
var obj_ty = self.inferExprType(fa.object);
|
|
// Auto-deref: if the object is a pointer field from a non-identifier
|
|
// (i.e., result of structGep on a pointer slot), load the pointer value.
|
|
if (fa.object.data != .identifier and !obj_ty.isBuiltin()) {
|
|
const pinfo = self.module.types.get(obj_ty);
|
|
if (pinfo == .pointer) {
|
|
obj_ptr = self.builder.load(obj_ptr, obj_ty);
|
|
obj_ty = pinfo.pointer.pointee;
|
|
}
|
|
}
|
|
|
|
// Special .len/.ptr handling only for slices, strings, arrays — NOT structs
|
|
const is_special_container = obj_ty == .string or (if (!obj_ty.isBuiltin()) blk: {
|
|
const obj_info = self.module.types.get(obj_ty);
|
|
break :blk obj_info == .slice or obj_info == .array or obj_info == .vector;
|
|
} else false);
|
|
|
|
if (is_special_container and std.mem.eql(u8, fa.field, "len")) {
|
|
const gep = self.builder.structGepTyped(obj_ptr, 1, .s64, obj_ty);
|
|
self.storeOrCompound(gep, val, asgn.op, .s64);
|
|
} else if (is_special_container and std.mem.eql(u8, fa.field, "ptr")) {
|
|
const gep = self.builder.structGepTyped(obj_ptr, 0, .s64, obj_ty);
|
|
self.storeOrCompound(gep, val, asgn.op, .s64);
|
|
} else if (self.fieldLvaluePtr(obj_ptr, obj_ty, fa.field)) |fl| {
|
|
// Resolve the target field (struct / union direct / promoted
|
|
// anonymous-struct member / tuple element / vector lane) via
|
|
// the shared lvalue resolver — the same one the address-of
|
|
// and multi-target store paths use — so the three never
|
|
// resolve a field to a different slot or default field 0
|
|
// (issue 0094 / issue-0083 two-resolver class). fl.ptr is
|
|
// *field_ty (the store handler unwraps one pointer level);
|
|
// fl.ty is the value type to coerce the rhs to.
|
|
const src_ty = self.builder.getRefType(val);
|
|
const coerced = self.coerceToType(val, src_ty, fl.ty);
|
|
self.storeOrCompound(fl.ptr, coerced, asgn.op, fl.ty);
|
|
} else {
|
|
// No struct / union / tuple / vector field matches the
|
|
// assignment target. Emit the same field-not-found
|
|
// diagnostic the read path uses (emitFieldError) and bail;
|
|
// building a pointer with field_ty = .unresolved would
|
|
// otherwise store through a pointer-to-.unresolved that
|
|
// panics at LLVM emission (issue 0094).
|
|
_ = self.emitFieldError(obj_ty, fa.field, asgn.target.span);
|
|
}
|
|
},
|
|
.index_expr => |ie| {
|
|
const idx = self.lowerExpr(ie.index);
|
|
const obj_ty = self.inferExprType(ie.object);
|
|
const elem_ty = self.getElementType(obj_ty);
|
|
const ptr_ty = self.module.types.ptrTo(elem_ty);
|
|
// For fixed-size array assignment targets, use the alloca pointer directly
|
|
// so that the store modifies the original variable (not a loaded copy).
|
|
const is_array = !obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .array;
|
|
const obj_alloca = if (is_array) self.getExprAlloca(ie.object) else null;
|
|
if (obj_alloca) |alloca_ref| {
|
|
// Array alloca: single-index GEP with element stride
|
|
const gep = self.builder.emit(.{ .index_gep = .{ .lhs = alloca_ref, .rhs = idx } }, ptr_ty);
|
|
self.storeOrCompound(gep, val, asgn.op, elem_ty);
|
|
} else if (is_array) {
|
|
// Array in a struct field or other composite: get pointer to array in-place
|
|
const obj_ptr = self.lowerExprAsPtr(ie.object);
|
|
const gep = self.builder.emit(.{ .index_gep = .{ .lhs = obj_ptr, .rhs = idx } }, ptr_ty);
|
|
self.storeOrCompound(gep, val, asgn.op, elem_ty);
|
|
} else {
|
|
// Pointer/slice: load the pointer value and GEP
|
|
const obj = self.lowerExpr(ie.object);
|
|
const gep = self.builder.emit(.{ .index_gep = .{ .lhs = obj, .rhs = idx } }, ptr_ty);
|
|
self.storeOrCompound(gep, val, asgn.op, elem_ty);
|
|
}
|
|
},
|
|
.deref_expr => |de| {
|
|
const ptr = self.lowerExpr(de.operand);
|
|
if (asgn.op == .assign) {
|
|
const pointee_ty = blk: {
|
|
const ptr_ty = self.inferExprType(de.operand);
|
|
if (!ptr_ty.isBuiltin()) {
|
|
const info = self.module.types.get(ptr_ty);
|
|
if (info == .pointer) break :blk info.pointer.pointee;
|
|
}
|
|
break :blk ptr_ty;
|
|
};
|
|
const val_ty = self.builder.getRefType(val);
|
|
const store_val = if (val_ty != pointee_ty and val_ty != .void and pointee_ty != .void)
|
|
self.coerceToType(val, val_ty, pointee_ty)
|
|
else
|
|
val;
|
|
self.builder.store(ptr, store_val);
|
|
} else {
|
|
const pointee_ty = self.inferExprType(de.operand);
|
|
const elem_ty = blk: {
|
|
if (!pointee_ty.isBuiltin()) {
|
|
const info = self.module.types.get(pointee_ty);
|
|
if (info == .pointer) break :blk info.pointer.pointee;
|
|
}
|
|
break :blk pointee_ty;
|
|
};
|
|
self.storeOrCompound(ptr, val, asgn.op, elem_ty);
|
|
}
|
|
},
|
|
else => {
|
|
_ = self.emitError("assignment_target", asgn.target.span);
|
|
},
|
|
}
|
|
}
|
|
|
|
const FieldLvalue = struct { ptr: Ref, ty: TypeId };
|
|
|
|
/// Resolve `obj.field` — where `obj_ptr` already points at the aggregate —
|
|
/// to a typed pointer into the field's storage plus the field's value type.
|
|
/// Handles union direct fields, promoted anonymous-struct union members,
|
|
/// tuple elements (numeric or named), vector lanes (`.x`/`.y`/`.z`/`.w` and
|
|
/// the colour aliases), and plain struct fields. Returns null when no field
|
|
/// matches; the caller emits the field-not-found diagnostic.
|
|
///
|
|
/// `ptr`'s IR type is `*field_ty` (a pointer to the field), NOT the field
|
|
/// value type: `emitStore` reads the store-target pointer's IR type and
|
|
/// unwraps one `.pointer` level to find the stored value's type. Labelling
|
|
/// the GEP with the bare field type instead would make a field whose own
|
|
/// type is a pointer-to-aggregate (`*Pair`) coerce the stored pointer into
|
|
/// the aggregate (closure auto-promotion in `coerceArg`), storing an
|
|
/// oversized struct that clobbers the neighbouring field. `.ty` carries the
|
|
/// field's value type for the caller's coercion.
|
|
///
|
|
/// Single source of lvalue field resolution shared by all three store/
|
|
/// address-of sites — lowerAssignment (single-target store), lowerExprAsPtr
|
|
/// (address-of), and lowerMultiAssign (multi-target store) — so they never
|
|
/// resolve a field to a different slot or default field 0 (issue 0094).
|
|
fn fieldLvaluePtr(self: *Lowering, obj_ptr: Ref, obj_ty: TypeId, field: []const u8) ?FieldLvalue {
|
|
if (obj_ty.isBuiltin()) return null;
|
|
const field_name_id = self.module.types.internString(field);
|
|
const type_info = self.module.types.get(obj_ty);
|
|
|
|
// Union / tagged-union: variants overlay at offset 0. A direct field is
|
|
// a union_gep; a promoted anonymous-struct member is a union_gep into
|
|
// the variant followed by a struct_gep into the member.
|
|
const union_fields: ?[]const types.TypeInfo.StructInfo.Field = switch (type_info) {
|
|
.@"union" => |u| u.fields,
|
|
.tagged_union => |u| u.fields,
|
|
else => null,
|
|
};
|
|
if (union_fields) |fields| {
|
|
for (fields, 0..) |f, i| {
|
|
if (f.name == field_name_id) {
|
|
const ptr = self.builder.emit(.{ .union_gep = .{ .base = obj_ptr, .field_index = @intCast(i), .base_type = obj_ty } }, self.module.types.ptrTo(f.ty));
|
|
return .{ .ptr = ptr, .ty = f.ty };
|
|
}
|
|
if (!f.ty.isBuiltin()) {
|
|
const fi = self.module.types.get(f.ty);
|
|
if (fi == .@"struct") {
|
|
for (fi.@"struct".fields, 0..) |sf, si| {
|
|
if (sf.name == field_name_id) {
|
|
const ug = self.builder.emit(.{ .union_gep = .{ .base = obj_ptr, .field_index = @intCast(i), .base_type = obj_ty } }, self.module.types.ptrTo(f.ty));
|
|
const ptr = self.builder.structGepTyped(ug, @intCast(si), self.module.types.ptrTo(sf.ty), f.ty);
|
|
return .{ .ptr = ptr, .ty = sf.ty };
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Tuple element: `.0` (numeric) or `.name`.
|
|
if (type_info == .tuple) {
|
|
const tup = type_info.tuple;
|
|
var elem_idx: ?usize = null;
|
|
if (std.fmt.parseInt(usize, field, 10)) |n| {
|
|
if (n < tup.fields.len) elem_idx = n;
|
|
} else |_| {
|
|
if (tup.names) |names| {
|
|
for (names, 0..) |nm, i| {
|
|
if (nm == field_name_id and i < tup.fields.len) {
|
|
elem_idx = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (elem_idx) |idx| {
|
|
const elem_ty = tup.fields[idx];
|
|
const ptr = self.builder.structGepTyped(obj_ptr, @intCast(idx), self.module.types.ptrTo(elem_ty), obj_ty);
|
|
return .{ .ptr = ptr, .ty = elem_ty };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Vector lane: `.x`/`.y`/`.z`/`.w` (or colour aliases `.r`/`.g`/`.b`/`.a`)
|
|
// → lane 0/1/2/3 via the same vectorLaneIndex the read path uses. A
|
|
// non-lane field on a vector is a genuine miss (caller diagnoses).
|
|
if (type_info == .vector) {
|
|
const vidx = Lowering.vectorLaneIndex(field) orelse return null;
|
|
const elem_ty = type_info.vector.element;
|
|
const ptr = self.builder.structGepTyped(obj_ptr, vidx, self.module.types.ptrTo(elem_ty), obj_ty);
|
|
return .{ .ptr = ptr, .ty = elem_ty };
|
|
}
|
|
|
|
// Plain struct field.
|
|
const struct_fields = self.getStructFields(obj_ty);
|
|
for (struct_fields, 0..) |f, i| {
|
|
if (f.name == field_name_id) {
|
|
const ptr = self.builder.structGepTyped(obj_ptr, @intCast(i), self.module.types.ptrTo(f.ty), obj_ty);
|
|
return .{ .ptr = ptr, .ty = f.ty };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Get the pointer (alloca ref) for an lvalue expression, without loading.
|
|
fn lowerExprAsPtr(self: *Lowering, node: *const Node) Ref {
|
|
switch (node.data) {
|
|
.identifier => |id| {
|
|
const local = if (self.scope) |scope| scope.lookup(id.name) else null;
|
|
if (local) |binding| {
|
|
if (binding.is_alloca) {
|
|
// If the variable IS a pointer (e.g., p: *Vec2), load it
|
|
// to get the actual pointer value for GEP/store operations
|
|
if (!binding.ty.isBuiltin()) {
|
|
const info = self.module.types.get(binding.ty);
|
|
if (info == .pointer) {
|
|
return self.builder.load(binding.ref, binding.ty);
|
|
}
|
|
}
|
|
return binding.ref;
|
|
}
|
|
} else if (self.program_index.global_names.get(id.name)) |gi| {
|
|
// Module-global lvalue: address into the global's live storage
|
|
// so a downstream GEP/store targets the global itself, not a
|
|
// loaded copy. A pointer-typed global is loaded first to get
|
|
// the pointer value to GEP through (mirrors the local pointer
|
|
// case above); any other global yields its storage address.
|
|
if (!gi.ty.isBuiltin() and self.module.types.get(gi.ty) == .pointer) {
|
|
return self.builder.emit(.{ .global_get = gi.id }, gi.ty);
|
|
}
|
|
return self.builder.emit(.{ .global_addr = gi.id }, self.module.types.ptrTo(gi.ty));
|
|
}
|
|
},
|
|
.field_access => |fa| {
|
|
var obj_ptr = self.lowerExprAsPtr(fa.object);
|
|
var obj_ty = self.inferExprType(fa.object);
|
|
// Auto-deref for chained pointer field access:
|
|
// When fa.object is a field_access or index_expr, lowerExprAsPtr returns
|
|
// a structGep/pointer to the slot. If the slot holds a pointer type,
|
|
// we need to load the pointer value before GEPing into the pointee struct.
|
|
// (Identifiers are already loaded by the identifier handler in lowerExprAsPtr.)
|
|
if (fa.object.data != .identifier and !obj_ty.isBuiltin()) {
|
|
const info = self.module.types.get(obj_ty);
|
|
if (info == .pointer) {
|
|
obj_ptr = self.builder.load(obj_ptr, obj_ty);
|
|
obj_ty = info.pointer.pointee;
|
|
}
|
|
}
|
|
// Resolve the field lvalue (struct / union direct / promoted
|
|
// anonymous-struct member / tuple element) via the shared
|
|
// resolver so address-of and the multi-target store path never
|
|
// disagree on the slot. No match → emit the read path's
|
|
// field-not-found diagnostic (lowerFieldAccessOnType →
|
|
// emitFieldError) instead of silently GEPing field 0 as .s64;
|
|
// that bogus pointer reaches LLVM emission as ptrTo(.unresolved)
|
|
// and panics (issue 0094).
|
|
if (self.fieldLvaluePtr(obj_ptr, obj_ty, fa.field)) |r| return r.ptr;
|
|
return self.emitFieldError(obj_ty, fa.field, node.span);
|
|
},
|
|
.index_expr => |ie| {
|
|
const idx = self.lowerExpr(ie.index);
|
|
const obj_ty = self.inferExprType(ie.object);
|
|
const elem_ty = self.getElementType(obj_ty);
|
|
const ptr_ty = self.module.types.ptrTo(elem_ty);
|
|
// For fixed-size arrays, use the alloca so GEP addresses the original memory
|
|
const is_array = !obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .array;
|
|
const base = if (is_array)
|
|
(self.getExprAlloca(ie.object) orelse self.lowerExprAsPtr(ie.object))
|
|
else
|
|
self.lowerExpr(ie.object);
|
|
return self.builder.emit(.{ .index_gep = .{ .lhs = base, .rhs = idx } }, ptr_ty);
|
|
},
|
|
.deref_expr => |de| {
|
|
return self.lowerExpr(de.operand);
|
|
},
|
|
else => {},
|
|
}
|
|
// Fallback: lower as expression (may produce a value, not pointer)
|
|
return self.lowerExpr(node);
|
|
}
|
|
|
|
/// Store a value to a GEP, handling both plain and compound assignment.
|
|
fn storeOrCompound(self: *Lowering, gep: Ref, val: Ref, op: ast.Assignment.Op, ty: TypeId) void {
|
|
if (op == .assign) {
|
|
const val_ty = self.builder.getRefType(val);
|
|
const store_val = if (val_ty != ty and val_ty != .void and ty != .void)
|
|
self.coerceToType(val, val_ty, ty)
|
|
else
|
|
val;
|
|
self.builder.store(gep, store_val);
|
|
} else {
|
|
const loaded = self.builder.load(gep, ty);
|
|
const result = self.emitCompoundOp(loaded, val, op, ty);
|
|
self.builder.store(gep, result);
|
|
}
|
|
}
|
|
|
|
fn emitCompoundOp(self: *Lowering, lhs: Ref, rhs: Ref, op: ast.Assignment.Op, ty: TypeId) Ref {
|
|
return switch (op) {
|
|
.add_assign => self.builder.add(lhs, rhs, ty),
|
|
.sub_assign => self.builder.sub(lhs, rhs, ty),
|
|
.mul_assign => self.builder.mul(lhs, rhs, ty),
|
|
.div_assign => self.builder.div(lhs, rhs, ty),
|
|
.mod_assign => self.builder.emit(.{ .mod = .{ .lhs = lhs, .rhs = rhs } }, ty),
|
|
.and_assign => self.builder.emit(.{ .bit_and = .{ .lhs = lhs, .rhs = rhs } }, ty),
|
|
.or_assign => self.builder.emit(.{ .bit_or = .{ .lhs = lhs, .rhs = rhs } }, ty),
|
|
.xor_assign => self.builder.emit(.{ .bit_xor = .{ .lhs = lhs, .rhs = rhs } }, ty),
|
|
.shl_assign => self.builder.emit(.{ .shl = .{ .lhs = lhs, .rhs = rhs } }, ty),
|
|
.shr_assign => self.builder.emit(.{ .shr = .{ .lhs = lhs, .rhs = rhs } }, ty),
|
|
else => self.emitError("compound_assign", null),
|
|
};
|
|
}
|
|
|
|
// ── Expression lowering ─────────────────────────────────────────
|
|
|
|
fn lowerExpr(self: *Lowering, node: *const Node) Ref {
|
|
// Stamp this node's source span onto the instructions it emits (ERR
|
|
// E3.0 — feeds DWARF line-info + comptime frame resolution). Save/
|
|
// restore so a parent's later emits keep the parent's span after a
|
|
// child lowers. Skip the empty default so synthetic nodes don't reset
|
|
// a meaningful enclosing span to offset 0.
|
|
const saved_span = self.builder.current_span;
|
|
defer self.builder.current_span = saved_span;
|
|
if (node.span.start != 0 or node.span.end != 0) self.builder.current_span = .{ .start = node.span.start, .end = node.span.end };
|
|
// A node carrying an explicit `source_file` is one spliced into a body
|
|
// from another module — a substituted caller comptime-`$`-arg (stamped
|
|
// at the `cpn` build site in lowerComptimeCall / monomorphizePackFn).
|
|
// Resolve its bare names in THAT module's visibility context, overriding
|
|
// the body's defining-module pin, then restore so sibling callee nodes
|
|
// keep the enclosing context. Ordinary expression nodes never carry a
|
|
// `source_file`, so this is a no-op on the hot path.
|
|
const restore_source = node.source_file != null;
|
|
const saved_source = self.current_source_file;
|
|
if (node.source_file) |sf| self.setCurrentSourceFile(sf);
|
|
defer if (restore_source) self.setCurrentSourceFile(saved_source);
|
|
return switch (node.data) {
|
|
// Bare `$<pack>` in expression position → an `[]Type` slice
|
|
// value where each element is a `const_type(arg_types[i])`.
|
|
// Per `Type → .any` mapping in type_bridge, the IR slice
|
|
// type is `[]Any`; the interp stores raw `.type_tag` Values
|
|
// (NOT Any-boxed) so `args[i]` reads back as a Type value
|
|
// directly. Step 4 final slice — lets builder fns walk the
|
|
// whole pack at interp time.
|
|
.comptime_pack_ref => |cpr| blk: {
|
|
// `$<name>` is overloaded in expression position:
|
|
// - Inside a pack-fn mono (or a `tryPackImplMatch`
|
|
// impl mono), `name` is a pack binding → slice of
|
|
// element types (`[]Type` lowered as `[]Any`).
|
|
// - Inside an impl mono whose impl pattern bound a
|
|
// single-type generic (`$R: Type` in
|
|
// `Closure(..$args) -> $R`), `name` is in
|
|
// `type_bindings` → single `const_type(R)` value.
|
|
// Pack arg types are checked first (the slice form),
|
|
// then pack_bindings (the impl-mono mirror), then
|
|
// type_bindings (single-type binding); only if all
|
|
// miss is it a real "outside an active binding" error.
|
|
if (self.pack_arg_types) |pat| {
|
|
if (pat.get(cpr.pack_name)) |arg_tys| {
|
|
break :blk self.buildPackSliceValue(arg_tys);
|
|
}
|
|
}
|
|
if (self.pack_bindings) |pb| {
|
|
if (pb.get(cpr.pack_name)) |arg_tys| {
|
|
break :blk self.buildPackSliceValue(arg_tys);
|
|
}
|
|
}
|
|
if (self.type_bindings) |tb| {
|
|
if (tb.get(cpr.pack_name)) |ty| {
|
|
break :blk self.builder.constType(ty);
|
|
}
|
|
}
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, node.span, "pack reference ${s} used outside an active pack binding", .{cpr.pack_name});
|
|
}
|
|
break :blk self.builder.constNull(self.module.types.sliceOf(.any));
|
|
},
|
|
// Pack-index in expression position: `$<pack>[<lit>]` →
|
|
// `const_type(arg_types[index])`. Yields a comptime-only
|
|
// Type value (`Value.type_tag(TypeId)` in the interp).
|
|
// OOB / no-active-pack-binding → focused diagnostic; the
|
|
// emitted Ref is a const_type(.void) placeholder so the
|
|
// verifier downstream catches misuse rather than silently
|
|
// succeeding with .void.
|
|
.pack_index_type_expr => |pi| blk: {
|
|
if (self.pack_arg_types) |pat| {
|
|
if (pat.get(pi.pack_name)) |arg_tys| {
|
|
if (pi.index < arg_tys.len) {
|
|
break :blk self.builder.constType(arg_tys[pi.index]);
|
|
}
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, node.span, "pack-index value ${s}[{}] out of bounds: '{s}' has {} element{s}", .{
|
|
pi.pack_name, pi.index, pi.pack_name, arg_tys.len,
|
|
if (arg_tys.len == 1) @as([]const u8, "") else @as([]const u8, "s"),
|
|
});
|
|
}
|
|
break :blk self.builder.constType(.void);
|
|
}
|
|
}
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, node.span, "pack-index value ${s}[{}] used outside an active pack binding", .{
|
|
pi.pack_name, pi.index,
|
|
});
|
|
}
|
|
break :blk self.builder.constType(.void);
|
|
},
|
|
.int_literal => |lit| {
|
|
// If target is a float type, emit as float literal
|
|
if (self.target_type) |tt| {
|
|
if (tt == .f32 or tt == .f64) {
|
|
return self.builder.constFloat(@floatFromInt(lit.value), tt);
|
|
}
|
|
}
|
|
const ty = if (self.target_type) |tt| blk: {
|
|
break :blk if (self.isIntEx(tt)) tt else .s64;
|
|
} else .s64;
|
|
return self.builder.constInt(lit.value, ty);
|
|
},
|
|
.float_literal => |lit| {
|
|
const fty: TypeId = if (self.target_type) |tt| (if (tt == .f32 or tt == .f64) tt else .f64) else .f64;
|
|
return self.builder.constFloat(lit.value, fty);
|
|
},
|
|
.bool_literal => |lit| self.builder.constBool(lit.value),
|
|
.string_literal => |lit| blk: {
|
|
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);
|
|
break :blk self.builder.constString(sid);
|
|
},
|
|
// A bare `null` / `---` with no surrounding type expectation is a
|
|
// legitimate typeless literal, not a failed lookup: `.void` is its
|
|
// intentional default (emitConstNull/emitConstUndef handle void as
|
|
// null-ptr / undef-i64). Not a candidate for the `.unresolved` tripwire.
|
|
.null_literal => self.builder.constNull(self.target_type orelse .void),
|
|
.undef_literal => self.builder.constUndef(self.target_type orelse .void),
|
|
|
|
.identifier => |id| blk: {
|
|
// A bare pack name in value position has no runtime
|
|
// representation (Decision 1). Projections (`xs.len`, `xs[i]`,
|
|
// `xs.value`) are field/index nodes handled elsewhere, so a bare
|
|
// `xs` reaching here is always a pack-as-value misuse.
|
|
if (self.isPackName(id.name)) {
|
|
break :blk self.diagPackAsValue(id.name, node.span, .generic);
|
|
}
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(id.name)) |binding| {
|
|
if (binding.is_alloca) {
|
|
break :blk self.builder.load(binding.ref, binding.ty);
|
|
}
|
|
break :blk binding.ref;
|
|
}
|
|
}
|
|
// Check compile-time constants (OS, ARCH, POINTER_SIZE) before globals
|
|
if (self.comptime_constants.get(id.name)) |cv| {
|
|
switch (cv) {
|
|
.int_val => |iv| break :blk self.builder.constInt(iv, .s64),
|
|
.enum_tag => |et| break :blk self.builder.constInt(@intCast(et.tag), et.ty),
|
|
}
|
|
}
|
|
// `context` resolves to a load through the lowering's
|
|
// current `__sx_ctx` pointer. Every sx function (and
|
|
// every `push Context.{...}` body) sets `current_ctx_ref`
|
|
// to a `*Context` it owns, so this is one indirection.
|
|
if (std.mem.eql(u8, id.name, "context")) {
|
|
if (!self.implicit_ctx_enabled or self.current_ctx_ref == Ref.none) {
|
|
break :blk self.diagnoseMissingContext("the `context` identifier");
|
|
}
|
|
const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse {
|
|
break :blk self.diagnoseMissingContext("the `context` identifier");
|
|
};
|
|
break :blk self.builder.load(self.current_ctx_ref, ctx_ty);
|
|
}
|
|
// Check globals (#run constants)
|
|
if (self.program_index.global_names.get(id.name)) |gi| {
|
|
break :blk self.builder.emit(.{ .global_get = gi.id }, gi.ty);
|
|
}
|
|
// Check module-level value constants (e.g. AF_INET :s32: 2)
|
|
if (self.program_index.module_const_map.get(id.name)) |ci| {
|
|
if (!self.isNameVisible(id.name)) {
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, node.span, "'{s}' is not visible; #import the module that declares it", .{id.name});
|
|
break :blk self.emitError(id.name, node.span);
|
|
}
|
|
break :blk self.emitModuleConst(ci);
|
|
}
|
|
// Check if it's a function name — produce function pointer reference
|
|
// Resolve mangled name for block-local functions
|
|
const eff_fn_name = if (self.scope) |scope| scope.lookupFn(id.name) orelse id.name else id.name;
|
|
if (self.program_index.fn_ast_map.contains(eff_fn_name)) {
|
|
// Visibility check only for user-typed bare names (id.name
|
|
// == eff_fn_name) without a UFCS alias. Mangled local-
|
|
// scope names and UFCS rewrites are compiler indirections
|
|
// and stay exempt.
|
|
if (std.mem.eql(u8, eff_fn_name, id.name) and
|
|
self.program_index.ufcs_alias_map.get(id.name) == null and
|
|
!self.isNameVisible(eff_fn_name))
|
|
{
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, node.span, "'{s}' is not visible; #import the module that declares it", .{eff_fn_name});
|
|
break :blk self.emitError(eff_fn_name, node.span);
|
|
}
|
|
// Type-as-value: if target is Any (Type variable), produce a type name string
|
|
if (self.target_type == .any) {
|
|
const fd = self.program_index.fn_ast_map.get(eff_fn_name).?;
|
|
const fn_type_str = self.formatFnTypeString(fd);
|
|
const sid = self.module.types.internString(fn_type_str);
|
|
const str = self.builder.constString(sid);
|
|
break :blk self.builder.boxAny(str, .string);
|
|
}
|
|
// fix-0102d site 2: taking a bare same-name fn as a VALUE
|
|
// (func_ref, fn-ptr / closure coercion) must capture the
|
|
// RESOLVED author's FuncId for a genuine flat collision, not
|
|
// the first-wins winner's. Plain bare name only; `.ambiguous`
|
|
// → loud diagnostic; `.none` → existing first-wins path. The
|
|
// winner is lazily lowered ONLY on `.none` — a rerouted value
|
|
// never uses the winner, so its body must not be lowered.
|
|
const value_fid: ?FuncId = blk_fv: {
|
|
if (std.mem.eql(u8, eff_fn_name, id.name) and
|
|
self.program_index.ufcs_alias_map.get(id.name) == null and
|
|
(if (self.scope) |scope| scope.lookup(id.name) == null else true))
|
|
{
|
|
if (self.current_source_file) |caller_file| {
|
|
switch (self.selectPlainCallableAuthor(id.name, caller_file)) {
|
|
.func => |sf| {
|
|
var selected = sf;
|
|
break :blk_fv self.selectedFuncId(&selected, id.name);
|
|
},
|
|
.ambiguous => {
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, node.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{id.name});
|
|
break :blk self.emitError(id.name, node.span);
|
|
},
|
|
.none => {},
|
|
}
|
|
}
|
|
}
|
|
if (!self.lowered_functions.contains(eff_fn_name)) {
|
|
self.lazyLowerFunction(eff_fn_name);
|
|
}
|
|
break :blk_fv self.resolveFuncByName(eff_fn_name);
|
|
};
|
|
if (value_fid) |fid| {
|
|
// Auto-promote bare function → closure when target_type is closure
|
|
if (self.target_type) |tt| {
|
|
if (!tt.isBuiltin()) {
|
|
const tt_info = self.module.types.get(tt);
|
|
if (tt_info == .closure) {
|
|
const tramp_id = self.createBareFnTrampoline(fid, tt_info.closure);
|
|
break :blk self.builder.closureCreate(tramp_id, Ref.none, tt);
|
|
}
|
|
// Coercing a bare fn name to a fn-pointer
|
|
// type — the call_conv must match. A
|
|
// default-conv sx fn assigned to a
|
|
// callconv(.c) slot (e.g. passed to
|
|
// pthread_create) would otherwise crash at
|
|
// runtime when the C caller doesn't supply
|
|
// the implicit __sx_ctx arg.
|
|
if (tt_info == .function) {
|
|
const func_cc = self.module.functions.items[@intFromEnum(fid)].call_conv;
|
|
if (func_cc != tt_info.function.call_conv) {
|
|
if (self.diagnostics) |d| {
|
|
const want_cc = if (tt_info.function.call_conv == .c) "callconv(.c)" else "default sx convention";
|
|
const have_cc = if (func_cc == .c) "callconv(.c)" else "default sx convention";
|
|
d.addFmt(.err, node.span, "call-convention mismatch: '{s}' is declared with {s} but the target type expects {s}", .{ eff_fn_name, have_cc, want_cc });
|
|
}
|
|
break :blk self.emitPlaceholder(eff_fn_name);
|
|
}
|
|
}
|
|
// NOTE: `xx <sx_fn> : *void` (e.g.
|
|
// `class_addMethod(_, _, xx my_imp, _)`)
|
|
// is intentionally NOT diagnosed here.
|
|
// Manually-constructed Closure values
|
|
// legitimately store default-conv sx fns
|
|
// into a `*void` slot for sx-side dispatch
|
|
// through the closure trampoline ABI. The
|
|
// compiler can't distinguish C-side vs
|
|
// sx-side use from the cast alone.
|
|
// examples/50-smoke.sx has both shapes.
|
|
}
|
|
}
|
|
break :blk self.builder.emit(.{ .func_ref = fid }, .s64);
|
|
}
|
|
}
|
|
// Type-as-value: a name that resolves to a TypeId
|
|
// (primitive, alias, registered struct/enum/union,
|
|
// generic-struct instantiation) evaluates to a
|
|
// `const_type` in expression position. Works for
|
|
// direct assignment to a `Type`-typed slot
|
|
// (`x: Type = Vec4`), comparison (`x == Vec4`), and
|
|
// pack-arg / Any context (boxing happens at the
|
|
// consumer).
|
|
const ty = blk_ty: {
|
|
if (self.type_bindings) |tb| {
|
|
if (tb.get(id.name)) |t| break :blk_ty t;
|
|
}
|
|
if (self.program_index.type_alias_map.get(id.name)) |t| break :blk_ty t;
|
|
if (type_bridge.resolveTypePrimitive(id.name)) |t| break :blk_ty t;
|
|
const name_id = self.module.types.internString(id.name);
|
|
if (self.module.types.findByName(name_id)) |t| break :blk_ty t;
|
|
break :blk_ty TypeId.void;
|
|
};
|
|
if (ty != .void) {
|
|
break :blk self.builder.constType(ty);
|
|
}
|
|
// Unknown identifier
|
|
break :blk self.emitError(id.name, node.span);
|
|
},
|
|
|
|
.binary_op => |bop| self.lowerBinaryOp(&bop),
|
|
|
|
.unary_op => |uop| blk: {
|
|
// `xx <pack>` with a slice target materializes the comptime
|
|
// pack into a runtime `[]elem` (issue 0053). Must run before the
|
|
// operand is lowered (a bare pack name otherwise hits the
|
|
// pack-as-value error).
|
|
if (uop.op == .xx and uop.operand.data == .identifier and self.isPackName(uop.operand.data.identifier.name)) {
|
|
const pname = uop.operand.data.identifier.name;
|
|
if (self.target_type) |tt| {
|
|
if (!tt.isBuiltin() and self.module.types.get(tt) == .slice) {
|
|
break :blk self.lowerPackToSlice(pname, tt);
|
|
}
|
|
}
|
|
break :blk self.diagPackAsValue(pname, node.span, .generic);
|
|
}
|
|
// address_of(index_expr) → emit index_gep (pointer to element) instead of index_get + addr_of
|
|
if (uop.op == .address_of and uop.operand.data == .index_expr) {
|
|
const ie = &uop.operand.data.index_expr;
|
|
const idx = self.lowerExpr(ie.index);
|
|
const obj_ty = self.inferExprType(ie.object);
|
|
const elem_ty = self.getElementType(obj_ty);
|
|
const ptr_ty = self.module.types.ptrTo(elem_ty);
|
|
// For array targets, use the storage pointer (alloca for a
|
|
// local, global_addr for a module global) so the resulting
|
|
// pointer is into live storage, not a loaded copy.
|
|
const is_array = !obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .array;
|
|
const base = if (is_array) (self.getExprAlloca(ie.object) orelse self.lowerExprAsPtr(ie.object)) else self.lowerExpr(ie.object);
|
|
break :blk self.builder.emit(.{ .index_gep = .{ .lhs = base, .rhs = idx } }, ptr_ty);
|
|
}
|
|
// address_of(field_access) → use lowerExprAsPtr for GEP chain
|
|
// Handles all cases: pointer-based, index-based, nested field access
|
|
if (uop.op == .address_of and uop.operand.data == .field_access) {
|
|
const inner_ty = self.inferExprType(uop.operand);
|
|
const ptr_ty = self.module.types.ptrTo(inner_ty);
|
|
const ptr = self.lowerExprAsPtr(uop.operand);
|
|
break :blk self.builder.emit(.{ .addr_of = .{ .operand = ptr } }, ptr_ty);
|
|
}
|
|
// address_of(identifier) → return alloca directly (pointer to variable)
|
|
if (uop.op == .address_of and uop.operand.data == .identifier) {
|
|
const id_name = uop.operand.data.identifier.name;
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(id_name)) |binding| {
|
|
if (binding.is_alloca) {
|
|
const ptr_ty = self.module.types.ptrTo(binding.ty);
|
|
break :blk self.builder.emit(.{ .addr_of = .{ .operand = binding.ref } }, ptr_ty);
|
|
}
|
|
}
|
|
}
|
|
// address_of(global) → emit global_addr (pointer to global, not load)
|
|
if (self.program_index.global_names.get(id_name)) |gi| {
|
|
const ptr_ty = self.module.types.ptrTo(gi.ty);
|
|
break :blk self.builder.emit(.{ .global_addr = gi.id }, ptr_ty);
|
|
}
|
|
}
|
|
const operand = self.lowerExpr(uop.operand);
|
|
break :blk switch (uop.op) {
|
|
.negate => self.builder.emit(.{ .neg = .{ .operand = operand } }, self.inferExprType(uop.operand)),
|
|
.not => self.builder.emit(.{ .bool_not = .{ .operand = operand } }, .bool),
|
|
.bit_not => self.builder.emit(.{ .bit_not = .{ .operand = operand } }, self.inferExprType(uop.operand)),
|
|
.xx => self.lowerXX(operand, uop.operand),
|
|
.address_of => blk2: {
|
|
const inner_ty = self.inferExprType(uop.operand);
|
|
const ptr_ty = self.module.types.ptrTo(inner_ty);
|
|
break :blk2 self.builder.emit(.{ .addr_of = .{ .operand = operand } }, ptr_ty);
|
|
},
|
|
};
|
|
},
|
|
|
|
.if_expr => |ie| self.lowerIfExpr(&ie),
|
|
.match_expr => |me| self.lowerMatch(&me),
|
|
.while_expr => |we| self.lowerWhile(&we),
|
|
.for_expr => |fe| self.lowerFor(&fe),
|
|
.break_expr => self.lowerBreak(),
|
|
.continue_expr => self.lowerContinue(),
|
|
.call => |c| self.lowerCall(&c),
|
|
.ffi_intrinsic_call => |fic| self.lowerFfiIntrinsicCall(&fic),
|
|
.field_access => |fa| self.lowerFieldAccess(&fa, node.span),
|
|
.struct_literal => |sl| self.lowerStructLiteral(&sl, node.span),
|
|
.array_literal => |al| self.lowerArrayLiteral(&al),
|
|
.index_expr => |ie| self.lowerIndexExpr(&ie),
|
|
.slice_expr => |se| self.lowerSliceExpr(&se),
|
|
.lambda => |lam| self.lowerLambda(&lam),
|
|
.force_unwrap => |fu| self.lowerForceUnwrap(&fu),
|
|
.null_coalesce => |nc| self.lowerNullCoalesce(&nc),
|
|
.deref_expr => |de| self.lowerDerefExpr(&de),
|
|
.enum_literal => |el| self.lowerEnumLiteral(&el),
|
|
.comptime_expr => |ct| self.lowerInlineComptime(ct.expr),
|
|
.insert_expr => |ins| blk: {
|
|
break :blk self.lowerInsertExprValue(ins.expr);
|
|
},
|
|
.tuple_literal => |tl| self.lowerTupleLiteral(&tl),
|
|
.spread_expr => self.emitError("spread_expr", node.span),
|
|
.chained_comparison => |cc| self.lowerChainedComparison(&cc),
|
|
|
|
// `#jni_env(env) { body }` in expression position — the block's
|
|
// value becomes the env-scope's value. Save→set→body-value→restore.
|
|
.jni_env_block => |eb| blk: {
|
|
const env_ref = self.lowerExpr(eb.env);
|
|
const fids = self.getJniEnvTlFids();
|
|
const ptr_ty = self.module.types.ptrTo(.void);
|
|
const saved_tl = self.builder.emit(.{ .call = .{ .callee = fids.get, .args = &.{} } }, ptr_ty);
|
|
const set_args = self.alloc.dupe(Ref, &.{env_ref}) catch unreachable;
|
|
_ = self.builder.emit(.{ .call = .{ .callee = fids.set, .args = set_args } }, .void);
|
|
self.jni_env_stack.append(self.alloc, env_ref) catch unreachable;
|
|
const value = self.lowerBlockValue(eb.body) orelse self.builder.constInt(0, .void);
|
|
_ = self.jni_env_stack.pop();
|
|
const restore_args = self.alloc.dupe(Ref, &.{saved_tl}) catch unreachable;
|
|
_ = self.builder.emit(.{ .call = .{ .callee = fids.set, .args = restore_args } }, .void);
|
|
break :blk value;
|
|
},
|
|
|
|
// Statements that can appear in expression position
|
|
.block => |blk| blk: {
|
|
// Create a child scope for block-level variable shadowing
|
|
var block_scope = Scope.init(self.alloc, self.scope);
|
|
const saved_scope = self.scope;
|
|
self.scope = &block_scope;
|
|
const saved_defer_len = self.defer_stack.items.len;
|
|
defer {
|
|
self.emitBlockDefers(saved_defer_len);
|
|
self.scope = saved_scope;
|
|
block_scope.deinit();
|
|
}
|
|
// This block sits in value position (lowerExpr is reached only
|
|
// for value contexts — statement blocks go through lowerBlock).
|
|
// If its last expression's value is discarded by a `;`, the
|
|
// surrounding expression has no value to use: report it.
|
|
if (!blk.produces_value and blk.discarded_semi != null) {
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, blk.discarded_semi.?, "this block is used as a value but its last expression's value is discarded by this `;` — drop the `;`", .{});
|
|
}
|
|
}
|
|
// A block in expression position yields its last statement's
|
|
// value only when it produces one (no trailing `;`); otherwise
|
|
// it runs as statements and evaluates to void.
|
|
if (blk.produces_value and blk.stmts.len > 0) {
|
|
for (blk.stmts[0 .. blk.stmts.len - 1]) |stmt| {
|
|
self.lowerStmt(stmt);
|
|
}
|
|
break :blk self.tryLowerAsExpr(blk.stmts[blk.stmts.len - 1]) orelse
|
|
self.builder.constInt(0, .void);
|
|
}
|
|
for (blk.stmts) |stmt| {
|
|
self.lowerStmt(stmt);
|
|
}
|
|
break :blk self.builder.constInt(0, .void);
|
|
},
|
|
|
|
// type_expr can appear as a variable reference when the name collides
|
|
// with a builtin type name (e.g. s2, u8). Check scope first.
|
|
.type_expr => |te| blk: {
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(te.name)) |binding| {
|
|
if (binding.is_alloca) {
|
|
break :blk self.builder.load(binding.ref, binding.ty);
|
|
}
|
|
break :blk binding.ref;
|
|
}
|
|
}
|
|
if (self.program_index.global_names.get(te.name)) |gi| {
|
|
break :blk self.builder.emit(.{ .global_get = gi.id }, gi.ty);
|
|
}
|
|
// Type literal in expression position → first-class
|
|
// `const_type` Value (i64 = TypeId.index()). Makes
|
|
// `t : Type = f64;` store a real TypeId; lets
|
|
// `t == f64` icmp at runtime against the same TypeId.
|
|
if (self.isKnownTypeName(te.name)) {
|
|
const ty = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
|
break :blk self.builder.constType(ty);
|
|
}
|
|
break :blk self.emitError(te.name, node.span);
|
|
},
|
|
|
|
.try_expr => |te| self.lowerTry(te.operand, node.span),
|
|
.catch_expr => |ce| self.lowerCatch(&ce, node.span),
|
|
.caller_location => self.lowerCallerLocation(node),
|
|
else => self.emitError("unknown_expr", node.span),
|
|
};
|
|
}
|
|
|
|
/// If `node` names a `for xs: (*x)` by-ref capture (an `*elem`), returns
|
|
/// the element (pointee) type so a value-position use can auto-deref it.
|
|
fn refCapturePointee(self: *Lowering, node: *const Node) ?TypeId {
|
|
if (node.data != .identifier) return null;
|
|
const scope = self.scope orelse return null;
|
|
const binding = scope.lookup(node.data.identifier.name) orelse return null;
|
|
if (!binding.is_ref_capture or binding.ty.isBuiltin()) return null;
|
|
const info = self.module.types.get(binding.ty);
|
|
return if (info == .pointer) info.pointer.pointee else null;
|
|
}
|
|
|
|
fn lowerBinaryOp(self: *Lowering, bop: *const ast.BinaryOp) Ref {
|
|
// Short-circuit: `a and b` → if a then b else false
|
|
if (bop.op == .and_op) {
|
|
const lhs = self.lowerExpr(bop.lhs);
|
|
const rhs_bb = self.freshBlock("and.rhs");
|
|
const merge_bb = self.freshBlockWithParams("and.merge", &.{.bool});
|
|
const false_val = self.builder.constBool(false);
|
|
self.builder.condBr(lhs, rhs_bb, &.{}, merge_bb, &.{false_val});
|
|
self.builder.switchToBlock(rhs_bb);
|
|
const rhs = self.lowerExpr(bop.rhs);
|
|
self.builder.br(merge_bb, &.{rhs});
|
|
self.builder.switchToBlock(merge_bb);
|
|
return self.builder.blockParam(merge_bb, 0, .bool);
|
|
}
|
|
// Short-circuit: `a or b` → if a then true else b
|
|
if (bop.op == .or_op) {
|
|
// A failable `or` (value-terminator or chain) routes to the error-
|
|
// handling lowering, not the optional/boolean unwrap below. Detected
|
|
// structurally (a `try`-chain's value type is non-failable `T`, so a
|
|
// type-only `exprIsFailable(lhs)` would miss nested chains).
|
|
if (self.orIsFailableChain(bop)) {
|
|
return self.lowerFailableOr(bop);
|
|
}
|
|
const lhs = self.lowerExpr(bop.lhs);
|
|
const rhs_bb = self.freshBlock("or.rhs");
|
|
const merge_bb = self.freshBlockWithParams("or.merge", &.{.bool});
|
|
const true_val = self.builder.constBool(true);
|
|
self.builder.condBr(lhs, merge_bb, &.{true_val}, rhs_bb, &.{});
|
|
self.builder.switchToBlock(rhs_bb);
|
|
const rhs = self.lowerExpr(bop.rhs);
|
|
self.builder.br(merge_bb, &.{rhs});
|
|
self.builder.switchToBlock(merge_bb);
|
|
return self.builder.blockParam(merge_bb, 0, .bool);
|
|
}
|
|
|
|
// Type-literal comparison fold: when both sides are type-shaped
|
|
// AST nodes (`s64`, `*u8`, `?T`, `[3]f64`, etc.) OR resolve to
|
|
// a static TypeId at lower time (`type_of(x)` for any
|
|
// statically-typed `x`), resolve each and emit a `const_bool`.
|
|
// Same semantic as `type_eq(A, B)` but using the standard `==`
|
|
// operator — the user's intuition. Without the fold, both
|
|
// sides lower as `const_type` undef-i64 and the runtime icmp
|
|
// returns garbage.
|
|
if (bop.op == .eq or bop.op == .neq) {
|
|
if (self.isStaticTypeRef(bop.lhs) and self.isStaticTypeRef(bop.rhs)) {
|
|
const lhs_ty = self.resolveTypeArg(bop.lhs);
|
|
const rhs_ty = self.resolveTypeArg(bop.rhs);
|
|
const eq_result = lhs_ty == rhs_ty;
|
|
return self.builder.constBool(if (bop.op == .eq) eq_result else !eq_result);
|
|
}
|
|
}
|
|
|
|
// Any-shaped `==` (e.g. `t == s64` where `t: Type`): both
|
|
// operands are 16-byte `{tag, value}` aggregates. LLVM
|
|
// doesn't accept `icmp` on aggregates directly. Decompose
|
|
// via `unbox_any` (which extracts the value field at
|
|
// `.s64`) and compare the i64s. Tag fields are stable
|
|
// across compilations of the same source so value-only
|
|
// identity is enough.
|
|
if (bop.op == .eq or bop.op == .neq) {
|
|
const lhs_ty = self.inferExprType(bop.lhs);
|
|
const rhs_ty = self.inferExprType(bop.rhs);
|
|
if (lhs_ty == .any and rhs_ty == .any) {
|
|
const lhs = self.lowerExpr(bop.lhs);
|
|
const rhs = self.lowerExpr(bop.rhs);
|
|
const lhs_val = self.builder.emit(.{ .unbox_any = .{ .operand = lhs } }, .s64);
|
|
const rhs_val = self.builder.emit(.{ .unbox_any = .{ .operand = rhs } }, .s64);
|
|
if (bop.op == .eq) {
|
|
return self.builder.emit(.{ .cmp_eq = .{ .lhs = lhs_val, .rhs = rhs_val } }, .bool);
|
|
} else {
|
|
return self.builder.emit(.{ .cmp_ne = .{ .lhs = lhs_val, .rhs = rhs_val } }, .bool);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Special case: optional == null / optional != null
|
|
if (bop.op == .eq or bop.op == .neq) {
|
|
const lhs_is_null = bop.lhs.data == .null_literal;
|
|
const rhs_is_null = bop.rhs.data == .null_literal;
|
|
if (lhs_is_null or rhs_is_null) {
|
|
const opt_node = if (rhs_is_null) bop.lhs else bop.rhs;
|
|
const opt_ty = self.inferExprType(opt_node);
|
|
if (!opt_ty.isBuiltin()) {
|
|
const info = self.module.types.get(opt_ty);
|
|
if (info == .optional) {
|
|
const opt_val = self.lowerExpr(opt_node);
|
|
const has = self.builder.emit(.{ .optional_has_value = .{ .operand = opt_val } }, .bool);
|
|
// == null → !has_value, != null → has_value
|
|
return if (bop.op == .eq) self.builder.emit(.{ .bool_not = .{ .operand = has } }, .bool) else has;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Error-set equality: an error-set value compares only with an
|
|
// `error.X` tag literal or another error-set value. Comparing to a raw
|
|
// integer is a type error (coerce with `xx`). `e == error.X` resolves
|
|
// X against e's set and validates membership.
|
|
if (bop.op == .eq or bop.op == .neq) {
|
|
if (self.tryLowerErrorSetEquality(bop)) |result| return result;
|
|
}
|
|
|
|
// Set target_type for null literals to match the other operand's type.
|
|
// This ensures null gets the same LLVM type as the value being compared.
|
|
if (bop.op == .eq or bop.op == .neq) {
|
|
const null_on_rhs = bop.rhs.data == .null_literal;
|
|
const null_on_lhs = bop.lhs.data == .null_literal;
|
|
if (null_on_rhs or null_on_lhs) {
|
|
var other_ty = if (null_on_rhs) self.inferExprType(bop.lhs) else self.inferExprType(bop.rhs);
|
|
// Lower the non-null side first when its type isn't statically
|
|
// inferable, and take the null's type from the lowered value —
|
|
// never a guess.
|
|
var pre_lowered: ?Ref = null;
|
|
if (other_ty == .unresolved) {
|
|
pre_lowered = self.lowerExpr(if (null_on_rhs) bop.lhs else bop.rhs);
|
|
other_ty = self.builder.getRefType(pre_lowered.?);
|
|
}
|
|
if (other_ty != .void and other_ty != .unresolved) {
|
|
const saved_tt = self.target_type;
|
|
self.target_type = other_ty;
|
|
const lv = if (null_on_lhs or pre_lowered == null) self.lowerExpr(bop.lhs) else pre_lowered.?;
|
|
const rv = if (null_on_rhs or pre_lowered == null) self.lowerExpr(bop.rhs) else pre_lowered.?;
|
|
self.target_type = saved_tt;
|
|
const cmp_op: inst_mod.Op = if (bop.op == .eq) .{ .cmp_eq = .{ .lhs = lv, .rhs = rv } } else .{ .cmp_ne = .{ .lhs = lv, .rhs = rv } };
|
|
return self.builder.emit(cmp_op, .bool);
|
|
}
|
|
}
|
|
}
|
|
var lhs = self.lowerExpr(bop.lhs);
|
|
// A `for xs: (*x)` capture is a pointer; in a value position (here, an
|
|
// operand) it auto-derefs to the element.
|
|
const lhs_ref_pointee = self.refCapturePointee(bop.lhs);
|
|
if (lhs_ref_pointee) |p| lhs = self.builder.load(lhs, p);
|
|
// Set target_type from LHS so enum literals on RHS resolve correctly.
|
|
// When the LHS isn't statically inferable (e.g. `#objc_call(...)`), use
|
|
// the lowered operand's concrete type rather than a guess.
|
|
const lhs_ty = blk: {
|
|
if (lhs_ref_pointee) |p| break :blk p;
|
|
const it = self.inferExprType(bop.lhs);
|
|
break :blk if (it == .unresolved) self.builder.getRefType(lhs) else it;
|
|
};
|
|
const saved_tt = self.target_type;
|
|
if (lhs_ty != .void) {
|
|
if (!lhs_ty.isBuiltin()) {
|
|
const lhs_info = self.module.types.get(lhs_ty);
|
|
if (lhs_info == .@"enum" or lhs_info == .@"union" or lhs_info == .tagged_union) {
|
|
self.target_type = lhs_ty;
|
|
}
|
|
} else if (lhs_ty == .f32 or lhs_ty == .f64) {
|
|
self.target_type = lhs_ty;
|
|
}
|
|
}
|
|
var rhs = self.lowerExpr(bop.rhs);
|
|
const rhs_ref_pointee = self.refCapturePointee(bop.rhs);
|
|
if (rhs_ref_pointee) |p| rhs = self.builder.load(rhs, p);
|
|
self.target_type = saved_tt;
|
|
// Result type follows the shared promotion rule: an int LHS with a
|
|
// float RHS promotes to the float (`s64 * f32` → `f32`); vectors /
|
|
// structs keep the LHS type. `inferExprType` reuses the same helper
|
|
// so static typing agrees with the value produced here.
|
|
const rhs_inferred = rhs_ref_pointee orelse self.inferExprType(bop.rhs);
|
|
var ty = arithResultType(lhs_ty, rhs_inferred);
|
|
|
|
// Auto-unwrap optional operands for arithmetic/comparison
|
|
if (!ty.isBuiltin()) {
|
|
const info = self.module.types.get(ty);
|
|
if (info == .optional) {
|
|
ty = info.optional.child;
|
|
lhs = self.builder.emit(.{ .optional_unwrap = .{ .operand = lhs } }, ty);
|
|
}
|
|
}
|
|
const rhs_ty = rhs_ref_pointee orelse self.inferExprType(bop.rhs);
|
|
if (!rhs_ty.isBuiltin()) {
|
|
const rhs_info = self.module.types.get(rhs_ty);
|
|
if (rhs_info == .optional) {
|
|
rhs = self.builder.emit(.{ .optional_unwrap = .{ .operand = rhs } }, rhs_info.optional.child);
|
|
}
|
|
}
|
|
|
|
// String comparison: use str_eq/str_ne (memcmp-based) instead of pointer comparison
|
|
if (ty == .string and (bop.op == .eq or bop.op == .neq)) {
|
|
return if (bop.op == .eq)
|
|
self.builder.emit(.{ .str_eq = .{ .lhs = lhs, .rhs = rhs } }, .bool)
|
|
else
|
|
self.builder.emit(.{ .str_ne = .{ .lhs = lhs, .rhs = rhs } }, .bool);
|
|
}
|
|
|
|
// Tuple operators
|
|
if (!ty.isBuiltin()) {
|
|
const lhs_info = self.module.types.get(ty);
|
|
if (lhs_info == .tuple) {
|
|
return self.lowerTupleOp(bop, lhs, rhs, ty);
|
|
}
|
|
}
|
|
// Tuple membership: value in (tuple)
|
|
if (bop.op == .in_op) {
|
|
const rhs_ty_raw = self.inferExprType(bop.rhs);
|
|
if (!rhs_ty_raw.isBuiltin()) {
|
|
const rhs_info_raw = self.module.types.get(rhs_ty_raw);
|
|
if (rhs_info_raw == .tuple) {
|
|
return self.lowerTupleMembership(lhs, rhs, rhs_info_raw.tuple);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reject scalar ops on incompatible operand types (e.g.
|
|
// `s64 + string`, `s64 < string`, `s64 & string`). The result type
|
|
// `ty` is derived from the LHS, so without this the op lowers as
|
|
// `<op> : <lhs>` and either reinterprets the RHS bytes (arithmetic
|
|
// / bitwise → garbage) or feeds mismatched LLVM types to `icmp`
|
|
// (ordering → verifier failure).
|
|
{
|
|
const group: enum { none, arith, ordering, bitwise } = switch (bop.op) {
|
|
.add, .sub, .mul, .div, .mod => .arith,
|
|
.lt, .lte, .gt, .gte => .ordering,
|
|
.bit_and, .bit_or, .bit_xor, .shl, .shr => .bitwise,
|
|
else => .none,
|
|
};
|
|
if (group != .none) {
|
|
const eff_rhs_ty = blk: {
|
|
if (rhs_ty == .unresolved) break :blk self.builder.getRefType(rhs);
|
|
if (!rhs_ty.isBuiltin()) {
|
|
const ri = self.module.types.get(rhs_ty);
|
|
if (ri == .optional) break :blk ri.optional.child;
|
|
}
|
|
break :blk rhs_ty;
|
|
};
|
|
const ok = switch (group) {
|
|
.arith => self.isArithOperand(ty) and self.isArithOperand(eff_rhs_ty),
|
|
.ordering => self.isOrderingOperand(ty) and self.isOrderingOperand(eff_rhs_ty),
|
|
.bitwise => self.isBitwiseOperand(ty) and self.isBitwiseOperand(eff_rhs_ty),
|
|
.none => true,
|
|
};
|
|
if (!ok) {
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, bop.lhs.span, "cannot apply '{s}' to operands of type '{s}' and '{s}'", .{
|
|
binOpSymbol(bop.op), self.formatTypeName(ty), self.formatTypeName(eff_rhs_ty),
|
|
});
|
|
}
|
|
return self.emitPlaceholder("operand-type-mismatch");
|
|
}
|
|
}
|
|
}
|
|
|
|
return switch (bop.op) {
|
|
.add => self.builder.add(lhs, rhs, ty),
|
|
.sub => self.builder.sub(lhs, rhs, ty),
|
|
.mul => self.builder.mul(lhs, rhs, ty),
|
|
.div => self.builder.div(lhs, rhs, ty),
|
|
.mod => self.builder.emit(.{ .mod = .{ .lhs = lhs, .rhs = rhs } }, ty),
|
|
.eq => self.builder.cmpEq(lhs, rhs),
|
|
.neq => self.builder.emit(.{ .cmp_ne = .{ .lhs = lhs, .rhs = rhs } }, .bool),
|
|
.lt => self.builder.cmpLt(lhs, rhs),
|
|
.lte => self.builder.emit(.{ .cmp_le = .{ .lhs = lhs, .rhs = rhs } }, .bool),
|
|
.gt => self.builder.cmpGt(lhs, rhs),
|
|
.gte => self.builder.emit(.{ .cmp_ge = .{ .lhs = lhs, .rhs = rhs } }, .bool),
|
|
.and_op => self.builder.emit(.{ .bool_and = .{ .lhs = lhs, .rhs = rhs } }, .bool),
|
|
.or_op => self.builder.emit(.{ .bool_or = .{ .lhs = lhs, .rhs = rhs } }, .bool),
|
|
.bit_and => self.builder.emit(.{ .bit_and = .{ .lhs = lhs, .rhs = rhs } }, ty),
|
|
.bit_or => self.builder.emit(.{ .bit_or = .{ .lhs = lhs, .rhs = rhs } }, ty),
|
|
.bit_xor => self.builder.emit(.{ .bit_xor = .{ .lhs = lhs, .rhs = rhs } }, ty),
|
|
.shl => self.builder.emit(.{ .shl = .{ .lhs = lhs, .rhs = rhs } }, ty),
|
|
.shr => self.builder.emit(.{ .shr = .{ .lhs = lhs, .rhs = rhs } }, ty),
|
|
.in_op => self.emitError("in_op", bop.lhs.span),
|
|
};
|
|
}
|
|
|
|
/// Handle tuple binary ops: concat (+), repeat (*), comparison (==, !=, <, <=, >, >=)
|
|
fn lowerTupleOp(self: *Lowering, bop: *const ast.BinaryOp, lhs: Ref, rhs: Ref, lhs_ty: TypeId) Ref {
|
|
const lhs_info = self.module.types.get(lhs_ty);
|
|
const lhs_fields = lhs_info.tuple.fields;
|
|
|
|
switch (bop.op) {
|
|
.add => {
|
|
// Tuple concatenation: (a, b) + (c, d) → (a, b, c, d)
|
|
const rhs_ty = self.inferExprType(bop.rhs);
|
|
const rhs_fields = if (!rhs_ty.isBuiltin()) blk: {
|
|
const ri = self.module.types.get(rhs_ty);
|
|
break :blk if (ri == .tuple) ri.tuple.fields else &[_]TypeId{};
|
|
} else &[_]TypeId{};
|
|
|
|
var all_fields = std.ArrayList(TypeId).empty;
|
|
defer all_fields.deinit(self.alloc);
|
|
var all_vals = std.ArrayList(Ref).empty;
|
|
defer all_vals.deinit(self.alloc);
|
|
|
|
for (lhs_fields, 0..) |f, i| {
|
|
all_fields.append(self.alloc, f) catch unreachable;
|
|
all_vals.append(self.alloc, self.builder.structGet(lhs, @intCast(i), f)) catch unreachable;
|
|
}
|
|
for (rhs_fields, 0..) |f, i| {
|
|
all_fields.append(self.alloc, f) catch unreachable;
|
|
all_vals.append(self.alloc, self.builder.structGet(rhs, @intCast(i), f)) catch unreachable;
|
|
}
|
|
|
|
const result_ty = self.module.types.intern(.{ .tuple = .{
|
|
.fields = self.alloc.dupe(TypeId, all_fields.items) catch unreachable,
|
|
.names = null,
|
|
} });
|
|
const owned = self.alloc.dupe(Ref, all_vals.items) catch unreachable;
|
|
return self.builder.emit(.{ .tuple_init = .{ .fields = owned } }, result_ty);
|
|
},
|
|
.mul => {
|
|
// Tuple repeat: (a, b) * 3 → (a, b, a, b, a, b)
|
|
const count: usize = switch (bop.rhs.data) {
|
|
.int_literal => |il| @intCast(@as(u64, @bitCast(il.value))),
|
|
else => 1,
|
|
};
|
|
|
|
var all_fields = std.ArrayList(TypeId).empty;
|
|
defer all_fields.deinit(self.alloc);
|
|
var all_vals = std.ArrayList(Ref).empty;
|
|
defer all_vals.deinit(self.alloc);
|
|
|
|
for (0..count) |_| {
|
|
for (lhs_fields, 0..) |f, i| {
|
|
all_fields.append(self.alloc, f) catch unreachable;
|
|
all_vals.append(self.alloc, self.builder.structGet(lhs, @intCast(i), f)) catch unreachable;
|
|
}
|
|
}
|
|
|
|
const result_ty = self.module.types.intern(.{ .tuple = .{
|
|
.fields = self.alloc.dupe(TypeId, all_fields.items) catch unreachable,
|
|
.names = null,
|
|
} });
|
|
const owned = self.alloc.dupe(Ref, all_vals.items) catch unreachable;
|
|
return self.builder.emit(.{ .tuple_init = .{ .fields = owned } }, result_ty);
|
|
},
|
|
.eq, .neq => {
|
|
// Element-wise equality (or single-element tuple vs scalar)
|
|
const rhs_is_tuple = blk: {
|
|
const rt = self.inferExprType(bop.rhs);
|
|
if (!rt.isBuiltin()) {
|
|
break :blk self.module.types.get(rt) == .tuple;
|
|
}
|
|
break :blk false;
|
|
};
|
|
if (!rhs_is_tuple and lhs_fields.len == 1) {
|
|
// Single-element tuple vs scalar: unwrap and compare
|
|
const lf = self.builder.structGet(lhs, 0, lhs_fields[0]);
|
|
const eq = self.builder.cmpEq(lf, rhs);
|
|
return if (bop.op == .neq) self.builder.emit(.{ .bool_not = .{ .operand = eq } }, .bool) else eq;
|
|
}
|
|
var result = self.builder.constBool(true);
|
|
for (lhs_fields, 0..) |f, i| {
|
|
const lf = self.builder.structGet(lhs, @intCast(i), f);
|
|
const rf = self.builder.structGet(rhs, @intCast(i), f);
|
|
const eq = self.builder.cmpEq(lf, rf);
|
|
result = self.builder.emit(.{ .bool_and = .{ .lhs = result, .rhs = eq } }, .bool);
|
|
}
|
|
return if (bop.op == .neq) self.builder.emit(.{ .bool_not = .{ .operand = result } }, .bool) else result;
|
|
},
|
|
.lt, .lte, .gt, .gte => {
|
|
// Lexicographic comparison
|
|
return self.lowerTupleLexCompare(bop.op, lhs, rhs, lhs_fields);
|
|
},
|
|
else => return self.builder.constInt(0, .s64),
|
|
}
|
|
}
|
|
|
|
fn lowerTupleLexCompare(self: *Lowering, op: ast.BinaryOp.Op, lhs: Ref, rhs: Ref, fields: []const TypeId) Ref {
|
|
// Lexicographic comparison using boolean logic.
|
|
// (a0,a1) < (b0,b1) = (a0 < b0) || (a0 == b0 && a1 < b1)
|
|
// (a0,a1) <= (b0,b1) = (a0 < b0) || (a0 == b0 && a1 <= b1)
|
|
if (fields.len == 0) return self.builder.constBool(op == .lte or op == .gte);
|
|
|
|
const n = fields.len;
|
|
// Start with the last field using the actual op
|
|
const lf_last = self.builder.structGet(lhs, @intCast(n - 1), fields[n - 1]);
|
|
const rf_last = self.builder.structGet(rhs, @intCast(n - 1), fields[n - 1]);
|
|
var result = switch (op) {
|
|
.lt => self.builder.cmpLt(lf_last, rf_last),
|
|
.lte => self.builder.emit(.{ .cmp_le = .{ .lhs = lf_last, .rhs = rf_last } }, .bool),
|
|
.gt => self.builder.cmpGt(lf_last, rf_last),
|
|
.gte => self.builder.emit(.{ .cmp_ge = .{ .lhs = lf_last, .rhs = rf_last } }, .bool),
|
|
else => unreachable,
|
|
};
|
|
|
|
// Work backwards: result = (a[i] < b[i]) || (a[i] == b[i] && result)
|
|
if (n > 1) {
|
|
var i: usize = n - 1;
|
|
while (i > 0) {
|
|
i -= 1;
|
|
const lf = self.builder.structGet(lhs, @intCast(i), fields[i]);
|
|
const rf = self.builder.structGet(rhs, @intCast(i), fields[i]);
|
|
const strict = if (op == .lt or op == .lte) self.builder.cmpLt(lf, rf) else self.builder.cmpGt(lf, rf);
|
|
const eq = self.builder.cmpEq(lf, rf);
|
|
const eq_and_rest = self.builder.emit(.{ .bool_and = .{ .lhs = eq, .rhs = result } }, .bool);
|
|
result = self.builder.emit(.{ .bool_or = .{ .lhs = strict, .rhs = eq_and_rest } }, .bool);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
fn lowerTupleMembership(self: *Lowering, value: Ref, tuple: Ref, tuple_info: anytype) Ref {
|
|
// value in (a, b, c) → value == a || value == b || value == c
|
|
var result = self.builder.constBool(false);
|
|
for (tuple_info.fields, 0..) |f, i| {
|
|
const elem = self.builder.structGet(tuple, @intCast(i), f);
|
|
const eq = self.builder.cmpEq(value, elem);
|
|
result = self.builder.emit(.{ .bool_or = .{ .lhs = result, .rhs = eq } }, .bool);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// ── Control flow ────────────────────────────────────────────────
|
|
|
|
fn lowerIfExpr(self: *Lowering, ie: *const ast.IfExpr) Ref {
|
|
// inline if: evaluate condition at compile time, only lower taken branch
|
|
if (ie.is_comptime) {
|
|
if (self.evalComptimeCondition(ie.condition)) |is_true| {
|
|
if (is_true) {
|
|
return self.lowerInlineBranch(ie.then_branch);
|
|
} else if (ie.else_branch) |eb| {
|
|
return self.lowerInlineBranch(eb);
|
|
}
|
|
return self.builder.constInt(0, .void);
|
|
}
|
|
// Condition couldn't be evaluated — fall through to runtime
|
|
}
|
|
|
|
// Check for constant-bool conditions (e.g., is_flags(T) → false) to avoid dead-code LLVM errors
|
|
if (self.tryConstBoolCondition(ie.condition)) |is_true| {
|
|
if (is_true) {
|
|
// Condition always true: only lower then-branch
|
|
if ((ie.is_inline or self.force_block_value) and ie.else_branch != null) {
|
|
return self.lowerExpr(ie.then_branch);
|
|
}
|
|
self.lowerBlock(ie.then_branch);
|
|
// If then-branch terminated (return/break), mark block as dead
|
|
if (self.currentBlockHasTerminator()) {
|
|
self.block_terminated = true;
|
|
return .none;
|
|
}
|
|
return self.builder.constInt(0, .void);
|
|
} else {
|
|
// Condition always false: only lower else-branch (if any)
|
|
if (ie.else_branch) |eb| {
|
|
if (ie.is_inline or self.force_block_value) {
|
|
return self.lowerExpr(eb);
|
|
}
|
|
self.lowerBlock(eb);
|
|
if (self.currentBlockHasTerminator()) {
|
|
self.block_terminated = true;
|
|
return .none;
|
|
}
|
|
}
|
|
return self.builder.constInt(0, .void);
|
|
}
|
|
}
|
|
|
|
// Optional binding: `if val := expr { ... }`
|
|
// Clear target_type so the ternary's result type doesn't leak into the condition
|
|
// (e.g., `if x != 0 then 1.0 else 2.0` — the `0` must be s64, not f32)
|
|
const saved_cond_target = self.target_type;
|
|
self.target_type = null;
|
|
const opt_val = self.lowerExpr(ie.condition);
|
|
self.target_type = saved_cond_target;
|
|
const cond = if (ie.binding_name != null) blk: {
|
|
// The condition is an optional — emit has_value check
|
|
break :blk self.builder.emit(.{ .optional_has_value = .{ .operand = opt_val } }, .bool);
|
|
} else opt_val;
|
|
const has_else = ie.else_branch != null;
|
|
// If-else produces a value when inline OR when in value position (force_block_value)
|
|
var is_value = (ie.is_inline or self.force_block_value) and has_else;
|
|
|
|
// Infer result type from then branch for value if-exprs
|
|
// If then_branch is null/void, try else_branch (e.g., `if cond then null else val`)
|
|
var result_type: TypeId = if (is_value) blk: {
|
|
var t = self.inferExprType(ie.then_branch);
|
|
if ((t == .void or t == .unresolved) and ie.else_branch != null) {
|
|
t = self.inferExprType(ie.else_branch.?);
|
|
}
|
|
// Branch type not statically inferable (e.g. `null` / a bare enum
|
|
// literal) — use the contextually expected type rather than a guess.
|
|
if (t == .unresolved) {
|
|
if (self.target_type) |tt| t = tt;
|
|
}
|
|
break :blk t;
|
|
} else .void;
|
|
|
|
// A value-position if/else whose branches yield no value (both are
|
|
// `;`-terminated / void blocks) is really a statement-if — lowering it
|
|
// as a value would build a `phi void`. Demote it.
|
|
if (is_value and result_type == .void) {
|
|
is_value = false;
|
|
result_type = .void;
|
|
}
|
|
|
|
const then_bb = self.freshBlock("if.then");
|
|
const else_bb: ?BlockId = if (has_else) self.freshBlock("if.else") else null;
|
|
const merge_params: []const TypeId = if (is_value) &.{result_type} else &.{};
|
|
const merge_bb = self.freshBlockWithParams("if.merge", merge_params);
|
|
|
|
// Conditional branch
|
|
self.builder.condBr(
|
|
cond,
|
|
then_bb,
|
|
&.{},
|
|
if (else_bb) |eb| eb else merge_bb,
|
|
&.{},
|
|
);
|
|
|
|
// Then branch
|
|
self.builder.switchToBlock(then_bb);
|
|
// If binding: unwrap the optional and bind to the name
|
|
if (ie.binding_name) |bind_name| {
|
|
const opt_ty = self.inferExprType(ie.condition);
|
|
const inner_ty = if (!opt_ty.isBuiltin()) blk: {
|
|
const info = self.module.types.get(opt_ty);
|
|
break :blk if (info == .optional) info.optional.child else opt_ty;
|
|
} else opt_ty;
|
|
const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = opt_val } }, inner_ty);
|
|
const slot = self.builder.alloca(inner_ty);
|
|
self.builder.store(slot, unwrapped);
|
|
if (self.scope) |scope| {
|
|
scope.put(bind_name, .{ .ref = slot, .ty = inner_ty, .is_alloca = true });
|
|
}
|
|
}
|
|
// Set target_type so null/undef in branches get the right type
|
|
const saved_target = self.target_type;
|
|
if (is_value and result_type != .void) self.target_type = result_type;
|
|
if (is_value) {
|
|
var v = self.lowerExpr(ie.then_branch);
|
|
if (!self.currentBlockHasTerminator()) {
|
|
const v_ty = self.builder.getRefType(v);
|
|
if (v_ty != result_type and v_ty != .void and result_type != .void) {
|
|
v = self.coerceToType(v, v_ty, result_type);
|
|
}
|
|
self.builder.br(merge_bb, &.{v});
|
|
}
|
|
} else {
|
|
self.lowerBlock(ie.then_branch);
|
|
if (!self.currentBlockHasTerminator()) {
|
|
self.builder.br(merge_bb, &.{});
|
|
}
|
|
}
|
|
|
|
// Else branch
|
|
if (has_else) {
|
|
self.builder.switchToBlock(else_bb.?);
|
|
if (is_value) {
|
|
var v = self.lowerExpr(ie.else_branch.?);
|
|
if (!self.currentBlockHasTerminator()) {
|
|
const v_ty = self.builder.getRefType(v);
|
|
if (v_ty != result_type and v_ty != .void and result_type != .void) {
|
|
v = self.coerceToType(v, v_ty, result_type);
|
|
}
|
|
self.builder.br(merge_bb, &.{v});
|
|
}
|
|
} else {
|
|
self.lowerBlock(ie.else_branch.?);
|
|
if (!self.currentBlockHasTerminator()) {
|
|
self.builder.br(merge_bb, &.{});
|
|
}
|
|
}
|
|
}
|
|
self.target_type = saved_target;
|
|
|
|
// Continue at merge
|
|
self.builder.switchToBlock(merge_bb);
|
|
if (is_value) {
|
|
return self.builder.blockParam(merge_bb, 0, result_type);
|
|
}
|
|
return self.builder.constInt(0, .void);
|
|
}
|
|
|
|
/// Try to evaluate an AST condition as a compile-time constant bool.
|
|
/// Returns true/false if the condition is known at compile time, null otherwise.
|
|
fn tryConstBoolCondition(self: *Lowering, node: *const Node) ?bool {
|
|
switch (node.data) {
|
|
.bool_literal => |bl| return bl.value,
|
|
.call => |c| {
|
|
if (c.callee.data == .identifier) {
|
|
const cname = c.callee.data.identifier.name;
|
|
if (std.mem.eql(u8, cname, "is_flags")) {
|
|
// Resolve the type arg to check if it's actually a flags enum
|
|
if (c.args.len > 0) {
|
|
const ty = self.resolveTypeArg(c.args[0]);
|
|
if (!ty.isBuiltin()) {
|
|
const info = self.module.types.get(ty);
|
|
if (info == .@"enum") return info.@"enum".is_flags;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
if (std.mem.eql(u8, cname, "type_eq") and c.args.len >= 2) {
|
|
const a = self.resolveTypeArg(c.args[0]);
|
|
const b = self.resolveTypeArg(c.args[1]);
|
|
return a == b;
|
|
}
|
|
if (std.mem.eql(u8, cname, "has_impl") and c.args.len >= 2) {
|
|
const ty = self.resolveTypeArg(c.args[1]);
|
|
return self.computeHasImpl(c.args[0], ty);
|
|
}
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Shared implementation for the `has_impl(P, T)` builtin and its
|
|
/// `tryConstBoolCondition` arm. The protocol expression is either:
|
|
/// - Plain `Hash` (identifier / type_expr) → walks
|
|
/// `protocol_thunk_map["Hash\x00<T>"]`.
|
|
/// - Parameterised `Into(Block)` (call) → walks `param_impl_map`
|
|
/// keyed by `"<P>\x00<arg_mangled>\x00<T_mangled>"`.
|
|
/// Returns false on any malformed protocol-arg shape (caller
|
|
/// reports a diagnostic if it wants).
|
|
fn computeHasImpl(self: *Lowering, proto_node: *const Node, ty: TypeId) bool {
|
|
switch (proto_node.data) {
|
|
.identifier => |id| return self.protocolResolver().hasImplPlain(id.name, ty),
|
|
.type_expr => |te| return self.protocolResolver().hasImplPlain(te.name, ty),
|
|
.call => |c| {
|
|
const p_name: []const u8 = switch (c.callee.data) {
|
|
.identifier => |id| id.name,
|
|
.type_expr => |te| te.name,
|
|
else => return false,
|
|
};
|
|
// Resolve protocol type args. Each goes through
|
|
// `resolveTypeArg` so type aliases / generics / pack-
|
|
// indexed types all work as protocol args.
|
|
var arg_mangles = std.ArrayList(u8).empty;
|
|
defer arg_mangles.deinit(self.alloc);
|
|
for (c.args, 0..) |a, i| {
|
|
if (i > 0) arg_mangles.append(self.alloc, 0) catch return false;
|
|
const aty = self.resolveTypeArg(a);
|
|
arg_mangles.appendSlice(self.alloc, self.mangleTypeName(aty)) catch return false;
|
|
}
|
|
const ty_mangled = self.mangleTypeName(ty);
|
|
const key = std.fmt.allocPrint(self.alloc, "{s}\x00{s}\x00{s}", .{
|
|
p_name, arg_mangles.items, ty_mangled,
|
|
}) catch return false;
|
|
return self.param_impl_map.contains(key);
|
|
},
|
|
else => return false,
|
|
}
|
|
}
|
|
|
|
/// Evaluate a compile-time condition for `inline if`.
|
|
/// Handles: `ident == .variant`, `ident != .variant`, `ident == int`, `ident != int`.
|
|
fn evalComptimeCondition(self: *Lowering, node: *const Node) ?bool {
|
|
if (node.data != .binary_op) return null;
|
|
const bo = &node.data.binary_op;
|
|
if (bo.op != .eq and bo.op != .neq) return null;
|
|
|
|
// LHS must be an identifier that's in comptime_constants
|
|
const name = switch (bo.lhs.data) {
|
|
.identifier => |id| id.name,
|
|
else => return null,
|
|
};
|
|
const cv = self.comptime_constants.get(name) orelse return null;
|
|
|
|
switch (cv) {
|
|
.enum_tag => |et| {
|
|
// RHS must be an enum literal (.variant)
|
|
const variant_name = switch (bo.rhs.data) {
|
|
.enum_literal => |el| el.name,
|
|
else => return null,
|
|
};
|
|
// Look up variant index in the enum type
|
|
const enum_info = self.module.types.get(et.ty);
|
|
if (enum_info != .@"enum") return null;
|
|
const variant_idx = self.findVariantIndex(enum_info.@"enum".variants, variant_name);
|
|
const result = et.tag == variant_idx;
|
|
return if (bo.op == .eq) result else !result;
|
|
},
|
|
.int_val => |iv| {
|
|
// RHS must be an integer literal
|
|
const rhs_val: i64 = switch (bo.rhs.data) {
|
|
.int_literal => |il| il.value,
|
|
else => return null,
|
|
};
|
|
const result = iv == rhs_val;
|
|
return if (bo.op == .eq) result else !result;
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Evaluate a compile-time match expression for `inline if ... == { case ... }`.
|
|
/// Returns the body of the matching arm, or null if the match can't be resolved.
|
|
fn evalComptimeMatch(self: *Lowering, me: *const ast.MatchExpr) ?*const Node {
|
|
// Subject must be a comptime constant identifier
|
|
const name = switch (me.subject.data) {
|
|
.identifier => |id| id.name,
|
|
else => return null,
|
|
};
|
|
const cv = self.comptime_constants.get(name) orelse return null;
|
|
|
|
switch (cv) {
|
|
.enum_tag => |et| {
|
|
const enum_info = self.module.types.get(et.ty);
|
|
if (enum_info != .@"enum") return null;
|
|
for (me.arms) |arm| {
|
|
if (arm.pattern == null) continue; // default arm
|
|
const variant_name = switch (arm.pattern.?.data) {
|
|
.enum_literal => |el| el.name,
|
|
else => continue,
|
|
};
|
|
const variant_idx = self.findVariantIndex(enum_info.@"enum".variants, variant_name);
|
|
if (et.tag == variant_idx) return arm.body;
|
|
}
|
|
// No match — try default arm
|
|
for (me.arms) |arm| {
|
|
if (arm.pattern == null) return arm.body;
|
|
}
|
|
return null;
|
|
},
|
|
.int_val => |iv| {
|
|
for (me.arms) |arm| {
|
|
if (arm.pattern == null) continue;
|
|
const rhs_val: i64 = switch (arm.pattern.?.data) {
|
|
.int_literal => |il| il.value,
|
|
else => continue,
|
|
};
|
|
if (iv == rhs_val) return arm.body;
|
|
}
|
|
for (me.arms) |arm| {
|
|
if (arm.pattern == null) return arm.body;
|
|
}
|
|
return null;
|
|
},
|
|
}
|
|
}
|
|
|
|
fn lowerWhile(self: *Lowering, we: *const ast.WhileExpr) Ref {
|
|
const header_bb = self.freshBlock("while.hdr");
|
|
const body_bb = self.freshBlock("while.body");
|
|
const exit_bb = self.freshBlock("while.exit");
|
|
|
|
// Branch to header
|
|
self.builder.br(header_bb, &.{});
|
|
|
|
// Header: evaluate condition
|
|
self.builder.switchToBlock(header_bb);
|
|
const cond = self.lowerExpr(we.condition);
|
|
self.builder.condBr(cond, body_bb, &.{}, exit_bb, &.{});
|
|
|
|
// Body
|
|
self.builder.switchToBlock(body_bb);
|
|
|
|
// Save and set loop targets
|
|
const old_break = self.break_target;
|
|
const old_continue = self.continue_target;
|
|
self.break_target = exit_bb;
|
|
self.continue_target = header_bb;
|
|
defer {
|
|
self.break_target = old_break;
|
|
self.continue_target = old_continue;
|
|
}
|
|
|
|
self.lowerBlock(we.body);
|
|
if (!self.currentBlockHasTerminator()) {
|
|
self.builder.br(header_bb, &.{});
|
|
}
|
|
|
|
// Continue at exit
|
|
self.builder.switchToBlock(exit_bb);
|
|
return self.builder.constInt(0, .void);
|
|
}
|
|
|
|
/// View a `List(T)`-like struct (`{ items: [*]T, len, … }`) as its backing
|
|
/// `items` pointer + element type + `len`, so `for list: (x)` iterates the
|
|
/// elements. Null for anything that isn't such a struct.
|
|
fn listView(self: *Lowering, value: Ref, ty: TypeId) ?struct { data: Ref, data_ty: TypeId, len: Ref } {
|
|
if (ty.isBuiltin()) return null;
|
|
const info = self.module.types.get(ty);
|
|
if (info != .@"struct") return null;
|
|
const items_id = self.module.types.internString("items");
|
|
const len_id = self.module.types.internString("len");
|
|
var items_idx: ?u32 = null;
|
|
var items_ty: TypeId = .unresolved;
|
|
var len_idx: ?u32 = null;
|
|
for (info.@"struct".fields, 0..) |f, i| {
|
|
if (f.name == items_id and !f.ty.isBuiltin() and self.module.types.get(f.ty) == .many_pointer) {
|
|
items_idx = @intCast(i);
|
|
items_ty = f.ty;
|
|
} else if (f.name == len_id) {
|
|
len_idx = @intCast(i);
|
|
}
|
|
}
|
|
if (items_idx == null or len_idx == null) return null;
|
|
return .{
|
|
.data = self.builder.emit(.{ .struct_get = .{ .base = value, .field_index = items_idx.? } }, items_ty),
|
|
.data_ty = items_ty,
|
|
.len = self.builder.emit(.{ .struct_get = .{ .base = value, .field_index = len_idx.? } }, .s64),
|
|
};
|
|
}
|
|
|
|
fn lowerFor(self: *Lowering, fe: *const ast.ForExpr) Ref {
|
|
if (fe.range_end) |end_node| {
|
|
if (fe.is_inline) return self.lowerInlineRangeFor(fe, end_node);
|
|
return self.lowerRuntimeRangeFor(fe, end_node);
|
|
}
|
|
// Collection-form `for xs : (x)` over a pack: a pack has no runtime
|
|
// value to iterate (Decision 1) — point the user at `inline for`.
|
|
if (fe.iterable.data == .identifier and self.isPackName(fe.iterable.data.identifier.name)) {
|
|
return self.diagPackAsValue(fe.iterable.data.identifier.name, fe.iterable.span, .runtime_iter);
|
|
}
|
|
|
|
// Lower iterable + resolve its static type.
|
|
var iterable = self.lowerExpr(fe.iterable);
|
|
var iterable_ty = self.inferExprType(fe.iterable);
|
|
|
|
// `*List` / `*[]T` etc. — deref to the collection value.
|
|
const ptr_info = if (iterable_ty.isBuiltin()) null else self.module.types.get(iterable_ty);
|
|
if (ptr_info != null and ptr_info.? == .pointer) {
|
|
iterable = self.builder.load(iterable, ptr_info.?.pointer.pointee);
|
|
iterable_ty = ptr_info.?.pointer.pointee;
|
|
}
|
|
|
|
// A `List(T)`-like struct iterates its `items[0..len]`; arrays/slices
|
|
// use their intrinsic length.
|
|
var len: Ref = undefined;
|
|
if (self.listView(iterable, iterable_ty)) |lv| {
|
|
iterable = lv.data;
|
|
iterable_ty = lv.data_ty;
|
|
len = lv.len;
|
|
} else {
|
|
len = self.builder.emit(.{ .length = .{ .operand = iterable } }, .s64);
|
|
}
|
|
|
|
// Create index variable
|
|
const idx_slot = self.builder.alloca(.s64);
|
|
const zero = self.builder.constInt(0, .s64);
|
|
self.builder.store(idx_slot, zero);
|
|
|
|
const header_bb = self.freshBlock("for.hdr");
|
|
const body_bb = self.freshBlock("for.body");
|
|
const inc_bb = self.freshBlock("for.inc");
|
|
const exit_bb = self.freshBlock("for.exit");
|
|
|
|
self.builder.br(header_bb, &.{});
|
|
|
|
// Header: compare index < length
|
|
self.builder.switchToBlock(header_bb);
|
|
const idx_val = self.builder.load(idx_slot, .s64);
|
|
const cmp = self.builder.cmpLt(idx_val, len);
|
|
self.builder.condBr(cmp, body_bb, &.{}, exit_bb, &.{});
|
|
|
|
// Body
|
|
self.builder.switchToBlock(body_bb);
|
|
|
|
// Bind element — resolve element type from iterable. `for xs: (*x)`
|
|
// binds a pointer into the collection (no per-element copy); `(x)`
|
|
// binds a value copy.
|
|
const elem_ty = self.getElementType(iterable_ty);
|
|
const bind_ty = if (fe.capture_by_ref) self.module.types.ptrTo(elem_ty) else elem_ty;
|
|
const elem = if (fe.capture_by_ref) blk: {
|
|
// A slice value carries its backing pointer, so GEP on it writes
|
|
// through. An array is a value — GEP needs its storage (alloca) or
|
|
// mutations would hit a copy.
|
|
const is_array = !iterable_ty.isBuiltin() and self.module.types.get(iterable_ty) == .array;
|
|
const base = if (is_array) (self.getExprAlloca(fe.iterable) orelse iterable) else iterable;
|
|
break :blk self.builder.emit(.{ .index_gep = .{ .lhs = base, .rhs = idx_val } }, bind_ty);
|
|
} else self.builder.emit(.{ .index_get = .{ .lhs = iterable, .rhs = idx_val } }, bind_ty);
|
|
|
|
var body_scope = Scope.init(self.alloc, self.scope);
|
|
const old_scope = self.scope;
|
|
self.scope = &body_scope;
|
|
|
|
body_scope.put(fe.capture_name, .{ .ref = elem, .ty = bind_ty, .is_alloca = false, .is_ref_capture = fe.capture_by_ref });
|
|
|
|
// Bind index if requested
|
|
if (fe.index_name) |iname| {
|
|
body_scope.put(iname, .{ .ref = idx_val, .ty = .s64, .is_alloca = false });
|
|
}
|
|
|
|
// Save and set loop targets
|
|
const old_break = self.break_target;
|
|
const old_continue = self.continue_target;
|
|
self.break_target = exit_bb;
|
|
self.continue_target = inc_bb; // continue → increment, not header
|
|
|
|
self.lowerBlock(fe.body);
|
|
|
|
self.break_target = old_break;
|
|
self.continue_target = old_continue;
|
|
self.scope = old_scope;
|
|
body_scope.deinit();
|
|
|
|
// Fall through to increment block
|
|
if (!self.currentBlockHasTerminator()) {
|
|
self.builder.br(inc_bb, &.{});
|
|
}
|
|
|
|
// Increment block: increment index and jump back to header
|
|
self.builder.switchToBlock(inc_bb);
|
|
{
|
|
const cur_idx = self.builder.load(idx_slot, .s64);
|
|
const one = self.builder.constInt(1, .s64);
|
|
const next_idx = self.builder.add(cur_idx, one, .s64);
|
|
self.builder.store(idx_slot, next_idx);
|
|
self.builder.br(header_bb, &.{});
|
|
}
|
|
|
|
// Continue at exit
|
|
self.builder.switchToBlock(exit_bb);
|
|
return self.builder.constInt(0, .void);
|
|
}
|
|
|
|
/// Runtime counting loop `for start..end (i) { }` — `i` (optional) is the
|
|
/// cursor, `end` is exclusive. Lowers to the same header/inc/exit shape as
|
|
/// the collection form, minus the element fetch.
|
|
fn lowerRuntimeRangeFor(self: *Lowering, fe: *const ast.ForExpr, end_node: *Node) Ref {
|
|
const start = self.lowerExpr(fe.iterable);
|
|
const end = self.lowerExpr(end_node);
|
|
|
|
const idx_slot = self.builder.alloca(.s64);
|
|
self.builder.store(idx_slot, start);
|
|
|
|
const header_bb = self.freshBlock("for.hdr");
|
|
const body_bb = self.freshBlock("for.body");
|
|
const inc_bb = self.freshBlock("for.inc");
|
|
const exit_bb = self.freshBlock("for.exit");
|
|
|
|
self.builder.br(header_bb, &.{});
|
|
|
|
self.builder.switchToBlock(header_bb);
|
|
const idx_val = self.builder.load(idx_slot, .s64);
|
|
const cmp = self.builder.cmpLt(idx_val, end);
|
|
self.builder.condBr(cmp, body_bb, &.{}, exit_bb, &.{});
|
|
|
|
self.builder.switchToBlock(body_bb);
|
|
var body_scope = Scope.init(self.alloc, self.scope);
|
|
const old_scope = self.scope;
|
|
self.scope = &body_scope;
|
|
if (fe.capture_name.len > 0) {
|
|
body_scope.put(fe.capture_name, .{ .ref = idx_val, .ty = .s64, .is_alloca = false });
|
|
}
|
|
|
|
const old_break = self.break_target;
|
|
const old_continue = self.continue_target;
|
|
self.break_target = exit_bb;
|
|
self.continue_target = inc_bb;
|
|
|
|
self.lowerBlock(fe.body);
|
|
|
|
self.break_target = old_break;
|
|
self.continue_target = old_continue;
|
|
self.scope = old_scope;
|
|
body_scope.deinit();
|
|
|
|
if (!self.currentBlockHasTerminator()) {
|
|
self.builder.br(inc_bb, &.{});
|
|
}
|
|
|
|
self.builder.switchToBlock(inc_bb);
|
|
{
|
|
const cur_idx = self.builder.load(idx_slot, .s64);
|
|
const one = self.builder.constInt(1, .s64);
|
|
const next_idx = self.builder.add(cur_idx, one, .s64);
|
|
self.builder.store(idx_slot, next_idx);
|
|
self.builder.br(header_bb, &.{});
|
|
}
|
|
|
|
self.builder.switchToBlock(exit_bb);
|
|
return self.builder.constInt(0, .void);
|
|
}
|
|
|
|
/// Comptime-unrolled `inline for start..end (i) { }`. `start`/`end` must be
|
|
/// comptime-known. The body is lowered `end - start` times with the cursor
|
|
/// bound as an `int_val` comptime constant, so `xs[i]` over a pack
|
|
/// substitutes the concrete per-position argument each iteration.
|
|
fn lowerInlineRangeFor(self: *Lowering, fe: *const ast.ForExpr, end_node: *Node) Ref {
|
|
const start = self.evalComptimeInt(fe.iterable) orelse {
|
|
if (self.diagnostics) |d| d.addFmt(.err, fe.iterable.span, "inline for: range start is not a compile-time integer", .{});
|
|
return self.builder.constInt(0, .void);
|
|
};
|
|
const end = self.evalComptimeInt(end_node) orelse {
|
|
if (self.diagnostics) |d| d.addFmt(.err, end_node.span, "inline for: range end is not a compile-time integer", .{});
|
|
return self.builder.constInt(0, .void);
|
|
};
|
|
|
|
var i: i64 = start;
|
|
while (i < end) : (i += 1) {
|
|
var body_scope = Scope.init(self.alloc, self.scope);
|
|
const old_scope = self.scope;
|
|
self.scope = &body_scope;
|
|
|
|
// Bind the cursor both as a runtime value (constInt, for uses like
|
|
// `print(i)`) and as a comptime constant (for `xs[i]` substitution).
|
|
var had_prev = false;
|
|
var prev: ComptimeValue = undefined;
|
|
if (fe.capture_name.len > 0) {
|
|
body_scope.put(fe.capture_name, .{ .ref = self.builder.constInt(i, .s64), .ty = .s64, .is_alloca = false });
|
|
if (self.comptime_constants.get(fe.capture_name)) |p| {
|
|
had_prev = true;
|
|
prev = p;
|
|
}
|
|
self.comptime_constants.put(fe.capture_name, .{ .int_val = i }) catch {};
|
|
}
|
|
|
|
self.lowerBlock(fe.body);
|
|
|
|
if (fe.capture_name.len > 0) {
|
|
if (had_prev) {
|
|
self.comptime_constants.put(fe.capture_name, prev) catch {};
|
|
} else {
|
|
_ = self.comptime_constants.remove(fe.capture_name);
|
|
}
|
|
}
|
|
|
|
self.scope = old_scope;
|
|
body_scope.deinit();
|
|
|
|
if (self.currentBlockHasTerminator()) break;
|
|
}
|
|
|
|
return self.builder.constInt(0, .void);
|
|
}
|
|
|
|
/// Evaluate an `inline for` range bound to a comptime integer. Delegates to
|
|
/// the shared `program_index.evalConstIntExpr` — the SAME integer folder the
|
|
/// array dimension / Vector lane / value-param count paths build on — so a
|
|
/// literal, a comptime constant (cursor), a module/generic const
|
|
/// (`inline for 0..M`), a `<pack>.len` leaf, a DIRECT integral float
|
|
/// (`0..-2.0` → -2), and any constant-foldable expression over those
|
|
/// (`inline for 0..(M + 1)`) all resolve identically. A range bound is an
|
|
/// ENDPOINT, not a count (specs.md §2), so it deliberately does NOT take the
|
|
/// `foldCountI64` float-const-leaf fallback the count sites add: it accepts a
|
|
/// direct integral float but leaves a float-const-leaf expression to the int
|
|
/// folder (negatives are valid here, unlike a count).
|
|
fn evalComptimeInt(self: *Lowering, node: *const Node) ?i64 {
|
|
return program_index_mod.evalConstIntExpr(node, self);
|
|
}
|
|
|
|
fn lowerMatch(self: *Lowering, me: *const ast.MatchExpr) Ref {
|
|
// inline if match: evaluate at compile time, only lower the matching arm
|
|
if (me.is_comptime) {
|
|
if (self.evalComptimeMatch(me)) |arm_body| {
|
|
return self.lowerInlineBranch(arm_body);
|
|
}
|
|
// Couldn't evaluate — fall through to runtime
|
|
}
|
|
|
|
const is_type_match = isTypeCategoryMatch(me);
|
|
var subject = self.lowerExpr(me.subject);
|
|
var subject_ty = self.inferExprType(me.subject);
|
|
// A pointer subject (e.g. a `for xs: (*x)` element capture) — deref to
|
|
// the pointed-to union/enum so tag/payload extraction works.
|
|
if (!subject_ty.isBuiltin()) {
|
|
const sinfo = self.module.types.get(subject_ty);
|
|
if (sinfo == .pointer and !sinfo.pointer.pointee.isBuiltin()) {
|
|
const pinfo = self.module.types.get(sinfo.pointer.pointee);
|
|
if (pinfo == .tagged_union or pinfo == .@"enum") {
|
|
subject = self.builder.load(subject, sinfo.pointer.pointee);
|
|
subject_ty = sinfo.pointer.pointee;
|
|
}
|
|
}
|
|
}
|
|
const is_optional_match = blk: {
|
|
if (!subject_ty.isBuiltin()) {
|
|
const info = self.module.types.get(subject_ty);
|
|
break :blk info == .optional;
|
|
}
|
|
break :blk false;
|
|
};
|
|
// An error-set subject (`catch e == { case .X: ... }` / `if e == { ... }`):
|
|
// the value IS its u32 tag id, and `case .X` matches the global tag id
|
|
// of `X`. Used by ERR E1.5's catch match-body form.
|
|
const is_error_set_match = blk: {
|
|
if (!subject_ty.isBuiltin()) {
|
|
break :blk self.module.types.get(subject_ty) == .error_set;
|
|
}
|
|
break :blk false;
|
|
};
|
|
|
|
// Determine if the match produces a value (has non-void arms)
|
|
// For type-category matches (inside any_to_string), only produce value when force_block_value
|
|
// For regular enum/optional matches, always produce value if arms are non-void
|
|
var inferred_result = self.inferMatchResultType(me);
|
|
// Arms not statically inferable (bare enum literals etc.): only a
|
|
// value-position match (`force_block_value`) needs a concrete result —
|
|
// use the contextually expected type. A statement match with non-value
|
|
// arms is a side-effect (void); don't let a leaked `target_type` turn
|
|
// it into a value match.
|
|
if (inferred_result == .unresolved) {
|
|
inferred_result = if (self.force_block_value) (self.target_type orelse .unresolved) else .void;
|
|
}
|
|
const is_value = if (is_type_match) self.force_block_value else (self.force_block_value or (inferred_result != .void and inferred_result != .unresolved));
|
|
const result_type: TypeId = if (is_value) inferred_result else .void;
|
|
// A fully-diverging match (`result_type == .noreturn` — every arm
|
|
// `return`s / `raise`s / etc.) produces no value, so it builds no
|
|
// merge phi; its arms terminate and the merge block is unreachable.
|
|
const has_value_merge = is_value and result_type != .void and result_type != .noreturn;
|
|
const merge_params: []const TypeId = if (has_value_merge) &.{result_type} else &.{};
|
|
const merge_bb = self.freshBlockWithParams("match.merge", merge_params);
|
|
|
|
// Build arm blocks
|
|
var default_bb: ?BlockId = null;
|
|
var arm_blocks = std.ArrayList(BlockId).empty;
|
|
defer arm_blocks.deinit(self.alloc);
|
|
for (me.arms) |_| {
|
|
arm_blocks.append(self.alloc, self.freshBlock("match.arm")) catch unreachable;
|
|
}
|
|
|
|
// Build case list and pre-collect type tags per arm
|
|
var cases = std.ArrayList(inst_mod.SwitchBranch.Case).empty;
|
|
defer cases.deinit(self.alloc);
|
|
var arm_tag_values = std.ArrayList([]const u64).empty;
|
|
defer arm_tag_values.deinit(self.alloc);
|
|
|
|
for (me.arms, 0..) |arm, i| {
|
|
if (arm.pattern == null) {
|
|
default_bb = arm_blocks.items[i];
|
|
arm_tag_values.append(self.alloc, &.{}) catch unreachable;
|
|
continue;
|
|
}
|
|
const pat = arm.pattern.?;
|
|
|
|
if (is_type_match) {
|
|
// Type-category match: resolve category name to tag values
|
|
const name = switch (pat.data) {
|
|
.identifier => |id| id.name,
|
|
.type_expr => |te| te.name,
|
|
else => "",
|
|
};
|
|
const tag_values = self.resolveTypeCategoryTags(name);
|
|
arm_tag_values.append(self.alloc, tag_values) catch unreachable;
|
|
for (tag_values) |tag| {
|
|
cases.append(self.alloc, .{
|
|
.value = @intCast(tag),
|
|
.target = arm_blocks.items[i],
|
|
.args = &.{},
|
|
}) catch unreachable;
|
|
}
|
|
} else if (is_optional_match) {
|
|
// Optional match: .some → 1 (has_value=true), .none → 0
|
|
arm_tag_values.append(self.alloc, &.{}) catch unreachable;
|
|
const pat_name = switch (pat.data) {
|
|
.enum_literal => |el| el.name,
|
|
.identifier => |id| id.name,
|
|
else => "",
|
|
};
|
|
const case_val: u64 = if (std.mem.eql(u8, pat_name, "some")) 1 else 0;
|
|
cases.append(self.alloc, .{
|
|
.value = @intCast(case_val),
|
|
.target = arm_blocks.items[i],
|
|
.args = &.{},
|
|
}) catch unreachable;
|
|
} else {
|
|
// Enum/value match: resolve variant name to actual tag value
|
|
arm_tag_values.append(self.alloc, &.{}) catch unreachable;
|
|
const case_val: u64 = blk: {
|
|
const pat_name = switch (pat.data) {
|
|
.enum_literal => |el| el.name,
|
|
.identifier => |id| id.name,
|
|
.int_literal => |il| break :blk @intCast(il.value),
|
|
.bool_literal => |bl| break :blk @as(u64, if (bl.value) 1 else 0),
|
|
else => break :blk @as(u64, @intCast(i)),
|
|
};
|
|
// Look up variant value in the subject's type
|
|
if (!subject_ty.isBuiltin()) {
|
|
const ty_info = self.module.types.get(subject_ty);
|
|
if (ty_info == .tagged_union) {
|
|
for (ty_info.tagged_union.fields, 0..) |f, vi| {
|
|
const vname = self.module.types.strings.get(f.name);
|
|
if (std.mem.eql(u8, vname, pat_name)) {
|
|
if (ty_info.tagged_union.explicit_tag_values) |vals| {
|
|
if (vi < vals.len) break :blk @intCast(@as(u64, @bitCast(vals[vi])));
|
|
}
|
|
break :blk @intCast(vi);
|
|
}
|
|
}
|
|
if (self.diagnostics) |diags| {
|
|
const ty_name = self.formatTypeName(subject_ty);
|
|
diags.addFmt(.err, pat.span, "no variant '{s}' on type '{s}'", .{ pat_name, ty_name });
|
|
}
|
|
} else if (ty_info == .@"enum") {
|
|
for (ty_info.@"enum".variants, 0..) |v, vi| {
|
|
const vname = self.module.types.strings.get(v);
|
|
if (std.mem.eql(u8, vname, pat_name)) {
|
|
if (ty_info.@"enum".explicit_values) |vals| {
|
|
if (vi < vals.len) break :blk @intCast(@as(u64, @bitCast(vals[vi])));
|
|
}
|
|
break :blk @intCast(vi);
|
|
}
|
|
}
|
|
if (self.diagnostics) |diags| {
|
|
const ty_name = self.formatTypeName(subject_ty);
|
|
diags.addFmt(.err, pat.span, "no variant '{s}' on type '{s}'", .{ pat_name, ty_name });
|
|
}
|
|
} else if (ty_info == .error_set) {
|
|
// `case .X` matches the global tag id of `X`.
|
|
break :blk @intCast(self.module.types.internTag(pat_name));
|
|
}
|
|
}
|
|
break :blk @intCast(i);
|
|
};
|
|
cases.append(self.alloc, .{
|
|
.value = @intCast(case_val),
|
|
.target = arm_blocks.items[i],
|
|
.args = &.{},
|
|
}) catch unreachable;
|
|
}
|
|
}
|
|
|
|
// If no default arm, create an unreachable default
|
|
if (default_bb == null) {
|
|
default_bb = self.freshBlock("match.unr");
|
|
}
|
|
|
|
// Switch on the subject (for type match, subject is either a
|
|
// bare TypeId (s64) or an Any-shaped Type value — unbox in the
|
|
// latter case so the switch sees the i64 type id).
|
|
const tag = if (is_type_match) tag_blk: {
|
|
if (subject_ty == .any) {
|
|
break :tag_blk self.builder.emit(.{ .unbox_any = .{ .operand = subject } }, .s64);
|
|
}
|
|
break :tag_blk subject;
|
|
} else if (is_optional_match) self.builder.emit(.{ .optional_has_value = .{ .operand = subject } }, .bool) else if (is_error_set_match) subject else blk: {
|
|
// Determine actual tag type from union info (e.g. u32 for SDL_Event)
|
|
const tag_ty: TypeId = tt: {
|
|
if (!subject_ty.isBuiltin()) {
|
|
const ty_info = self.module.types.get(subject_ty);
|
|
if (ty_info == .tagged_union) break :tt ty_info.tagged_union.tag_type;
|
|
}
|
|
break :tt .s32;
|
|
};
|
|
break :blk self.builder.enumTag(subject, tag_ty);
|
|
};
|
|
self.builder.switchBr(tag, cases.items, default_bb.?, &.{});
|
|
|
|
// Lower each arm's body
|
|
for (me.arms, 0..) |arm, i| {
|
|
self.builder.switchToBlock(arm_blocks.items[i]);
|
|
|
|
// For type-match arms with empty tag lists, the arm is unreachable
|
|
// (no switch case targets it). Skip lowering to avoid invalid IR
|
|
// from runtime cast/dispatch with no matching types.
|
|
if (is_type_match and arm.pattern != null and arm_tag_values.items[i].len == 0) {
|
|
self.builder.emitUnreachable();
|
|
continue;
|
|
}
|
|
|
|
var arm_scope = Scope.init(self.alloc, self.scope);
|
|
const old_scope = self.scope;
|
|
self.scope = &arm_scope;
|
|
|
|
if (arm.capture) |capture_name| {
|
|
if (is_optional_match) {
|
|
// For optional match, unwrap the optional value
|
|
const opt_info = self.module.types.get(subject_ty);
|
|
const child_ty = if (opt_info == .optional) opt_info.optional.child else .s64;
|
|
const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = subject } }, child_ty);
|
|
arm_scope.put(capture_name, .{ .ref = unwrapped, .ty = child_ty, .is_alloca = false });
|
|
} else {
|
|
// Resolve actual variant index and payload type from the subject's type
|
|
var variant_idx: u32 = @intCast(i);
|
|
var payload_ty: TypeId = .unresolved;
|
|
if (arm.pattern) |arm_pat| {
|
|
const pat_name = switch (arm_pat.data) {
|
|
.enum_literal => |el| el.name,
|
|
.identifier => |id| id.name,
|
|
else => "",
|
|
};
|
|
if (!subject_ty.isBuiltin()) {
|
|
const ty_info = self.module.types.get(subject_ty);
|
|
if (ty_info == .tagged_union) {
|
|
for (ty_info.tagged_union.fields, 0..) |f, vi| {
|
|
const vname = self.module.types.strings.get(f.name);
|
|
if (std.mem.eql(u8, vname, pat_name)) {
|
|
variant_idx = @intCast(vi);
|
|
payload_ty = f.ty;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const payload = self.builder.emit(.{ .enum_payload = .{
|
|
.base = subject,
|
|
.field_index = variant_idx,
|
|
} }, payload_ty);
|
|
arm_scope.put(capture_name, .{ .ref = payload, .ty = payload_ty, .is_alloca = false });
|
|
}
|
|
}
|
|
|
|
// Set match arm context for runtime type dispatch
|
|
const saved_match_tags = self.current_match_tags;
|
|
if (is_type_match) {
|
|
self.current_match_tags = arm_tag_values.items[i];
|
|
}
|
|
|
|
if (has_value_merge) {
|
|
// Lower the arm body against the merge's result type so literals
|
|
// (and negated literals) in the arm pick the right width — the
|
|
// phi operands must all match `result_type` (issue 0066).
|
|
const saved_arm_target = self.target_type;
|
|
self.target_type = result_type;
|
|
const maybe_v = self.lowerBlockValue(arm.body);
|
|
self.target_type = saved_arm_target;
|
|
self.current_match_tags = saved_match_tags;
|
|
self.scope = old_scope;
|
|
arm_scope.deinit();
|
|
// Only materialize a value + branch to the merge when the arm
|
|
// body did NOT diverge. A diverging arm (e.g. `return x`) has
|
|
// already terminated its block; emitting the fallback const
|
|
// here would land AFTER the terminator (the issue-0057 bug).
|
|
if (!self.currentBlockHasTerminator()) {
|
|
var v = maybe_v orelse if (result_type == .string or !result_type.isBuiltin())
|
|
self.builder.constUndef(result_type)
|
|
else
|
|
self.builder.constInt(0, result_type);
|
|
const v_ty = self.builder.getRefType(v);
|
|
v = self.coerceToType(v, v_ty, result_type);
|
|
self.builder.br(merge_bb, &.{v});
|
|
}
|
|
} else {
|
|
self.lowerBlock(arm.body);
|
|
self.current_match_tags = saved_match_tags;
|
|
self.scope = old_scope;
|
|
arm_scope.deinit();
|
|
if (!self.currentBlockHasTerminator()) {
|
|
self.builder.br(merge_bb, &.{});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Emit default block if no explicit else arm
|
|
if (default_bb != null) {
|
|
var found_default = false;
|
|
for (me.arms) |arm| {
|
|
if (arm.pattern == null) { found_default = true; break; }
|
|
}
|
|
if (!found_default) {
|
|
self.builder.switchToBlock(default_bb.?);
|
|
if (is_type_match) {
|
|
// For type-category matches, unrecognized tags should skip to merge
|
|
// (e.g., optional types not covered by any_to_string categories)
|
|
if (has_value_merge) {
|
|
const default_val = self.builder.constUndef(result_type);
|
|
self.builder.br(merge_bb, &.{default_val});
|
|
} else {
|
|
self.builder.br(merge_bb, &.{});
|
|
}
|
|
} else {
|
|
// For non-exhaustive matches (union/enum with unhandled variants),
|
|
// fall through to merge instead of unreachable
|
|
const is_exhaustive = blk: {
|
|
if (!subject_ty.isBuiltin()) {
|
|
const ty_info = self.module.types.get(subject_ty);
|
|
if (ty_info == .tagged_union) {
|
|
break :blk cases.items.len >= ty_info.tagged_union.fields.len;
|
|
} else if (ty_info == .@"enum") {
|
|
break :blk cases.items.len >= ty_info.@"enum".variants.len;
|
|
}
|
|
}
|
|
break :blk false;
|
|
};
|
|
if (is_exhaustive) {
|
|
self.builder.emitUnreachable();
|
|
} else if (has_value_merge) {
|
|
const default_val = self.builder.constUndef(result_type);
|
|
self.builder.br(merge_bb, &.{default_val});
|
|
} else {
|
|
self.builder.br(merge_bb, &.{});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.builder.switchToBlock(merge_bb);
|
|
if (has_value_merge) {
|
|
return self.builder.blockParam(merge_bb, 0, result_type);
|
|
}
|
|
return self.builder.constInt(0, .void);
|
|
}
|
|
|
|
fn lowerBreak(self: *Lowering) Ref {
|
|
if (self.break_target) |target| {
|
|
self.builder.br(target, &.{});
|
|
}
|
|
return Ref.none;
|
|
}
|
|
|
|
fn lowerContinue(self: *Lowering) Ref {
|
|
if (self.continue_target) |target| {
|
|
self.builder.br(target, &.{});
|
|
}
|
|
return Ref.none;
|
|
}
|
|
|
|
// ── Struct/enum/union ops ───────────────────────────────────────
|
|
|
|
fn lowerStructLiteral(self: *Lowering, sl: *const ast.StructLiteral, span: ast.Span) Ref {
|
|
// Check for tagged enum construction: .Variant.{ payload_fields }
|
|
// This happens when type_expr is an enum_literal and target_type is a union
|
|
if (sl.type_expr) |te| {
|
|
if (te.data == .enum_literal) {
|
|
const variant_name = te.data.enum_literal.name;
|
|
const union_ty = self.target_type orelse .unresolved;
|
|
if (!union_ty.isBuiltin()) {
|
|
const union_info = self.module.types.get(union_ty);
|
|
if (union_info == .tagged_union) {
|
|
return self.lowerTaggedEnumLiteral(sl, variant_name, union_ty, union_info.tagged_union, span);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// `.{ name = ... }` against a tagged-union target_type. Reject:
|
|
// the only valid construction forms are `.variant(payload)` and
|
|
// `.variant.{ field, ... }`. Falling through would lower the
|
|
// user's values straight into the `(tag, payload_bytes)` slot
|
|
// pair and emit IR that LLVM later rejects.
|
|
if (sl.type_expr == null and sl.struct_name == null) {
|
|
const tu_ty = self.target_type orelse .unresolved;
|
|
if (!tu_ty.isBuiltin()) {
|
|
const tu_info = self.module.types.get(tu_ty);
|
|
if (tu_info == .tagged_union) {
|
|
if (sl.field_inits.len > 0 and sl.field_inits[0].name != null) {
|
|
const first_name = sl.field_inits[0].name.?;
|
|
if (self.diagnostics) |diags| {
|
|
const ty_name = self.formatTypeName(tu_ty);
|
|
if (self.findTaggedVariant(tu_info.tagged_union, first_name) != null) {
|
|
diags.addFmt(
|
|
.err,
|
|
span,
|
|
"cannot construct tagged union '{s}' from `.{{ {s} = ... }}`; use `.{s}(...)` or `.{s}.{{ ... }}`",
|
|
.{ ty_name, first_name, first_name, first_name },
|
|
);
|
|
} else {
|
|
self.emitBadVariant(tu_ty, tu_info.tagged_union, first_name, span);
|
|
}
|
|
}
|
|
return self.builder.enumInit(0, Ref.none, tu_ty);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const ty: TypeId = if (sl.struct_name) |name|
|
|
// Source-aware (E2): a bare struct-literal type name resolves to the
|
|
// querying source's OWN same-name author, not the global `findByName`
|
|
// first-match — so `Box.{...}` in module B builds B's `Box`, never a
|
|
// flat-imported A's. `.undeclared`/`.pending` keep the empty-struct
|
|
// stub (byte-identical to the legacy `findByName orelse intern`);
|
|
// `.ambiguous`/`.not_visible` surface their loud diagnostic + poison.
|
|
self.resolveNominalLeaf(name, false, span)
|
|
else if (sl.type_expr) |te|
|
|
// Generic struct literal: Pair(s32).{ ... } — resolve type from type_expr
|
|
self.resolveTypeWithBindings(te)
|
|
else self.target_type orelse .unresolved;
|
|
|
|
// Get struct field types for coercion and ordering
|
|
const struct_fields = self.getStructFields(ty);
|
|
|
|
// Look up field defaults from AST
|
|
const struct_name_for_defaults = if (sl.struct_name) |n| n else if (!ty.isBuiltin()) blk: {
|
|
const ti = self.module.types.get(ty);
|
|
break :blk if (ti == .@"struct") self.module.types.getString(ti.@"struct".name) else @as(?[]const u8, null);
|
|
} else @as(?[]const u8, null);
|
|
const field_defaults: []const ?*const Node = if (struct_name_for_defaults) |sn|
|
|
(self.struct_defaults_map.get(sn) orelse &.{})
|
|
else
|
|
&.{};
|
|
|
|
// Check if any field_init has a name (named literal)
|
|
const has_names = sl.field_inits.len > 0 and sl.field_inits[0].name != null;
|
|
|
|
if (has_names and struct_fields.len > 0) {
|
|
// Named literal: reorder fields to match struct declaration order
|
|
// First, lower all field values in source order (to preserve evaluation order)
|
|
var lowered = std.ArrayList(struct { val: Ref, name: []const u8, node: *const Node }).empty;
|
|
defer lowered.deinit(self.alloc);
|
|
for (sl.field_inits) |fi| {
|
|
const saved_tt = self.target_type;
|
|
// Set target_type to the field's declared type so array literals
|
|
// know if the target is a vector, etc.
|
|
if (fi.name) |fname| {
|
|
for (struct_fields) |sf| {
|
|
if (std.mem.eql(u8, self.module.types.getString(sf.name), fname)) {
|
|
self.target_type = sf.ty;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
const val = self.lowerExpr(fi.value);
|
|
self.target_type = saved_tt;
|
|
lowered.append(self.alloc, .{
|
|
.val = val,
|
|
.name = fi.name orelse "",
|
|
.node = fi.value,
|
|
}) catch unreachable;
|
|
}
|
|
|
|
// Build fields in declaration order
|
|
var fields = std.ArrayList(Ref).empty;
|
|
defer fields.deinit(self.alloc);
|
|
for (struct_fields, 0..) |sf, fi| {
|
|
const sf_name = self.module.types.getString(sf.name);
|
|
// Find the matching lowered value
|
|
var found = false;
|
|
for (lowered.items) |l| {
|
|
if (std.mem.eql(u8, l.name, sf_name)) {
|
|
var val = l.val;
|
|
const src_ty = self.builder.getRefType(val);
|
|
val = self.coerceToType(val, src_ty, sf.ty);
|
|
fields.append(self.alloc, val) catch unreachable;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
// Field not specified — use default if available, else zero
|
|
if (fi < field_defaults.len) {
|
|
if (field_defaults[fi]) |default_expr| {
|
|
// Coerce the default to the field type at the IR
|
|
// level (the implicit narrowing rule) so a float
|
|
// default folds/errors here instead of being
|
|
// silently bit-coerced by the backend.
|
|
fields.append(self.alloc, self.lowerCoercedDefault(default_expr, sf.ty)) catch unreachable;
|
|
} else {
|
|
fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable;
|
|
}
|
|
} else {
|
|
fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable;
|
|
}
|
|
}
|
|
}
|
|
|
|
const result = self.builder.structInit(fields.items, ty);
|
|
if (sl.init_block) |ib| {
|
|
return self.lowerInitBlock(result, ty, ib);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Positional literal: use source order
|
|
var fields = std.ArrayList(Ref).empty;
|
|
defer fields.deinit(self.alloc);
|
|
|
|
for (sl.field_inits, 0..) |fi, i| {
|
|
var val = self.lowerExpr(fi.value);
|
|
// Coerce field value to match struct field type
|
|
if (i < struct_fields.len) {
|
|
const src_ty = self.inferExprType(fi.value);
|
|
val = self.coerceToType(val, src_ty, struct_fields[i].ty);
|
|
}
|
|
fields.append(self.alloc, val) catch unreachable;
|
|
}
|
|
|
|
// Pad missing fields with defaults or zeroes
|
|
if (fields.items.len < struct_fields.len) {
|
|
for (struct_fields[fields.items.len..], fields.items.len..) |sf, fi| {
|
|
if (fi < field_defaults.len) {
|
|
if (field_defaults[fi]) |default_expr| {
|
|
fields.append(self.alloc, self.lowerCoercedDefault(default_expr, sf.ty)) catch unreachable;
|
|
continue;
|
|
}
|
|
}
|
|
fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable;
|
|
}
|
|
}
|
|
|
|
const result = self.builder.structInit(fields.items, ty);
|
|
|
|
// Lower init block if present
|
|
if (sl.init_block) |ib| {
|
|
return self.lowerInitBlock(result, ty, ib);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// Lower an init block: store struct value to alloca, bind `self`, execute block, reload.
|
|
fn lowerInitBlock(self: *Lowering, struct_val: Ref, ty: TypeId, ib: *const Node) Ref {
|
|
// Store struct value to a temporary alloca
|
|
const ptr_ty = self.module.types.ptrTo(ty);
|
|
const slot = self.builder.alloca(ty);
|
|
self.builder.store(slot, struct_val);
|
|
|
|
// Create a nested scope with `self` bound to the alloca pointer
|
|
var init_scope = Scope.init(self.alloc, self.scope);
|
|
defer init_scope.deinit();
|
|
const saved_scope = self.scope;
|
|
self.scope = &init_scope;
|
|
|
|
// `self` is the pointer to the struct (not an alloca itself — it IS the pointer value)
|
|
init_scope.put("self", .{ .ref = slot, .ty = ptr_ty, .is_alloca = false });
|
|
|
|
// Lower the init block body
|
|
self.lowerBlock(ib);
|
|
|
|
// Restore scope
|
|
self.scope = saved_scope;
|
|
|
|
// Load and return the (possibly modified) struct value
|
|
return self.builder.load(slot, ty);
|
|
}
|
|
|
|
/// Get the field list for a struct TypeId, or empty if not a struct.
|
|
pub fn getStructFields(self: *Lowering, ty: TypeId) []const types.TypeInfo.StructInfo.Field {
|
|
if (ty.isBuiltin()) return &.{};
|
|
var resolved = ty;
|
|
const info = self.module.types.get(resolved);
|
|
// Dereference pointer types to get to the underlying struct
|
|
if (info == .pointer) {
|
|
resolved = info.pointer.pointee;
|
|
if (resolved.isBuiltin()) return &.{};
|
|
const inner = self.module.types.get(resolved);
|
|
return switch (inner) {
|
|
.@"struct" => |s| s.fields,
|
|
else => &.{},
|
|
};
|
|
}
|
|
return switch (info) {
|
|
.@"struct" => |s| s.fields,
|
|
else => &.{},
|
|
};
|
|
}
|
|
|
|
/// If a method's first param expects a pointer (*T) but we're passing T by value,
|
|
/// swap the first arg with the alloca address (implicit address-of).
|
|
fn fixupMethodReceiver(self: *Lowering, method_args: *std.ArrayList(Ref), func: *const Function, obj_node: *const Node, obj_ty: TypeId) void {
|
|
// Skip the implicit __sx_ctx param when inspecting the receiver slot.
|
|
const skip: usize = if (func.has_implicit_ctx) 1 else 0;
|
|
if (func.params.len <= skip) return;
|
|
const first_param_ty = func.params[skip].ty;
|
|
// Check if first param expects a pointer
|
|
if (!first_param_ty.isBuiltin()) {
|
|
const pi = self.module.types.get(first_param_ty);
|
|
if (pi == .pointer) {
|
|
// If obj is already a pointer type, it's already correct (no addr_of needed)
|
|
if (!obj_ty.isBuiltin()) {
|
|
const oi = self.module.types.get(obj_ty);
|
|
if (oi == .pointer) return; // already a pointer
|
|
}
|
|
// Method expects *T — pass the address of the receiver (value type in alloca)
|
|
if (obj_node.data == .identifier) {
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(obj_node.data.identifier.name)) |binding| {
|
|
if (binding.is_alloca) {
|
|
const ptr_ty = self.module.types.ptrTo(binding.ty);
|
|
method_args.items[0] = self.builder.emit(.{ .addr_of = .{ .operand = binding.ref } }, ptr_ty);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Field access: obj.field.method() → GEP to field, pass pointer directly.
|
|
// This avoids copying the struct value (mutations through *T must be visible).
|
|
if (obj_node.data == .field_access) {
|
|
const gep_ref = self.lowerExprAsPtr(obj_node);
|
|
// GEP returns a pointer in LLVM but its IR type is the field value type.
|
|
// Wrap with addr_of (no-op in LLVM) to set the IR type to *T,
|
|
// preventing coerceCallArgs from doing a spurious alloca+store.
|
|
const ptr_ty = self.module.types.ptrTo(obj_ty);
|
|
method_args.items[0] = self.builder.emit(.{ .addr_of = .{ .operand = gep_ref } }, ptr_ty);
|
|
return;
|
|
}
|
|
// General case: alloca+store the value and pass the alloca pointer
|
|
{
|
|
const slot = self.builder.alloca(obj_ty);
|
|
self.builder.store(slot, method_args.items[0]);
|
|
method_args.items[0] = slot;
|
|
}
|
|
} else {
|
|
// Method expects a value `T` but the receiver is a `*T` (e.g. a
|
|
// `for xs: (*x)` by-ref capture) — deref to pass the value.
|
|
if (!obj_ty.isBuiltin()) {
|
|
const oi = self.module.types.get(obj_ty);
|
|
if (oi == .pointer and oi.pointer.pointee == first_param_ty) {
|
|
method_args.items[0] = self.builder.load(method_args.items[0], first_param_ty);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Get the name of a struct type (dereferencing pointers). Returns null for non-struct types.
|
|
fn getStructTypeName(self: *Lowering, ty: TypeId) ?[]const u8 {
|
|
if (ty.isBuiltin()) {
|
|
// Map builtin types to their names for method resolution (e.g., s64.eq)
|
|
return builtinTypeName(ty);
|
|
}
|
|
var resolved = ty;
|
|
const info = self.module.types.get(resolved);
|
|
if (info == .pointer) {
|
|
resolved = info.pointer.pointee;
|
|
if (resolved.isBuiltin()) return builtinTypeName(resolved);
|
|
}
|
|
const ri = self.module.types.get(resolved);
|
|
return switch (ri) {
|
|
.@"struct" => |s| self.module.types.getString(s.name),
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
fn builtinTypeName(ty: TypeId) ?[]const u8 {
|
|
return switch (ty) {
|
|
.s8 => "s8",
|
|
.s16 => "s16",
|
|
.s32 => "s32",
|
|
.s64 => "s64",
|
|
.u8 => "u8",
|
|
.u16 => "u16",
|
|
.u32 => "u32",
|
|
.u64 => "u64",
|
|
.f32 => "f32",
|
|
.f64 => "f64",
|
|
.bool => "bool",
|
|
.string => "string",
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
/// Resolve the type of a named field on a given type.
|
|
fn resolveFieldType(self: *Lowering, ty: TypeId, field: []const u8) TypeId {
|
|
if (std.mem.eql(u8, field, "len")) return .s64;
|
|
if (std.mem.eql(u8, field, "ptr")) {
|
|
const elem_ty = self.getElementType(ty);
|
|
return self.module.types.manyPtrTo(elem_ty);
|
|
}
|
|
const field_name_id = self.module.types.internString(field);
|
|
// Check union fields + promoted fields
|
|
if (!ty.isBuiltin()) {
|
|
const info = self.module.types.get(ty);
|
|
const u_fields: ?[]const types.TypeInfo.StructInfo.Field = switch (info) {
|
|
.@"union" => |u| u.fields,
|
|
.tagged_union => |u| u.fields,
|
|
else => null,
|
|
};
|
|
if (u_fields) |ufields| {
|
|
for (ufields) |f| {
|
|
if (f.name == field_name_id) return f.ty;
|
|
// Check promoted fields from anonymous struct variants
|
|
if (!f.ty.isBuiltin()) {
|
|
const fi = self.module.types.get(f.ty);
|
|
if (fi == .@"struct") {
|
|
for (fi.@"struct".fields) |sf| {
|
|
if (sf.name == field_name_id) return sf.ty;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Check tuple fields
|
|
if (!ty.isBuiltin()) {
|
|
const ti = self.module.types.get(ty);
|
|
if (ti == .tuple) {
|
|
const tuple = ti.tuple;
|
|
// Try named fields
|
|
if (tuple.names) |names| {
|
|
for (names, 0..) |name_id, i| {
|
|
if (name_id == field_name_id) return tuple.fields[i];
|
|
}
|
|
}
|
|
// Try numeric index
|
|
const idx = std.fmt.parseInt(usize, field, 10) catch {
|
|
return .unresolved;
|
|
};
|
|
if (idx < tuple.fields.len) return tuple.fields[idx];
|
|
return .unresolved;
|
|
}
|
|
}
|
|
const struct_fields = self.getStructFields(ty);
|
|
for (struct_fields) |f| {
|
|
if (f.name == field_name_id) return f.ty;
|
|
}
|
|
return .unresolved;
|
|
}
|
|
|
|
fn lowerFieldAccess(self: *Lowering, fa: *const ast.FieldAccess, span: ast.Span) Ref {
|
|
// `error.X` — an error-tag literal. The `error` keyword in expression
|
|
// position parses as identifier "error" (E0.2), so `error.X` is a
|
|
// field access we intercept here. `error` is reserved, so this is
|
|
// unambiguous (no struct/pack can be named `error`).
|
|
if (fa.object.data == .identifier and std.mem.eql(u8, fa.object.data.identifier.name, "error")) {
|
|
return self.lowerErrorTagLiteral(fa.field, span);
|
|
}
|
|
|
|
// Pack-arity intercept: `<pack_name>.len` in a pack-fn mono's
|
|
// body resolves to the comptime-known N. The mono doesn't
|
|
// materialise the `[]Any` slice that the inline path used, so
|
|
// `args` isn't in scope as a value.
|
|
if (self.pack_param_count) |ppc| {
|
|
if (fa.object.data == .identifier and std.mem.eql(u8, fa.field, "len")) {
|
|
if (ppc.get(fa.object.data.identifier.name)) |n| {
|
|
return self.builder.constInt(@as(i64, @intCast(n)), .s64);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pack value projection: `xs.<m>` where `<m>` is a (zero-arg) method of
|
|
// the pack's constraint protocol projects it over every element →
|
|
// a tuple `(xs[0].<m>(), …, xs[N-1].<m>())`. (`xs.len` handled above.)
|
|
if (self.pack_constraint) |pcon| {
|
|
if (fa.object.data == .identifier) {
|
|
if (pcon.get(fa.object.data.identifier.name)) |proto| {
|
|
if (self.lookupProtocolField(proto, fa.field) != null) {
|
|
return self.lowerPackValueProjection(fa.object.data.identifier.name, fa.field, span);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Interface-only enforcement (Decision): a member access on a
|
|
// constrained pack element `xs[i].<m>` may only name a method of the
|
|
// constraint protocol — not an arbitrary concrete field. Checked here,
|
|
// on the `xs[i]` (index_expr) base, BEFORE substitution erases the
|
|
// "constrained to P" context. Protocol method CALLS go through the call
|
|
// path; a method name passes this check (it's in the protocol).
|
|
if (self.pack_constraint) |pcon| {
|
|
if (fa.object.data == .index_expr and fa.object.data.index_expr.object.data == .identifier) {
|
|
const base_name = fa.object.data.index_expr.object.data.identifier.name;
|
|
if (pcon.get(base_name)) |proto| {
|
|
if (self.lookupProtocolField(proto, fa.field) == null) {
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, span, "'{s}' is not part of protocol '{s}' — a pack element exposes only the protocol's interface", .{ fa.field, proto });
|
|
}
|
|
return self.builder.constInt(0, .void);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for struct constant access: Struct.CONST
|
|
if (fa.object.data == .identifier) {
|
|
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fa.object.data.identifier.name, fa.field }) catch fa.field;
|
|
if (self.struct_const_map.get(qualified)) |info| {
|
|
return self.lowerStructConstant(info);
|
|
}
|
|
}
|
|
|
|
// Numeric-limit accessor: `<IntType>.min` / `.max` folds to a comptime
|
|
// const of the queried type (sibling of the identifier-receiver
|
|
// intercepts above). Placed AFTER `Struct.CONST` so a user const named
|
|
// `min`/`max` wins on its own struct; a builtin type name can never
|
|
// name a user struct (reserved — issue 0076), so they never collide.
|
|
if (self.lowerNumericLimit(fa, span)) |ref| return ref;
|
|
|
|
// M1.3 — `obj.class` on any Obj-C-class pointer lowers to
|
|
// `object_getClass(obj)`. Sugar; the receiver is opaque so
|
|
// we don't auto-deref. Returns `Class` (alias for *void;
|
|
// typed Class(T) parameterization is M1.1.b).
|
|
if (std.mem.eql(u8, fa.field, "class")) {
|
|
const expr_ty = self.inferExprType(fa.object);
|
|
if (self.objc().isObjcClassPointer(expr_ty)) {
|
|
const obj_ref = self.lowerExpr(fa.object);
|
|
const ptr_void = self.module.types.ptrTo(.void);
|
|
const get_class_fid = self.ensureCRuntimeDecl("object_getClass", &.{ptr_void}, ptr_void);
|
|
const args = self.alloc.alloc(Ref, 1) catch unreachable;
|
|
args[0] = obj_ref;
|
|
return self.builder.emit(.{ .call = .{ .callee = get_class_fid, .args = args } }, ptr_void);
|
|
}
|
|
}
|
|
|
|
// M2.2 — `obj.field` where `field` is declared with `#property`
|
|
// on a foreign Obj-C class lowers as `[obj field]` (the synthesized
|
|
// getter). Receiver stays opaque — no auto-deref.
|
|
if (self.lookupObjcPropertyOnPointer(fa.object, fa.field)) |prop| {
|
|
return self.lowerObjcPropertyGetter(fa.object, prop, fa.field, span);
|
|
}
|
|
|
|
// M1.2 A.3 — `self.field` (or `obj.field`) on a *sx-defined-class
|
|
// pointer for a plain instance field (NOT a #property) lowers as
|
|
// `object_getIvar(obj, load(__<Cls>_state_ivar))` + struct_gep on
|
|
// the state struct + load. The receiver is the opaque Obj-C id
|
|
// (matching Apple's `self` semantics); the state lives in the
|
|
// hidden `__sx_state` ivar.
|
|
if (self.lookupObjcDefinedStateFieldOnPointer(fa.object, fa.field)) |info| {
|
|
return self.lowerObjcDefinedStateFieldRead(fa.object, info);
|
|
}
|
|
|
|
var obj = self.lowerExpr(fa.object);
|
|
var obj_ty = self.inferExprType(fa.object);
|
|
|
|
// Auto-deref: if the object is a pointer to a struct, load through it
|
|
if (!obj_ty.isBuiltin()) {
|
|
const ptr_info = self.module.types.get(obj_ty);
|
|
if (ptr_info == .pointer) {
|
|
const pointee = ptr_info.pointer.pointee;
|
|
obj = self.builder.load(obj, pointee);
|
|
obj_ty = pointee;
|
|
}
|
|
}
|
|
|
|
// Special fields on slices/strings (NOT structs with .len/.ptr fields)
|
|
if (std.mem.eql(u8, fa.field, "len") or std.mem.eql(u8, fa.field, "ptr")) {
|
|
// Only use length/data_ptr for slice, string, array, vector types
|
|
const is_special = obj_ty == .string or (if (!obj_ty.isBuiltin()) blk: {
|
|
const info = self.module.types.get(obj_ty);
|
|
break :blk info == .slice or info == .array or info == .vector;
|
|
} else false);
|
|
|
|
if (is_special) {
|
|
if (std.mem.eql(u8, fa.field, "len")) {
|
|
return self.builder.emit(.{ .length = .{ .operand = obj } }, .s64);
|
|
}
|
|
{
|
|
const elem_ty = self.getElementType(obj_ty);
|
|
const mp_ty = self.module.types.manyPtrTo(elem_ty);
|
|
return self.builder.emit(.{ .data_ptr = .{ .operand = obj } }, mp_ty);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Optional chaining: p?.field
|
|
if (fa.is_optional) {
|
|
return self.lowerOptionalChain(obj, fa, span);
|
|
}
|
|
|
|
return self.lowerFieldAccessOnType(obj, obj_ty, fa.field, span);
|
|
}
|
|
|
|
/// True when an `.identifier` receiver text resolves to an in-scope VALUE
|
|
/// binding rather than a builtin type. A backtick raw identifier (F0.6) can
|
|
/// bind a value whose spelling shadows a builtin type name (`` `f64 := … ``);
|
|
/// such a value is reachable through the same three sources the ordinary
|
|
/// identifier field-access path consults (see `expr_typer` `.identifier`
|
|
/// arm): lexical `scope`, program `global_names`, and module value
|
|
/// constants `module_const_map`. The numeric-limit intercept must defer to
|
|
/// ordinary field access whenever ANY of the three binds the name, so a
|
|
/// raw value field read is never hijacked into a numeric-limit fold
|
|
/// (issues 0092 local / 0093 global + module-const). A single helper used
|
|
/// by both lowering and inference keeps the two resolvers in lockstep
|
|
/// (issue-0083 two-resolver defect class).
|
|
pub fn identifierBindsValue(self: *Lowering, name: []const u8) bool {
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(name) != null) return true;
|
|
}
|
|
if (self.program_index.global_names.get(name) != null) return true;
|
|
if (self.program_index.module_const_map.get(name) != null) return true;
|
|
return false;
|
|
}
|
|
|
|
/// Numeric-limit accessor intercept (`<Type>.min`/`.max`/`.epsilon`/
|
|
/// `.min_positive`/`.true_min`/`.inf`/`.nan`), a sibling of the `error.X` /
|
|
/// `Struct.CONST` / pack-arity identifier-receiver intercepts in
|
|
/// `lowerFieldAccess`. Folds the limit to a comptime const of the queried
|
|
/// type via the shared `TypeResolver` logic (no second computor) + the
|
|
/// existing `constInt` / `constFloat` const paths:
|
|
/// - integer `.min`/`.max` → `constInt` (NL.1, via `integerLimitFor`);
|
|
/// - float `.min`/`.max`/`.epsilon`/`.min_positive`/`.true_min`/`.inf`/
|
|
/// `.nan` → `constFloat` (via `floatLimitFor`).
|
|
/// Returns null when the field is not a limit accessor, or the receiver is not
|
|
/// a builtin type (a user struct → ordinary field lowering reports
|
|
/// field-not-found). Two clean diagnostics (then a placeholder, so lowering
|
|
/// finishes and `hasErrors()` aborts the build):
|
|
/// - a FLOAT-only accessor on an integer type (`s32.epsilon`, `u8.inf`);
|
|
/// - any accessor on a builtin NON-numeric receiver
|
|
/// (`bool`/`string`/`void`/`Any`/`noreturn`).
|
|
fn lowerNumericLimit(self: *Lowering, fa: *const ast.FieldAccess, span: ast.Span) ?Ref {
|
|
const name = switch (fa.object.data) {
|
|
.identifier => |id| id.name,
|
|
.type_expr => |te| te.name,
|
|
else => return null,
|
|
};
|
|
if (!TypeResolver.isLimitField(fa.field)) return null;
|
|
const ty = TypeResolver.resolveBuiltinName(name, &self.module.types) orelse return null;
|
|
|
|
// A backtick raw identifier (F0.6) can bind a value whose spelling
|
|
// shadows a builtin type name (`` `f64 := … ``). Field access on that
|
|
// value is an ordinary field read, not a numeric-limit fold — defer to
|
|
// the normal field-access path when the receiver identifier resolves to
|
|
// a value binding through any of scope / globals / module consts
|
|
// (issues 0092, 0093). A `.type_expr` receiver is unambiguously a type
|
|
// and can never be value-shadowed.
|
|
if (fa.object.data == .identifier and self.identifierBindsValue(name)) return null;
|
|
|
|
if (TypeResolver.integerLimitFor(name, fa.field)) |value| {
|
|
return self.builder.constInt(value, ty);
|
|
}
|
|
if (TypeResolver.floatLimitFor(name, fa.field)) |value| {
|
|
return self.builder.constFloat(value, ty);
|
|
}
|
|
// The field is a limit accessor, but it does not apply to this type.
|
|
if (self.diagnostics) |d| {
|
|
if (TypeResolver.integerWidthSign(name) != null) {
|
|
// Integer receiver + a float-only accessor.
|
|
d.addFmt(.err, span, "type '{s}' has no '.{s}' — '.{s}' applies only to float types (f32/f64); integer types expose only '.min'/'.max'", .{ name, fa.field, fa.field });
|
|
} else {
|
|
// Non-numeric builtin receiver (bool/string/void/Any/noreturn).
|
|
d.addFmt(.err, span, "type '{s}' has no '.{s}' — numeric limits apply only to integer and float types", .{ name, fa.field });
|
|
}
|
|
}
|
|
return self.emitPlaceholder(fa.field);
|
|
}
|
|
|
|
/// Lower each pack element to a Ref: `pack_name[i]` when `method` is null,
|
|
/// or `pack_name[i].method()` when given. Synthesizes the index/field/call
|
|
/// AST per element and lowers it (substitution turns `xs[i]` into the
|
|
/// concrete arg; UFCS dispatches the method). Caller owns the returned slice.
|
|
fn lowerPackElems(self: *Lowering, pack_name: []const u8, method: ?[]const u8, span: ast.Span) []Ref {
|
|
const n: u32 = if (self.pack_param_count) |ppc| (ppc.get(pack_name) orelse 0) else 0;
|
|
var refs = std.ArrayList(Ref).empty;
|
|
var i: u32 = 0;
|
|
while (i < n) : (i += 1) {
|
|
const id_node = self.alloc.create(Node) catch break;
|
|
id_node.* = .{ .span = span, .data = .{ .identifier = .{ .name = pack_name } } };
|
|
const idx_node = self.alloc.create(Node) catch break;
|
|
idx_node.* = .{ .span = span, .data = .{ .int_literal = .{ .value = @intCast(i) } } };
|
|
const index_node = self.alloc.create(Node) catch break;
|
|
index_node.* = .{ .span = span, .data = .{ .index_expr = .{ .object = id_node, .index = idx_node } } };
|
|
var elem_node = index_node;
|
|
if (method) |m| {
|
|
const fa_node = self.alloc.create(Node) catch break;
|
|
fa_node.* = .{ .span = span, .data = .{ .field_access = .{ .object = index_node, .field = m } } };
|
|
const call_node = self.alloc.create(Node) catch break;
|
|
call_node.* = .{ .span = span, .data = .{ .call = .{ .callee = fa_node, .args = &.{} } } };
|
|
elem_node = call_node;
|
|
}
|
|
refs.append(self.alloc, self.lowerExpr(elem_node)) catch break;
|
|
}
|
|
return refs.toOwnedSlice(self.alloc) catch &.{};
|
|
}
|
|
|
|
/// Value-position pack projection `xs.<method>`: call the (zero-arg)
|
|
/// protocol method on each element and collect the results into a tuple
|
|
/// `(xs[0].<method>(), …, xs[N-1].<method>())`. N=0 yields the empty tuple.
|
|
fn lowerPackValueProjection(self: *Lowering, pack_name: []const u8, method: []const u8, span: ast.Span) Ref {
|
|
const refs = self.lowerPackElems(pack_name, method, span);
|
|
defer self.alloc.free(refs);
|
|
var tys = std.ArrayList(TypeId).empty;
|
|
defer tys.deinit(self.alloc);
|
|
for (refs) |r| tys.append(self.alloc, self.builder.getRefType(r)) catch {};
|
|
const tuple_ty = self.module.types.intern(.{ .tuple = .{
|
|
.fields = self.alloc.dupe(TypeId, tys.items) catch return self.builder.constInt(0, .void),
|
|
.names = null,
|
|
} });
|
|
const owned = self.alloc.dupe(Ref, refs) catch return self.builder.constInt(0, .void);
|
|
return self.builder.emit(.{ .tuple_init = .{ .fields = owned } }, tuple_ty);
|
|
}
|
|
|
|
/// If `operand` is a pack spread — `..xs` (bare pack) or `..xs.method`
|
|
/// (per-element projection) — return the per-element Refs to splice into a
|
|
/// call's positional args. Null when it's not a pack spread (e.g. a runtime
|
|
/// slice `..arr`, handled by the slice-variadic path). Caller owns the slice.
|
|
fn packSpreadRefs(self: *Lowering, operand: *const Node, span: ast.Span) ?[]Ref {
|
|
const ppc = self.pack_param_count orelse return null;
|
|
switch (operand.data) {
|
|
.identifier => |id| {
|
|
if (ppc.contains(id.name)) return self.lowerPackElems(id.name, null, span);
|
|
},
|
|
.field_access => |fa| {
|
|
if (fa.object.data == .identifier and ppc.contains(fa.object.data.identifier.name)) {
|
|
return self.lowerPackElems(fa.object.data.identifier.name, fa.field, span);
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Lower a struct-level constant value (e.g., Phys.GRAVITY).
|
|
fn lowerStructConstant(self: *Lowering, info: StructConstInfo) Ref {
|
|
const val_node = info.value;
|
|
return switch (val_node.data) {
|
|
.int_literal => |lit| self.builder.constInt(lit.value, info.ty orelse .s64),
|
|
.float_literal => |lit| self.builder.constFloat(lit.value, info.ty orelse .f64),
|
|
.bool_literal => |lit| self.builder.constBool(lit.value),
|
|
.string_literal => |lit| self.builder.constString(self.module.types.internString(lit.raw)),
|
|
else => self.lowerExpr(val_node),
|
|
};
|
|
}
|
|
|
|
/// Lower optional chaining: `p?.field` where p is ?T
|
|
/// Produces ?FieldType: some(unwrap(p).field) if p has value, else null
|
|
/// If FieldType is already optional (?U), flattens to ?U (no double wrapping)
|
|
fn lowerOptionalChain(self: *Lowering, obj: Ref, fa: *const ast.FieldAccess, span: ast.Span) Ref {
|
|
const obj_ty = self.inferExprType(fa.object);
|
|
// Get the inner (non-optional) type
|
|
const inner_ty = if (!obj_ty.isBuiltin()) blk: {
|
|
const info = self.module.types.get(obj_ty);
|
|
break :blk if (info == .optional) info.optional.child else obj_ty;
|
|
} else obj_ty;
|
|
|
|
// Get the field type on the inner type
|
|
const field_ty = self.resolveFieldType(inner_ty, fa.field);
|
|
// If field is already optional, flatten (don't double-wrap)
|
|
const field_already_optional = if (!field_ty.isBuiltin()) self.module.types.get(field_ty) == .optional else false;
|
|
const result_ty = if (field_already_optional) field_ty else self.module.types.optionalOf(field_ty);
|
|
|
|
// Check if optional has value
|
|
const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = obj } }, .bool);
|
|
|
|
// Create blocks
|
|
const some_bb = self.freshBlock("chain.some");
|
|
const none_bb = self.freshBlock("chain.none");
|
|
const merge_bb = self.freshBlockWithParams("chain.merge", &.{result_ty});
|
|
|
|
self.builder.condBr(has_val, some_bb, &.{}, none_bb, &.{});
|
|
|
|
// Some: unwrap, access field (already ?FieldType if flattened, else wrap)
|
|
self.builder.switchToBlock(some_bb);
|
|
const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = obj } }, inner_ty);
|
|
const field_val = self.lowerFieldAccessOnType(unwrapped, inner_ty, fa.field, span);
|
|
const some_result = if (field_already_optional) field_val else self.builder.emit(.{ .optional_wrap = .{ .operand = field_val } }, result_ty);
|
|
self.builder.br(merge_bb, &.{some_result});
|
|
|
|
// None: produce null optional
|
|
self.builder.switchToBlock(none_bb);
|
|
const none_result = self.builder.constNull(result_ty);
|
|
self.builder.br(merge_bb, &.{none_result});
|
|
|
|
// Merge
|
|
self.builder.switchToBlock(merge_bb);
|
|
return self.builder.blockParam(merge_bb, 0, result_ty);
|
|
}
|
|
|
|
/// Field access on a known type (shared by regular field access and optional chaining)
|
|
/// Map a Vector swizzle component (`.x`/`.y`/`.z`/`.w` or the colour
|
|
/// aliases `.r`/`.g`/`.b`/`.a`) to its lane index. Returns null for any
|
|
/// other field name so the read path (`lowerFieldAccessOnType`) and the
|
|
/// write path (`lowerAssignment`) share one resolver and reject a
|
|
/// non-lane field identically (issue 0086).
|
|
pub fn vectorLaneIndex(field: []const u8) ?u32 {
|
|
if (std.mem.eql(u8, field, "x") or std.mem.eql(u8, field, "r")) return 0;
|
|
if (std.mem.eql(u8, field, "y") or std.mem.eql(u8, field, "g")) return 1;
|
|
if (std.mem.eql(u8, field, "z") or std.mem.eql(u8, field, "b")) return 2;
|
|
if (std.mem.eql(u8, field, "w") or std.mem.eql(u8, field, "a")) return 3;
|
|
return null;
|
|
}
|
|
|
|
fn lowerFieldAccessOnType(self: *Lowering, obj: Ref, obj_ty: TypeId, field: []const u8, span: ast.Span) Ref {
|
|
const field_name_id = self.module.types.internString(field);
|
|
|
|
// Check if it's a union type
|
|
if (!obj_ty.isBuiltin()) {
|
|
const info = self.module.types.get(obj_ty);
|
|
switch (info) {
|
|
.tagged_union => |u| {
|
|
// .tag → extract the enum tag value with the correct tag type
|
|
if (std.mem.eql(u8, field, "tag")) {
|
|
return self.builder.emit(.{ .enum_tag = .{ .operand = obj } }, u.tag_type);
|
|
}
|
|
// Tagged union — use enum_payload
|
|
for (u.fields, 0..) |f, i| {
|
|
if (f.name == field_name_id) {
|
|
return self.builder.emit(.{ .enum_payload = .{ .base = obj, .field_index = @intCast(i) } }, f.ty);
|
|
}
|
|
}
|
|
// Check promoted fields from anonymous struct variants
|
|
for (u.fields) |f| {
|
|
if (!f.ty.isBuiltin()) {
|
|
const field_info = self.module.types.get(f.ty);
|
|
if (field_info == .@"struct") {
|
|
for (field_info.@"struct".fields, 0..) |sf, si| {
|
|
if (sf.name == field_name_id) {
|
|
const reinterpreted = self.builder.emit(.{ .union_get = .{ .base = obj, .field_index = 0 } }, f.ty);
|
|
return self.builder.structGet(reinterpreted, @intCast(si), sf.ty);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
.@"union" => |u| {
|
|
// Untagged union — use union_get to reinterpret bytes
|
|
for (u.fields, 0..) |f, i| {
|
|
if (f.name == field_name_id) {
|
|
return self.builder.emit(.{ .union_get = .{ .base = obj, .field_index = @intCast(i) } }, f.ty);
|
|
}
|
|
}
|
|
// Check promoted fields from anonymous struct variants
|
|
for (u.fields) |f| {
|
|
if (!f.ty.isBuiltin()) {
|
|
const field_info = self.module.types.get(f.ty);
|
|
if (field_info == .@"struct") {
|
|
for (field_info.@"struct".fields, 0..) |sf, si| {
|
|
if (sf.name == field_name_id) {
|
|
const reinterpreted = self.builder.emit(.{ .union_get = .{ .base = obj, .field_index = 0 } }, f.ty);
|
|
return self.builder.structGet(reinterpreted, @intCast(si), sf.ty);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
// Vector lane access: .x/.y/.z/.w (or colour aliases .r/.g/.b/.a) →
|
|
// lane 0/1/2/3. Shares lane-index resolution with the write path
|
|
// (lowerAssignment) via vectorLaneIndex; a non-lane field falls
|
|
// through to the field-not-found error below.
|
|
if (!obj_ty.isBuiltin()) {
|
|
const vinfo = self.module.types.get(obj_ty);
|
|
if (vinfo == .vector) {
|
|
if (Lowering.vectorLaneIndex(field)) |vidx| {
|
|
return self.builder.structGet(obj, vidx, vinfo.vector.element);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Closure field access: .fn_ptr → field 0, .env → field 1
|
|
if (!obj_ty.isBuiltin()) {
|
|
const cinfo = self.module.types.get(obj_ty);
|
|
if (cinfo == .closure) {
|
|
if (std.mem.eql(u8, field, "fn_ptr")) {
|
|
const fn_ptr_ty = self.module.types.ptrTo(.void);
|
|
return self.builder.structGet(obj, 0, fn_ptr_ty);
|
|
} else if (std.mem.eql(u8, field, "env")) {
|
|
const env_ty = self.module.types.ptrTo(.void);
|
|
return self.builder.structGet(obj, 1, env_ty);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tuple field access: .0, .1, etc. or named fields
|
|
if (!obj_ty.isBuiltin()) {
|
|
const tinfo = self.module.types.get(obj_ty);
|
|
if (tinfo == .tuple) {
|
|
const tuple = tinfo.tuple;
|
|
// Try named fields first
|
|
if (tuple.names) |names| {
|
|
for (names, 0..) |name_id, i| {
|
|
if (name_id == field_name_id) {
|
|
return self.builder.structGet(obj, @intCast(i), tuple.fields[i]);
|
|
}
|
|
}
|
|
}
|
|
// Try numeric index (e.g., "0", "1")
|
|
const idx = std.fmt.parseInt(u32, field, 10) catch {
|
|
return self.emitFieldError(obj_ty, field, span);
|
|
};
|
|
if (idx < tuple.fields.len) {
|
|
return self.builder.structGet(obj, idx, tuple.fields[idx]);
|
|
}
|
|
return self.emitFieldError(obj_ty, field, span);
|
|
}
|
|
}
|
|
|
|
// Resolve struct field index and type
|
|
const struct_fields = self.getStructFields(obj_ty);
|
|
for (struct_fields, 0..) |f, i| {
|
|
if (f.name == field_name_id) {
|
|
return self.builder.structGet(obj, @intCast(i), f.ty);
|
|
}
|
|
}
|
|
|
|
return self.emitFieldError(obj_ty, field, span);
|
|
}
|
|
|
|
fn lowerEnumLiteral(self: *Lowering, el: *const ast.EnumLiteral) Ref {
|
|
const target = self.target_type orelse .unresolved;
|
|
const tag = self.resolveVariantValue(target, el.name);
|
|
return self.builder.enumInit(tag, Ref.none, target);
|
|
}
|
|
|
|
/// Lower an `error.X` tag literal to its global tag id (a `u32`). When the
|
|
/// destination context (`target_type`) is a named error set, the value is
|
|
/// typed as that set and `X`'s membership is validated; otherwise the value
|
|
/// is the raw `u32` global tag id (per the spec's context rule).
|
|
fn lowerErrorTagLiteral(self: *Lowering, tag_name: []const u8, span: ast.Span) Ref {
|
|
const tag_id = self.module.types.internTag(tag_name);
|
|
if (self.target_type) |t| {
|
|
if (!t.isBuiltin()) {
|
|
const info = self.module.types.get(t);
|
|
if (info == .error_set) {
|
|
// The bare-`!` inferred placeholder (reserved name "!") accepts
|
|
// any tag — its members aren't known until the whole-program SCC
|
|
// pass (E1.4) folds in every raised tag. Skip membership for it.
|
|
if (!std.mem.eql(u8, self.module.types.getString(info.error_set.name), "!")) {
|
|
var in_set = false;
|
|
for (info.error_set.tags) |member| {
|
|
if (member == tag_id) {
|
|
in_set = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!in_set) {
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, span, "error tag 'error.{s}' is not in error set '{s}'", .{ tag_name, self.module.types.getString(info.error_set.name) });
|
|
}
|
|
}
|
|
}
|
|
return self.builder.constInt(@as(i64, @intCast(tag_id)), t);
|
|
}
|
|
}
|
|
}
|
|
return self.builder.constInt(@as(i64, @intCast(tag_id)), .u32);
|
|
}
|
|
|
|
/// Lower a tagged enum construction: .Variant.{ field_inits }
|
|
/// The struct literal provides the payload fields; we wrap them in an enum_init.
|
|
fn lowerTaggedEnumLiteral(
|
|
self: *Lowering,
|
|
sl: *const ast.StructLiteral,
|
|
variant_name: []const u8,
|
|
union_ty: TypeId,
|
|
union_info: types.TypeInfo.TaggedUnionInfo,
|
|
span: ast.Span,
|
|
) Ref {
|
|
if (self.findTaggedVariant(union_info, variant_name) == null) {
|
|
self.emitBadVariant(union_ty, union_info, variant_name, span);
|
|
return self.builder.enumInit(0, Ref.none, union_ty);
|
|
}
|
|
|
|
const tag = self.resolveVariantValue(union_ty, variant_name);
|
|
const name_id = self.module.types.internString(variant_name);
|
|
|
|
// Find the payload type for this variant
|
|
var payload_ty: TypeId = .void;
|
|
for (union_info.fields) |f| {
|
|
if (f.name == name_id) {
|
|
payload_ty = f.ty;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (payload_ty == .void or sl.field_inits.len == 0) {
|
|
// No payload or no fields — just tag
|
|
return self.builder.enumInit(tag, Ref.none, union_ty);
|
|
}
|
|
|
|
// Lower the payload as a struct init of the payload type
|
|
const saved_tt = self.target_type;
|
|
self.target_type = payload_ty;
|
|
const payload_fields = self.getStructFields(payload_ty);
|
|
|
|
var fields = std.ArrayList(Ref).empty;
|
|
defer fields.deinit(self.alloc);
|
|
|
|
for (sl.field_inits, 0..) |fi, i| {
|
|
if (i < payload_fields.len) {
|
|
const saved_inner = self.target_type;
|
|
self.target_type = payload_fields[i].ty;
|
|
var val = self.lowerExpr(fi.value);
|
|
self.target_type = saved_inner;
|
|
const src_ty = self.inferExprType(fi.value);
|
|
val = self.coerceToType(val, src_ty, payload_fields[i].ty);
|
|
fields.append(self.alloc, val) catch unreachable;
|
|
} else {
|
|
fields.append(self.alloc, self.lowerExpr(fi.value)) catch unreachable;
|
|
}
|
|
}
|
|
|
|
// Pad missing payload fields with zeroes
|
|
if (fields.items.len < payload_fields.len) {
|
|
for (payload_fields[fields.items.len..]) |sf| {
|
|
fields.append(self.alloc, self.zeroValue(sf.ty)) catch unreachable;
|
|
}
|
|
}
|
|
|
|
const payload = self.builder.structInit(fields.items, payload_ty);
|
|
self.target_type = saved_tt;
|
|
|
|
return self.builder.enumInit(tag, payload, union_ty);
|
|
}
|
|
|
|
fn findTaggedVariant(
|
|
self: *Lowering,
|
|
union_info: types.TypeInfo.TaggedUnionInfo,
|
|
variant_name: []const u8,
|
|
) ?usize {
|
|
const name_id = self.module.types.internString(variant_name);
|
|
for (union_info.fields, 0..) |f, i| {
|
|
if (f.name == name_id) return i;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
fn emitBadVariant(
|
|
self: *Lowering,
|
|
union_ty: TypeId,
|
|
union_info: types.TypeInfo.TaggedUnionInfo,
|
|
variant_name: []const u8,
|
|
span: ast.Span,
|
|
) void {
|
|
const diags = self.diagnostics orelse return;
|
|
const ty_name = self.formatTypeName(union_ty);
|
|
var list: std.ArrayList(u8) = .empty;
|
|
for (union_info.fields, 0..) |f, i| {
|
|
if (i > 0) list.appendSlice(self.alloc, ", ") catch return;
|
|
list.appendSlice(self.alloc, self.module.types.getString(f.name)) catch return;
|
|
}
|
|
diags.addFmt(
|
|
.err,
|
|
span,
|
|
"'{s}' is not a variant of '{s}' (variants are: {s})",
|
|
.{ variant_name, ty_name, list.items },
|
|
);
|
|
}
|
|
|
|
/// Resolve a variant name to its runtime value (flags: power-of-2, regular: index).
|
|
fn resolveVariantValue(self: *Lowering, ty: TypeId, variant_name: []const u8) u32 {
|
|
if (ty.isBuiltin()) return 0;
|
|
const info = self.module.types.get(ty);
|
|
const name_id = self.module.types.internString(variant_name);
|
|
switch (info) {
|
|
.@"enum" => |e| {
|
|
for (e.variants, 0..) |v, i| {
|
|
if (v == name_id) {
|
|
if (e.explicit_values) |vals| {
|
|
if (i < vals.len) return @intCast(@as(u64, @bitCast(vals[i])));
|
|
}
|
|
return @intCast(i);
|
|
}
|
|
}
|
|
},
|
|
.tagged_union => |u| {
|
|
for (u.fields, 0..) |f, i| {
|
|
if (f.name == name_id) {
|
|
if (u.explicit_tag_values) |vals| {
|
|
if (i < vals.len) return @intCast(@as(u64, @bitCast(vals[i])));
|
|
}
|
|
return @intCast(i);
|
|
}
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/// Resolve a variant name to its tag index within an enum or union type.
|
|
pub fn resolveVariantIndex(self: *Lowering, ty: TypeId, variant_name: []const u8) u32 {
|
|
if (ty.isBuiltin()) return 0;
|
|
const info = self.module.types.get(ty);
|
|
const name_id = self.module.types.internString(variant_name);
|
|
switch (info) {
|
|
.tagged_union => |u| {
|
|
for (u.fields, 0..) |f, i| {
|
|
if (f.name == name_id) return @intCast(i);
|
|
}
|
|
},
|
|
.@"enum" => |e| {
|
|
for (e.variants, 0..) |v, i| {
|
|
if (v == name_id) return @intCast(i);
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
fn lowerArrayLiteral(self: *Lowering, al: *const ast.ArrayLiteral) Ref {
|
|
var elems = std.ArrayList(Ref).empty;
|
|
defer elems.deinit(self.alloc);
|
|
|
|
// Determine element type: explicit type_expr > target_type > inference
|
|
var elem_ty: TypeId = .unresolved;
|
|
var from_target = false;
|
|
var is_vector = false;
|
|
|
|
// First, check explicit type annotation on the literal (e.g. Vector(3,f32).[1,2,3])
|
|
if (al.type_expr) |te| {
|
|
const resolved = self.resolveArrayLiteralType(te);
|
|
if (resolved != .unresolved) {
|
|
if (!resolved.isBuiltin()) {
|
|
const info = self.module.types.get(resolved);
|
|
switch (info) {
|
|
.array => |a| {
|
|
elem_ty = a.element;
|
|
from_target = true;
|
|
},
|
|
.vector => |v| {
|
|
elem_ty = v.element;
|
|
from_target = true;
|
|
is_vector = true;
|
|
},
|
|
.slice => |s| {
|
|
elem_ty = s.element;
|
|
from_target = true;
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!from_target) {
|
|
if (self.target_type) |tt| {
|
|
if (!tt.isBuiltin()) {
|
|
const info = self.module.types.get(tt);
|
|
switch (info) {
|
|
.array => |a| {
|
|
elem_ty = a.element;
|
|
from_target = true;
|
|
},
|
|
.slice => |s| {
|
|
elem_ty = s.element;
|
|
from_target = true;
|
|
},
|
|
.vector => |v| {
|
|
elem_ty = v.element;
|
|
from_target = true;
|
|
is_vector = true;
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!from_target and al.elements.len > 0) {
|
|
const inferred = self.inferExprType(al.elements[0]);
|
|
if (inferred != .void) elem_ty = inferred;
|
|
}
|
|
|
|
for (al.elements) |elem| {
|
|
const old_tt = self.target_type;
|
|
self.target_type = elem_ty;
|
|
var val = self.lowerExpr(elem);
|
|
self.target_type = old_tt;
|
|
// A nested `.[...]` element at a slice element type lowers to an
|
|
// aggregate array `[N]U` (lowerArrayLiteral always yields an array
|
|
// value); materialize it into a `[]U` slice so the element is a real
|
|
// {ptr,len} header rather than a raw array the callee would read its
|
|
// header off of (issue 0085). This per-element coercion recurses with
|
|
// the literal nesting, so `[][]T` and deeper coerce at every level.
|
|
if (!elem_ty.isBuiltin()) {
|
|
const ei = self.module.types.get(elem_ty);
|
|
if (ei == .slice) {
|
|
const val_ty = self.builder.getRefType(val);
|
|
if (!val_ty.isBuiltin()) {
|
|
const vi = self.module.types.get(val_ty);
|
|
if (vi == .array and vi.array.element == ei.slice.element) {
|
|
val = self.coerceToType(val, val_ty, elem_ty);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
elems.append(self.alloc, val) catch unreachable;
|
|
}
|
|
|
|
const result_ty = if (is_vector)
|
|
self.module.types.vectorOf(elem_ty, @intCast(al.elements.len))
|
|
else
|
|
self.module.types.arrayOf(elem_ty, @intCast(al.elements.len));
|
|
return self.builder.structInit(elems.items, result_ty);
|
|
}
|
|
|
|
/// Resolve the type annotation on an array literal (e.g. Vector(3,f32).[...]).
|
|
/// Handles call nodes (Vector(3,f32)), parameterized_type_expr, and identifier/type_expr.
|
|
fn resolveArrayLiteralType(self: *Lowering, te: *const Node) TypeId {
|
|
switch (te.data) {
|
|
.call => |cl| {
|
|
// Vector(3, f32) or Module.Vector(3, f32)
|
|
const callee_name = switch (cl.callee.data) {
|
|
.identifier => |id| id.name,
|
|
.field_access => |fa| fa.field,
|
|
else => return .unresolved,
|
|
};
|
|
if (std.mem.eql(u8, callee_name, "Vector")) {
|
|
if (cl.args.len == 2) {
|
|
const length = self.resolveVectorLane(cl.args[0]) orelse return .unresolved;
|
|
const elem = self.resolveTypeWithBindings(cl.args[1]);
|
|
return self.module.types.vectorOf(elem, length);
|
|
}
|
|
}
|
|
// Try as generic struct
|
|
if (self.program_index.struct_template_map.getPtr(callee_name)) |tmpl| {
|
|
return self.instantiateGenericStruct(tmpl, cl.args);
|
|
}
|
|
return .unresolved;
|
|
},
|
|
.parameterized_type_expr => |pt| return self.resolveParameterizedWithBindings(&pt, te.span),
|
|
.identifier => |id| {
|
|
const name_id = self.module.types.internString(id.name);
|
|
return self.module.types.findByName(name_id) orelse .unresolved;
|
|
},
|
|
.type_expr => return type_bridge.resolveAstType(te, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map),
|
|
.field_access => |fa| {
|
|
// Module.Type — try to resolve the field as a type name
|
|
const name_id = self.module.types.internString(fa.field);
|
|
return self.module.types.findByName(name_id) orelse .unresolved;
|
|
},
|
|
else => return .unresolved,
|
|
}
|
|
}
|
|
|
|
fn lowerIndexExpr(self: *Lowering, ie: *const ast.IndexExpr) Ref {
|
|
// Pack-arg substitution: `args[<int_literal>]` inside a body
|
|
// whose enclosing comptime call bound `args` as a pack name.
|
|
// Lowering the i-th call-site arg directly gives the concrete
|
|
// call-arg type — bypasses the `[]Any` slice boxing that would
|
|
// otherwise lose the type. Non-literal indices fall through to
|
|
// the standard slice indexing path.
|
|
if (self.packArgNodeAt(ie)) |arg_node| {
|
|
return self.lowerExpr(arg_node);
|
|
}
|
|
// Out-of-bounds pack indexing: object IS a pack name + index
|
|
// IS a comptime int literal but exceeds the pack arity. Emit
|
|
// a focused diagnostic so the user gets "pack index 2 out of
|
|
// bounds" instead of the generic "unresolved 'args'" that the
|
|
// fall-through scope-lookup would produce.
|
|
if (self.diagPackIndexOOB(ie)) {
|
|
return self.builder.constInt(0, .s64);
|
|
}
|
|
// Runtime index into a comptime-only pack (Decision 1): a pack has no
|
|
// runtime representation, so the index must be a compile-time constant.
|
|
// A runtime index is a hard error — clearer than the "unresolved
|
|
// '<pack>'" the slice-index fall-through would otherwise produce.
|
|
if (self.pack_param_count) |ppc| {
|
|
if (ie.object.data == .identifier) {
|
|
const pname = ie.object.data.identifier.name;
|
|
if (ppc.contains(pname) and self.comptimeIndexOf(ie.index) == null) {
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, ie.index.span, "pack '{s}' must be indexed by a compile-time constant — a pack is comptime-only and has no runtime value", .{pname});
|
|
}
|
|
return self.builder.constInt(0, .s64);
|
|
}
|
|
}
|
|
}
|
|
const obj = self.lowerExpr(ie.object);
|
|
const idx = self.lowerExpr(ie.index);
|
|
// Infer element type from the object's slice/array type
|
|
const obj_ty = self.inferExprType(ie.object);
|
|
const elem_ty = self.getElementType(obj_ty);
|
|
return self.builder.emit(.{ .index_get = .{ .lhs = obj, .rhs = idx } }, elem_ty);
|
|
}
|
|
|
|
/// Detect `<pack_name>[<int_literal>]` where the literal exceeds
|
|
/// the pack arity (or is negative). Emits a diagnostic and
|
|
/// returns true; caller skips the standard indexing path and
|
|
/// returns a placeholder Ref. Returns false for non-pack bases,
|
|
/// non-literal indices, or in-range indices.
|
|
fn diagPackIndexOOB(self: *Lowering, ie: *const ast.IndexExpr) bool {
|
|
const ppc = self.pack_param_count orelse return false;
|
|
if (ie.object.data != .identifier) return false;
|
|
const pack_name = ie.object.data.identifier.name;
|
|
const n = ppc.get(pack_name) orelse return false;
|
|
// Any comptime index (int literal or a comptime-constant cursor) that's
|
|
// out of range — runtime indices are handled by the caller's
|
|
// must-be-comptime check.
|
|
const raw: i64 = self.comptimeIndexOf(ie.index) orelse return false;
|
|
if (raw >= 0 and @as(u32, @intCast(raw)) < n) return false;
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, ie.index.span, "pack index {} out of bounds: '{s}' has {} element{s}", .{
|
|
raw, pack_name, n, if (n == 1) @as([]const u8, "") else @as([]const u8, "s"),
|
|
});
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// Returns the call-site arg AST node when `ie` matches
|
|
/// `<pack_name>[<comptime_int_literal>]` with the pack name bound
|
|
/// in the active `pack_arg_nodes` map and the index in range.
|
|
/// Otherwise null — caller falls back to standard slice indexing.
|
|
pub fn packArgNodeAt(self: *Lowering, ie: *const ast.IndexExpr) ?*const Node {
|
|
const pan = self.pack_arg_nodes orelse return null;
|
|
if (ie.object.data != .identifier) return null;
|
|
const arg_nodes = pan.get(ie.object.data.identifier.name) orelse return null;
|
|
const raw: i64 = self.comptimeIndexOf(ie.index) orelse return null;
|
|
if (raw < 0) return null;
|
|
const i: usize = @intCast(raw);
|
|
if (i >= arg_nodes.len) return null;
|
|
return arg_nodes[i];
|
|
}
|
|
|
|
/// Resolve an index expression to a comptime-known integer: a literal,
|
|
/// or an identifier bound to an `int_val` in `comptime_constants` (e.g.
|
|
/// the cursor of an `inline for 0..N (i)` unroll). Otherwise null.
|
|
pub fn comptimeIndexOf(self: *Lowering, index: *const Node) ?i64 {
|
|
switch (index.data) {
|
|
.int_literal => |lit| return lit.value,
|
|
.identifier => |id| {
|
|
if (self.comptime_constants.get(id.name)) |cv| {
|
|
switch (cv) {
|
|
.int_val => |iv| return iv,
|
|
else => return null,
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
else => return null,
|
|
}
|
|
}
|
|
|
|
const PackValueKind = enum { storage, call_arg, return_value, runtime_iter, generic };
|
|
|
|
/// `xs` is a pack name used where a runtime value is required. A pack is
|
|
/// comptime-only (Decision 1), so this is an error — with a context-tailored
|
|
/// suggestion for how to express the intent instead.
|
|
fn diagPackAsValue(self: *Lowering, name: []const u8, span: ast.Span, kind: PackValueKind) Ref {
|
|
if (self.diagnostics) |d| {
|
|
const id = d.addFmtId(.err, span, "pack '{s}' has no runtime value — a pack is comptime-only and can't be used as a value here", .{name});
|
|
switch (kind) {
|
|
.storage => d.addHelpFmt(id, span, null, "to store it, materialize a tuple: `(..{s})`", .{name}),
|
|
.call_arg => d.addHelpFmt(id, span, null, "to pass it to a `[]Any`/`[]P` parameter, materialize it with `xx {s}`", .{name}),
|
|
.return_value => d.addHelpFmt(id, span, null, "to return it, return a tuple `(..{s})` and make the return type that tuple", .{name}),
|
|
.runtime_iter => d.addHelpFmt(id, span, null, "to iterate at comptime use `inline for 0..{s}.len (i)`; for a runtime loop declare it as `..{s}: []P` (a protocol slice) instead of a pack", .{ name, name }),
|
|
.generic => d.addHelpFmt(id, span, null, "materialize a tuple `(..{s})` to store it, or `xx {s}` to convert it to an expected `[]Any`/`[]P` slice", .{ name, name }),
|
|
}
|
|
}
|
|
return self.emitPlaceholder(name);
|
|
}
|
|
|
|
/// True when `name` is a pack parameter bound in the current mono body.
|
|
fn isPackName(self: *Lowering, name: []const u8) bool {
|
|
const ppc = self.pack_param_count orelse return false;
|
|
return ppc.contains(name);
|
|
}
|
|
|
|
/// `xx <pack>` with a slice target: materialize the comptime pack into a
|
|
/// runtime `[]elem` by lowering each element node and boxing (`[]Any`) or
|
|
/// `xx`-erasing (`[]P`) it into a stack `[N]elem`, then return the slice.
|
|
/// This is the explicit pack→slice bridge (issue 0053).
|
|
fn lowerPackToSlice(self: *Lowering, pack_name: []const u8, slice_ty: TypeId) Ref {
|
|
const arg_nodes = (self.pack_arg_nodes orelse return self.builder.constInt(0, .unresolved)).get(pack_name) orelse
|
|
return self.builder.constInt(0, .unresolved);
|
|
const elem_ty = self.module.types.get(slice_ty).slice.element;
|
|
const is_any = elem_ty == .any;
|
|
const elem_is_protocol = blk: {
|
|
if (elem_ty.isBuiltin()) break :blk false;
|
|
const ei = self.module.types.get(elem_ty);
|
|
break :blk ei == .@"struct" and ei.@"struct".is_protocol;
|
|
};
|
|
const slice_slot = self.builder.alloca(slice_ty);
|
|
const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(elem_ty), slice_ty);
|
|
const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, slice_ty);
|
|
if (arg_nodes.len == 0) {
|
|
self.builder.store(ptr_gep, self.builder.constNull(self.module.types.ptrTo(elem_ty)));
|
|
self.builder.store(len_gep, self.builder.constInt(0, .s64));
|
|
return self.builder.load(slice_slot, slice_ty);
|
|
}
|
|
const array_ty = self.module.types.arrayOf(elem_ty, @intCast(arg_nodes.len));
|
|
const array_slot = self.builder.alloca(array_ty);
|
|
for (arg_nodes, 0..) |arg, i| {
|
|
var val = self.lowerExpr(arg);
|
|
var source_ty = self.inferExprType(arg);
|
|
if (source_ty == .unresolved) source_ty = self.builder.getRefType(val);
|
|
if (is_any) {
|
|
if (source_ty != .any) val = self.builder.boxAny(val, source_ty);
|
|
} else if (elem_is_protocol) {
|
|
if (source_ty != elem_ty) val = self.buildProtocolErasure(val, arg, source_ty, elem_ty);
|
|
}
|
|
const ep = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = self.builder.constInt(@intCast(i), .s64) } }, self.module.types.ptrTo(elem_ty));
|
|
self.builder.store(ep, val);
|
|
}
|
|
const data_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = self.builder.constInt(0, .s64) } }, self.module.types.ptrTo(elem_ty));
|
|
self.builder.store(ptr_gep, data_ptr);
|
|
self.builder.store(len_gep, self.builder.constInt(@intCast(arg_nodes.len), .s64));
|
|
return self.builder.load(slice_slot, slice_ty);
|
|
}
|
|
|
|
fn lowerSliceExpr(self: *Lowering, se: *const ast.SliceExpr) Ref {
|
|
const obj = self.lowerExpr(se.object);
|
|
const lo = if (se.start) |s| self.lowerExpr(s) else self.builder.constInt(0, .s64);
|
|
const hi = if (se.end) |e| self.lowerExpr(e) else self.builder.emit(.{ .length = .{ .operand = obj } }, .s64);
|
|
// Infer result slice type from the object
|
|
const obj_ty = self.inferExprType(se.object);
|
|
// Subslice of string stays string (same {ptr, i64} layout, correct type category)
|
|
if (obj_ty == .string) {
|
|
return self.builder.emit(.{ .subslice = .{ .base = obj, .lo = lo, .hi = hi } }, .string);
|
|
}
|
|
const elem_ty = self.getElementType(obj_ty);
|
|
const slice_ty = if (elem_ty != .void) self.module.types.sliceOf(elem_ty) else self.module.types.sliceOf(.u8);
|
|
return self.builder.emit(.{ .subslice = .{ .base = obj, .lo = lo, .hi = hi } }, slice_ty);
|
|
}
|
|
|
|
fn lowerTupleLiteral(self: *Lowering, tl: *const ast.TupleLiteral) Ref {
|
|
var elems = std.ArrayList(Ref).empty;
|
|
defer elems.deinit(self.alloc);
|
|
var field_type_ids = std.ArrayList(TypeId).empty;
|
|
defer field_type_ids.deinit(self.alloc);
|
|
var name_ids = std.ArrayList(types.StringId).empty;
|
|
defer name_ids.deinit(self.alloc);
|
|
var has_names = false;
|
|
|
|
// A tuple_init's element values must match its field types exactly
|
|
// (LLVM `insertvalue` does no implicit conversion). When a contextual
|
|
// target tuple of matching arity is in scope (annotation, assignment
|
|
// LHS, call/return slot), its field types drive element lowering so an
|
|
// ambient scalar `target_type` (e.g. the enclosing fn's int return
|
|
// type) can't narrow an element below its field width. Otherwise each
|
|
// element's type is inferred independently.
|
|
// A pack-spread element `(..xs)` / `(..xs.method)` expands to N fields,
|
|
// so element-count ≠ field-count and a contextual target tuple can't be
|
|
// aligned by index — infer field types from the expanded refs instead.
|
|
var has_spread = false;
|
|
for (tl.elements) |elem| {
|
|
if (elem.value.data == .spread_expr) has_spread = true;
|
|
}
|
|
|
|
// Contextual target tuple field types. Without a spread we require
|
|
// exact arity (existing behavior); with a spread we index positionally
|
|
// by output position (so `(..sources)` into a `(VL(T0), …)` field coerces
|
|
// / erases each spliced element to its slot's type).
|
|
var target_fields: ?[]const TypeId = null;
|
|
if (self.target_type) |tt| {
|
|
if (!tt.isBuiltin()) {
|
|
const tinfo = self.module.types.get(tt);
|
|
if (tinfo == .tuple and (has_spread or tinfo.tuple.fields.len == tl.elements.len)) {
|
|
target_fields = tinfo.tuple.fields;
|
|
}
|
|
}
|
|
}
|
|
|
|
const saved_target = self.target_type;
|
|
var out_idx: usize = 0;
|
|
for (tl.elements) |elem| {
|
|
// Pack-spread element → splice its per-element values as fields.
|
|
if (elem.value.data == .spread_expr) {
|
|
const sp_operand = elem.value.data.spread_expr.operand;
|
|
if (self.packSpreadRefs(sp_operand, elem.value.span)) |refs| {
|
|
defer self.alloc.free(refs);
|
|
// Element AST nodes (for protocol-erasure lvalue/name fallback)
|
|
// when the spread is a bare pack name.
|
|
const elem_nodes: ?[]const *const Node = if (sp_operand.data == .identifier and self.pack_arg_nodes != null)
|
|
self.pack_arg_nodes.?.get(sp_operand.data.identifier.name)
|
|
else
|
|
null;
|
|
for (refs, 0..) |r, ri| {
|
|
var val = r;
|
|
var vty = self.builder.getRefType(r);
|
|
if (target_fields) |tf| {
|
|
if (out_idx < tf.len and tf[out_idx] != vty and tf[out_idx] != .void) {
|
|
const want = tf[out_idx];
|
|
const node = if (elem_nodes) |ens| (if (ri < ens.len) ens[ri] else elem.value) else elem.value;
|
|
val = self.coerceOrErase(r, vty, want, node);
|
|
vty = want;
|
|
}
|
|
}
|
|
elems.append(self.alloc, val) catch unreachable;
|
|
field_type_ids.append(self.alloc, vty) catch unreachable;
|
|
name_ids.append(self.alloc, self.module.types.internString("")) catch unreachable;
|
|
out_idx += 1;
|
|
}
|
|
continue;
|
|
}
|
|
// Not a pack spread (e.g. tuple-value spread) — not yet handled.
|
|
_ = self.lowerExpr(elem.value); // surfaces the spread_expr diagnostic
|
|
continue;
|
|
}
|
|
const field_ty = if (target_fields) |tf| (if (out_idx < tf.len) tf[out_idx] else self.inferExprType(elem.value)) else self.inferExprType(elem.value);
|
|
self.target_type = field_ty;
|
|
var val = self.lowerExpr(elem.value);
|
|
self.target_type = saved_target;
|
|
const val_ty = self.builder.getRefType(val);
|
|
if (val_ty != field_ty and val_ty != .void) {
|
|
val = self.coerceToType(val, val_ty, field_ty);
|
|
}
|
|
elems.append(self.alloc, val) catch unreachable;
|
|
field_type_ids.append(self.alloc, field_ty) catch unreachable;
|
|
if (elem.name) |name| {
|
|
name_ids.append(self.alloc, self.module.types.internString(name)) catch unreachable;
|
|
has_names = true;
|
|
} else {
|
|
name_ids.append(self.alloc, self.module.types.internString("")) catch unreachable;
|
|
}
|
|
out_idx += 1;
|
|
}
|
|
|
|
// Reuse the contextual target tuple type when it drove lowering so the
|
|
// value's type identity (incl. field names) matches the destination
|
|
// slot; otherwise build the tuple type from the inferred fields.
|
|
const tuple_ty = if (target_fields != null and self.target_type != null)
|
|
self.target_type.?
|
|
else
|
|
self.module.types.intern(.{ .tuple = .{
|
|
.fields = self.alloc.dupe(TypeId, field_type_ids.items) catch unreachable,
|
|
.names = if (has_names) self.alloc.dupe(types.StringId, name_ids.items) catch unreachable else null,
|
|
} });
|
|
|
|
const owned = self.alloc.dupe(Ref, elems.items) catch unreachable;
|
|
return self.builder.emit(.{ .tuple_init = .{ .fields = owned } }, tuple_ty);
|
|
}
|
|
|
|
fn lowerDerefExpr(self: *Lowering, de: *const ast.DerefExpr) Ref {
|
|
const ptr = self.lowerExpr(de.operand);
|
|
// Resolve pointee type from the pointer type.
|
|
const ptr_ty = self.inferExprType(de.operand);
|
|
if (!ptr_ty.isBuiltin()) {
|
|
const info = self.module.types.get(ptr_ty);
|
|
if (info == .pointer) {
|
|
return self.builder.emit(.{ .deref = .{ .operand = ptr } }, info.pointer.pointee);
|
|
}
|
|
}
|
|
// Operand isn't a pointer — `.*` is invalid. Diagnose here instead of
|
|
// emitting a `.deref` with an `.unresolved` result type, which would
|
|
// otherwise slip through to emit_llvm's "unresolved type reached LLVM
|
|
// emission" panic with no source location.
|
|
if (self.diagnostics) |d| {
|
|
d.addFmt(.err, de.operand.span, "cannot dereference with `.*`: '{s}' is not a pointer", .{self.formatTypeName(ptr_ty)});
|
|
}
|
|
return ptr;
|
|
}
|
|
|
|
fn lowerForceUnwrap(self: *Lowering, fu: *const ast.ForceUnwrap) Ref {
|
|
const val = self.lowerExpr(fu.operand);
|
|
const inner_ty = self.resolveOptionalInner(self.inferExprType(fu.operand));
|
|
return self.builder.optionalUnwrap(val, inner_ty);
|
|
}
|
|
|
|
fn lowerNullCoalesce(self: *Lowering, nc: *const ast.NullCoalesce) Ref {
|
|
const lhs = self.lowerExpr(nc.lhs);
|
|
const inner_ty = self.resolveOptionalInner(self.inferExprType(nc.lhs));
|
|
|
|
// Short-circuit: only evaluate RHS if LHS is null.
|
|
// IMPORTANT: optional_unwrap must be in the "has value" branch,
|
|
// not before the condBr — the interpreter errors on unwrapping null.
|
|
const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = lhs } }, .bool);
|
|
|
|
const then_bb = self.freshBlock("nc.has");
|
|
const rhs_bb = self.freshBlock("nc.rhs");
|
|
const merge_bb = self.freshBlockWithParams("nc.merge", &.{inner_ty});
|
|
|
|
// If has value, go to then_bb to unwrap; else go to rhs_bb
|
|
self.builder.condBr(has_val, then_bb, &.{}, rhs_bb, &.{});
|
|
|
|
// Then block: unwrap LHS and branch to merge
|
|
self.builder.switchToBlock(then_bb);
|
|
const unwrapped = self.builder.optionalUnwrap(lhs, inner_ty);
|
|
self.builder.br(merge_bb, &.{unwrapped});
|
|
|
|
// RHS block: evaluate fallback and branch to merge
|
|
self.builder.switchToBlock(rhs_bb);
|
|
var rhs = self.lowerExpr(nc.rhs);
|
|
const rhs_ty = self.builder.getRefType(rhs);
|
|
if (rhs_ty != inner_ty and rhs_ty != .void and inner_ty != .void) {
|
|
rhs = self.coerceToType(rhs, rhs_ty, inner_ty);
|
|
}
|
|
self.builder.br(merge_bb, &.{rhs});
|
|
|
|
// Continue at merge
|
|
self.builder.switchToBlock(merge_bb);
|
|
return self.builder.blockParam(merge_bb, 0, inner_ty);
|
|
}
|
|
|
|
fn resolveOptionalInner(self: *Lowering, ty: TypeId) TypeId {
|
|
if (!ty.isBuiltin()) {
|
|
const info = self.module.types.get(ty);
|
|
if (info == .optional) return info.optional.child;
|
|
}
|
|
return .unresolved;
|
|
}
|
|
|
|
// ── FFI intrinsics (#objc_call / #jni_call / #jni_static_call) ─
|
|
|
|
/// Intern an Obj-C selector string into a module-scoped `SEL*` slot.
|
|
/// First call creates the global; subsequent calls return the same
|
|
/// `GlobalId`. emit_llvm.zig walks `module.objc_selector_cache` and
|
|
/// synthesizes a constructor that populates each slot via
|
|
/// `sel_registerName` exactly once at module load.
|
|
///
|
|
/// Slot name matches clang's convention: `OBJC_SELECTOR_REFERENCES_<sel>`
|
|
/// with `:` replaced by `_` to keep the symbol name valid.
|
|
fn internObjcSelector(self: *Lowering, sel_str: []const u8) inst_mod.GlobalId {
|
|
if (self.module.lookupObjcSelector(sel_str)) |gid| return gid;
|
|
|
|
// Mangle selector: replace colons with underscores. Apple's
|
|
// toolchain does the same (foo:bar: → foo_bar_).
|
|
var mangled = std.ArrayList(u8).empty;
|
|
defer mangled.deinit(self.alloc);
|
|
mangled.appendSlice(self.alloc, "OBJC_SELECTOR_REFERENCES_") catch unreachable;
|
|
for (sel_str) |ch| {
|
|
mangled.append(self.alloc, if (ch == ':') '_' else ch) catch unreachable;
|
|
}
|
|
const slot_name = self.module.types.internString(mangled.items);
|
|
const vptr_ty = self.module.types.ptrTo(.void);
|
|
const gid = self.module.addGlobal(.{
|
|
.name = slot_name,
|
|
.ty = vptr_ty,
|
|
.init_val = .null_val,
|
|
.is_extern = false,
|
|
.is_const = false,
|
|
});
|
|
self.module.appendObjcSelector(sel_str, gid);
|
|
return gid;
|
|
}
|
|
|
|
/// Intern an Obj-C class name into a module-scoped `Class*` slot.
|
|
/// First call creates the global; subsequent calls return the same
|
|
/// `GlobalId`. emit_llvm.zig walks `module.objc_class_cache` and
|
|
/// synthesizes a constructor that populates each slot via
|
|
/// `objc_getClass` exactly once at module load.
|
|
///
|
|
/// Slot name matches clang's convention: `OBJC_CLASSLIST_REFERENCES_<Cls>`.
|
|
fn internObjcClassObject(self: *Lowering, class_name: []const u8) inst_mod.GlobalId {
|
|
if (self.module.lookupObjcClass(class_name)) |gid| return gid;
|
|
|
|
var mangled = std.ArrayList(u8).empty;
|
|
defer mangled.deinit(self.alloc);
|
|
mangled.appendSlice(self.alloc, "OBJC_CLASSLIST_REFERENCES_") catch unreachable;
|
|
mangled.appendSlice(self.alloc, class_name) catch unreachable;
|
|
const slot_name = self.module.types.internString(mangled.items);
|
|
const vptr_ty = self.module.types.ptrTo(.void);
|
|
const gid = self.module.addGlobal(.{
|
|
.name = slot_name,
|
|
.ty = vptr_ty,
|
|
.init_val = .null_val,
|
|
.is_extern = false,
|
|
.is_const = false,
|
|
});
|
|
self.module.appendObjcClass(class_name, gid);
|
|
return gid;
|
|
}
|
|
|
|
/// Lazily declare `sel_registerName(name: *u8) -> *void` as an extern.
|
|
/// Cached per Lowering instance so multiple `#objc_call` sites share
|
|
/// one declaration.
|
|
fn getSelRegisterNameFid(self: *Lowering) FuncId {
|
|
if (self.sel_register_name_fid) |fid| return fid;
|
|
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
|
const name_str = self.module.types.internString("name");
|
|
const ptr_ty = self.module.types.ptrTo(.u8);
|
|
params.append(self.alloc, .{ .name = name_str, .ty = ptr_ty }) catch unreachable;
|
|
const fn_name = self.module.types.internString("sel_registerName");
|
|
const ret_ty = self.module.types.ptrTo(.void);
|
|
const fid = self.builder.declareExtern(fn_name, params.toOwnedSlice(self.alloc) catch unreachable, ret_ty);
|
|
const func = self.module.getFunctionMut(fid);
|
|
func.call_conv = .c;
|
|
self.sel_register_name_fid = fid;
|
|
return fid;
|
|
}
|
|
|
|
/// Lower `#objc_call(T)(recv, "sel:", args...)` to:
|
|
/// %sel = call ptr @sel_registerName(<"sel:">)
|
|
/// %ret = call <ABI(T)> @objc_msgSend(recv, %sel, args...)
|
|
/// For Phase 1.3 only the (void return, no extra args) form is
|
|
/// fully wired. Extra arities + non-void returns will land in
|
|
/// subsequent phase-1 steps.
|
|
fn lowerFfiIntrinsicCall(self: *Lowering, fic: *const ast.FfiIntrinsicCall) Ref {
|
|
if (fic.kind == .jni_call or fic.kind == .jni_static_call) {
|
|
return self.lowerJniCall(fic);
|
|
}
|
|
|
|
if (fic.args.len < 2) {
|
|
if (self.diagnostics) |d| {
|
|
d.add(.err, "#objc_call requires at least a receiver and a selector", null);
|
|
}
|
|
return Ref.none;
|
|
}
|
|
|
|
// Resolve the return type from the syntactic slot.
|
|
const ret_ty = self.resolveType(fic.return_type);
|
|
|
|
if (fic.args.len < 2) {
|
|
if (self.diagnostics) |d| {
|
|
d.add(.err, "#objc_call requires at least a receiver and a selector", null);
|
|
}
|
|
return Ref.none;
|
|
}
|
|
|
|
// Receiver expression.
|
|
const recv = self.lowerExpr(fic.args[0]);
|
|
|
|
// Selector. Literal selectors get interned into a module-
|
|
// scoped `SEL*` slot — emit_llvm.zig tags the slot into
|
|
// `__DATA,__objc_selrefs` so dyld populates it at load time
|
|
// (matches clang's `@selector(...)` lowering exactly).
|
|
// Non-literal selectors keep the per-call `sel_registerName`
|
|
// fallback.
|
|
const sel_arg_node = fic.args[1];
|
|
const vptr_ty = self.module.types.ptrTo(.void);
|
|
const sel = blk: {
|
|
if (sel_arg_node.data == .string_literal) {
|
|
const raw = sel_arg_node.data.string_literal.raw;
|
|
const slot_gid = self.internObjcSelector(raw);
|
|
const slot_ptr = self.builder.emit(.{ .global_addr = slot_gid }, self.module.types.ptrTo(vptr_ty));
|
|
break :blk self.builder.emit(.{ .load = .{ .operand = slot_ptr } }, vptr_ty);
|
|
}
|
|
const sel_ref = self.lowerExpr(sel_arg_node);
|
|
const sel_fid = self.getSelRegisterNameFid();
|
|
var sel_args = std.ArrayList(Ref).empty;
|
|
sel_args.append(self.alloc, sel_ref) catch unreachable;
|
|
const sel_owned = sel_args.toOwnedSlice(self.alloc) catch unreachable;
|
|
break :blk self.builder.emit(.{ .call = .{ .callee = sel_fid, .args = sel_owned } }, vptr_ty);
|
|
};
|
|
|
|
// Additional args after recv + selector.
|
|
var extra = std.ArrayList(Ref).empty;
|
|
var ai: usize = 2;
|
|
while (ai < fic.args.len) : (ai += 1) {
|
|
extra.append(self.alloc, self.lowerExpr(fic.args[ai])) catch unreachable;
|
|
}
|
|
const extra_owned = extra.toOwnedSlice(self.alloc) catch unreachable;
|
|
|
|
return self.builder.emit(.{ .objc_msg_send = .{
|
|
.recv = recv,
|
|
.sel = sel,
|
|
.args = extra_owned,
|
|
} }, ret_ty);
|
|
}
|
|
|
|
fn lowerJniCall(self: *Lowering, fic: *const ast.FfiIntrinsicCall) Ref {
|
|
// env is always implicit: lexical-direct from the enclosing `#jni_env(env)`
|
|
// block (2.16b, cheap), else the thread-local slot the block populated
|
|
// at runtime (2.16c, one TL load per call). Surface form is uniform:
|
|
// #jni_call(T)(target, "name", "sig", method-args...) (≥3 args)
|
|
if (fic.args.len < 3) {
|
|
if (self.diagnostics) |d| {
|
|
d.add(.err, "#jni_call requires target, method name, and signature", null);
|
|
}
|
|
return Ref.none;
|
|
}
|
|
|
|
const ret_ty = self.resolveType(fic.return_type);
|
|
|
|
const env_ref = if (self.jni_env_stack.items.len > self.jni_env_stack_base)
|
|
self.jni_env_stack.items[self.jni_env_stack.items.len - 1]
|
|
else blk: {
|
|
const fids = self.getJniEnvTlFids();
|
|
const ptr_ty = self.module.types.ptrTo(.void);
|
|
break :blk self.builder.emit(.{ .call = .{ .callee = fids.get, .args = &.{} } }, ptr_ty);
|
|
};
|
|
|
|
const target_idx: usize = 0;
|
|
const name_idx: usize = 1;
|
|
const sig_idx: usize = 2;
|
|
const first_method_arg_idx: usize = 3;
|
|
|
|
const target_ref = self.lowerExpr(fic.args[target_idx]);
|
|
const name_node = fic.args[name_idx];
|
|
const sig_node = fic.args[sig_idx];
|
|
const name_ref = self.lowerExpr(name_node);
|
|
const sig_ref = self.lowerExpr(sig_node);
|
|
|
|
// Capture the (name, sig) literal content when both args are
|
|
// string literals — emit_llvm uses this as the intern key for
|
|
// the shared `jclass`/`jmethodID` slot pair (step 1.17).
|
|
const cache_key: ?inst_mod.CacheKey = if (name_node.data == .string_literal and sig_node.data == .string_literal)
|
|
inst_mod.CacheKey{
|
|
.name_str = name_node.data.string_literal.raw,
|
|
.sig_str = sig_node.data.string_literal.raw,
|
|
}
|
|
else
|
|
null;
|
|
|
|
var extra = std.ArrayList(Ref).empty;
|
|
var ai: usize = first_method_arg_idx;
|
|
while (ai < fic.args.len) : (ai += 1) {
|
|
extra.append(self.alloc, self.lowerExpr(fic.args[ai])) catch unreachable;
|
|
}
|
|
const extra_owned = extra.toOwnedSlice(self.alloc) catch unreachable;
|
|
|
|
return self.builder.emit(.{ .jni_msg_send = .{
|
|
.env = env_ref,
|
|
.target = target_ref,
|
|
.name = name_ref,
|
|
.sig = sig_ref,
|
|
.args = extra_owned,
|
|
.is_static = fic.kind == .jni_static_call,
|
|
.cache_key = cache_key,
|
|
} }, ret_ty);
|
|
}
|
|
|
|
/// Lower an `inst.method(args)` call where `inst`'s type is a foreign-class
|
|
/// alias declared by `#jni_class("...") { ... }` (or its parallel forms).
|
|
/// JNI runtimes lower directly to `jni_msg_send` with a descriptor derived
|
|
/// from the method's sx signature; Obj-C / Swift runtimes are deferred to
|
|
/// Phase 3/4 and currently surface a clear diagnostic.
|
|
fn lowerForeignMethodCall(
|
|
self: *Lowering,
|
|
fcd: *const ast.ForeignClassDecl,
|
|
method_name: []const u8,
|
|
target: Ref,
|
|
method_args: []const Ref,
|
|
span: ast.Span,
|
|
) Ref {
|
|
// M2.3 — walk the `#extends` chain when the method isn't
|
|
// declared directly on this fcd. The dispatch target stays
|
|
// the original receiver — objc_msgSend's runtime walks the
|
|
// class hierarchy by isa, so we just need to find ANY
|
|
// ancestor that declared the method (for the selector
|
|
// mangling + signature info). The receiver-class fcd is
|
|
// still used for `*Self` substitution at the dispatch site
|
|
// — the inherited method's *Self should resolve to the
|
|
// child receiver, not the parent.
|
|
const found = self.findForeignMethodInChain(fcd, method_name) orelse {
|
|
if (self.diagnostics) |d| {
|
|
d.addFmt(.err, span, "no method '{s}' on foreign class '{s}' (or any `#extends` ancestor)", .{ method_name, fcd.name });
|
|
}
|
|
return Ref.none;
|
|
};
|
|
const method = found.method;
|
|
|
|
// Obj-C instance dispatch (Phase 3 step 3.0 + M1.2 A.7).
|
|
// `inst.method(args)` on an `#objc_class` / `#objc_protocol`
|
|
// receiver derives a selector from the sx method name (default
|
|
// mangling: split on `_`, each piece becomes a keyword with a
|
|
// trailing `:`; niladic stays verbatim) and lowers to
|
|
// `objc_msg_send`. Both foreign and sx-defined classes flow
|
|
// through the same path — sx-defined classes have their IMPs
|
|
// registered at module-init (M1.2 A.4b.iii) so `objc_msgSend`
|
|
// finds them. The Swift runtimes still bail — Phase 4.
|
|
if (fcd.runtime == .objc_class or fcd.runtime == .objc_protocol) {
|
|
return self.lowerObjcMethodCall(fcd, method, target, method_args, span);
|
|
}
|
|
if (!fcd.is_foreign) {
|
|
if (self.diagnostics) |d| {
|
|
d.addFmt(.err, span, "sx-defined classes on non-Obj-C runtimes can't yet be dispatched into (class '{s}', runtime '{s}')", .{ fcd.name, @tagName(fcd.runtime) });
|
|
}
|
|
return Ref.none;
|
|
}
|
|
if (fcd.runtime != .jni_class and fcd.runtime != .jni_interface) {
|
|
if (self.diagnostics) |d| {
|
|
d.addFmt(.err, span, "method calls on '{s}' runtime not yet supported (Phase 3/4)", .{@tagName(fcd.runtime)});
|
|
}
|
|
return Ref.none;
|
|
}
|
|
|
|
if (self.jni_env_stack.items.len == 0) {
|
|
if (self.diagnostics) |d| {
|
|
d.addFmt(.err, span, "method call on '{s}' requires an enclosing '#jni_env' scope", .{fcd.name});
|
|
}
|
|
return Ref.none;
|
|
}
|
|
const env_ref = self.jni_env_stack.items[self.jni_env_stack.items.len - 1];
|
|
|
|
// Build a ClassRegistry snapshot so descriptor derivation can
|
|
// resolve `*Foo` cross-class refs to their foreign paths.
|
|
var registry = jni_descriptor.ClassRegistry.init(self.alloc);
|
|
defer registry.deinit();
|
|
var it = self.program_index.foreign_class_map.iterator();
|
|
while (it.next()) |entry| {
|
|
registry.put(entry.key_ptr.*, entry.value_ptr.*.foreign_path) catch {};
|
|
}
|
|
|
|
const desc_str = jni_descriptor.deriveMethod(self.alloc, .{
|
|
.enclosing_path = fcd.foreign_path,
|
|
.classes = ®istry,
|
|
}, method) catch |err| {
|
|
if (self.diagnostics) |d| {
|
|
d.addFmt(.err, span, "JNI descriptor derivation failed for '{s}.{s}': {s}", .{ fcd.name, method.name, @errorName(err) });
|
|
}
|
|
return Ref.none;
|
|
};
|
|
|
|
const name_sid = self.module.types.internString(method_name);
|
|
const name_ref = self.builder.constString(name_sid);
|
|
const sig_sid = self.module.types.internString(desc_str);
|
|
const sig_ref = self.builder.constString(sig_sid);
|
|
|
|
const ret_ty = if (method.return_type) |rt| self.resolveType(rt) else .void;
|
|
|
|
// Reject return types the JNI emit path can't dispatch — emit_llvm's
|
|
// Call<T>Method switch only covers void / bool / s32 / s64 / f32 / f64
|
|
// / pointer-returning. Anything else (s8 / s16 / u8 / u16 / aggregates)
|
|
// would silently lower to LLVMGetUndef and produce wrong arguments at
|
|
// the call site (chess Android touch shipped broken because s32→s32+
|
|
// f32 returns hit the undef path before .f32 was wired up).
|
|
if (!jni_descriptor.isJniReturnTypeSupported(&self.module.types, ret_ty)) {
|
|
if (self.diagnostics) |d| {
|
|
d.addFmt(.err, span, "JNI method '{s}.{s}' returns '{s}', which isn't supported by the JNI call-method lowering yet — only void/bool/s32/s64/f32/f64 and pointers are wired up", .{ fcd.name, method.name, self.module.types.typeName(ret_ty) });
|
|
}
|
|
return Ref.none;
|
|
}
|
|
|
|
const cache_key: inst_mod.CacheKey = .{
|
|
.name_str = method_name,
|
|
.sig_str = desc_str,
|
|
};
|
|
|
|
const args_owned = self.alloc.dupe(Ref, method_args) catch unreachable;
|
|
return self.builder.emit(.{ .jni_msg_send = .{
|
|
.env = env_ref,
|
|
.target = target,
|
|
.name = name_ref,
|
|
.sig = sig_ref,
|
|
.args = args_owned,
|
|
.is_static = method.is_static,
|
|
.cache_key = cache_key,
|
|
} }, ret_ty);
|
|
}
|
|
|
|
// Pure Obj-C decision helpers (selector derivation, type-encoding, ARC
|
|
// property-kind, class-pointer recognition, state-struct planning) live in
|
|
// `ffi_objc.zig` (`ObjcLowering`, a `*Lowering` facade). Reached via
|
|
// `self.objc()`. Emission-heavy IMP builders + `lowerObjc*Call` stay here.
|
|
|
|
/// Resolve a foreign-class member type, substituting `Self` (and `*Self`)
|
|
/// with the foreign class's own struct type. Without this substitution
|
|
/// chained calls like `Cls.alloc().init()` see the inner result as a
|
|
/// fictitious `Self` struct and the next dispatch lookup fails.
|
|
fn resolveForeignClassMemberType(
|
|
self: *Lowering,
|
|
fcd: *const ast.ForeignClassDecl,
|
|
type_node: *const ast.Node,
|
|
) TypeId {
|
|
if (type_node.data == .type_expr and std.mem.eql(u8, type_node.data.type_expr.name, "Self")) {
|
|
return self.foreignClassStructType(fcd);
|
|
}
|
|
if (type_node.data == .pointer_type_expr) {
|
|
const pt = type_node.data.pointer_type_expr;
|
|
if (pt.pointee_type.data == .type_expr and std.mem.eql(u8, pt.pointee_type.data.type_expr.name, "Self")) {
|
|
return self.module.types.ptrTo(self.foreignClassStructType(fcd));
|
|
}
|
|
}
|
|
return self.resolveType(type_node);
|
|
}
|
|
|
|
pub fn resolveForeignMethodReturnType(
|
|
self: *Lowering,
|
|
fcd: *const ast.ForeignClassDecl,
|
|
method: ast.ForeignMethodDecl,
|
|
) TypeId {
|
|
const rt = method.return_type orelse return .void;
|
|
return self.resolveForeignClassMemberType(fcd, rt);
|
|
}
|
|
|
|
fn foreignClassStructType(self: *Lowering, fcd: *const ast.ForeignClassDecl) TypeId {
|
|
const name_id = self.module.types.internString(fcd.name);
|
|
if (self.module.types.findByName(name_id)) |existing| return existing;
|
|
return self.module.types.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } });
|
|
}
|
|
|
|
/// Lower `inst.method(args)` on an `#objc_class` / `#objc_protocol`
|
|
/// receiver. The selector is derived by `deriveObjcSelector`; arity
|
|
/// is validated against the keyword count produced by the mangling
|
|
/// (excluding self). Dispatch then runs through `objc_msg_send`,
|
|
/// sharing the cached-SEL slot path with explicit `#objc_call`.
|
|
fn lowerObjcMethodCall(
|
|
self: *Lowering,
|
|
fcd: *const ast.ForeignClassDecl,
|
|
method: ast.ForeignMethodDecl,
|
|
target: Ref,
|
|
method_args: []const Ref,
|
|
span: ast.Span,
|
|
) Ref {
|
|
const arity = method_args.len;
|
|
const derived = self.objc().deriveObjcSelector(method, arity);
|
|
|
|
// Arity validation: the keyword count (number of `:` in the
|
|
// selector) must equal the number of args passed at the call
|
|
// site. For methods using the default mangling rule, a mismatch
|
|
// is an error because the user can fix the sx-side name. For
|
|
// `#selector("...")` overrides, the user has deliberately
|
|
// chosen the selector — downgrade to a warning so the build
|
|
// proceeds, but still surface the typo case (Obj-C's runtime
|
|
// doesn't validate colon-vs-arg, so this is the last defense).
|
|
if (arity > 0 and derived.keyword_count != arity) {
|
|
if (self.diagnostics) |d| {
|
|
if (derived.is_override) {
|
|
d.addFmt(
|
|
.warn,
|
|
span,
|
|
"Obj-C selector \"{s}\" (override for '{s}.{s}') has {} keyword(s) but the call passes {} argument(s); the runtime will dispatch but the colon count is inconsistent with the arity — double-check the selector string",
|
|
.{ derived.sel, fcd.name, method.name, derived.keyword_count, arity },
|
|
);
|
|
} else {
|
|
d.addFmt(
|
|
.err,
|
|
span,
|
|
"Obj-C selector for '{s}.{s}' has {} keyword(s) but the call passes {} argument(s); split the sx method name on '_' so it produces exactly {} keyword(s), or override with `#selector(\"...\")`",
|
|
.{ fcd.name, method.name, derived.keyword_count, arity, arity },
|
|
);
|
|
return Ref.none;
|
|
}
|
|
}
|
|
}
|
|
|
|
const ret_ty = self.resolveForeignMethodReturnType(fcd, method);
|
|
|
|
// Cache the SEL slot per (selector-string, module) like
|
|
// `#objc_call` does. The mangling produces the literal selector
|
|
// string; we don't need a runtime sel_registerName call at the
|
|
// dispatch site because the global initializer already does it.
|
|
const vptr_ty = self.module.types.ptrTo(.void);
|
|
const slot_gid = self.internObjcSelector(derived.sel);
|
|
const slot_ptr = self.builder.emit(.{ .global_addr = slot_gid }, self.module.types.ptrTo(vptr_ty));
|
|
const sel = self.builder.emit(.{ .load = .{ .operand = slot_ptr } }, vptr_ty);
|
|
|
|
const args_owned = self.alloc.dupe(Ref, method_args) catch unreachable;
|
|
return self.builder.emit(.{ .objc_msg_send = .{
|
|
.recv = target,
|
|
.sel = sel,
|
|
.args = args_owned,
|
|
} }, ret_ty);
|
|
}
|
|
|
|
/// Lower `Cls.static_method(args)` on an `#objc_class` /
|
|
/// `#objc_protocol` alias. Loads the class object through the
|
|
/// module-scoped cached slot (populated by `objc_getClass` at
|
|
/// module-init) and dispatches `objc_msg_send` with the same
|
|
/// selector mangling as instance methods (Phase 3.0).
|
|
fn lowerObjcStaticCall(
|
|
self: *Lowering,
|
|
fcd: *const ast.ForeignClassDecl,
|
|
method: ast.ForeignMethodDecl,
|
|
method_args: []const Ref,
|
|
span: ast.Span,
|
|
) Ref {
|
|
const arity = method_args.len;
|
|
const derived = self.objc().deriveObjcSelector(method, arity);
|
|
|
|
if (arity > 0 and derived.keyword_count != arity) {
|
|
if (self.diagnostics) |d| {
|
|
if (derived.is_override) {
|
|
d.addFmt(
|
|
.warn,
|
|
span,
|
|
"Obj-C selector \"{s}\" (override for static call '{s}.{s}') has {} keyword(s) but the call passes {} argument(s); the runtime will dispatch but the colon count is inconsistent with the arity — double-check the selector string",
|
|
.{ derived.sel, fcd.name, method.name, derived.keyword_count, arity },
|
|
);
|
|
} else {
|
|
d.addFmt(
|
|
.err,
|
|
span,
|
|
"Obj-C selector for static call '{s}.{s}' has {} keyword(s) but the call passes {} argument(s); split the sx method name on '_' so it produces exactly {} keyword(s), or override with `#selector(\"...\")`",
|
|
.{ fcd.name, method.name, derived.keyword_count, arity, arity },
|
|
);
|
|
return Ref.none;
|
|
}
|
|
}
|
|
}
|
|
|
|
const ret_ty = self.resolveForeignMethodReturnType(fcd, method);
|
|
|
|
const vptr_ty = self.module.types.ptrTo(.void);
|
|
|
|
// Load the class object from its module-scoped cached slot.
|
|
// `objc_getClass(<name>)` runs once at module-init via the
|
|
// constructor emit_llvm synthesizes (see `emitObjcClassInit`).
|
|
const class_slot_gid = self.internObjcClassObject(fcd.foreign_path);
|
|
const class_slot_ptr = self.builder.emit(.{ .global_addr = class_slot_gid }, self.module.types.ptrTo(vptr_ty));
|
|
const class_obj = self.builder.emit(.{ .load = .{ .operand = class_slot_ptr } }, vptr_ty);
|
|
|
|
// M4.0b: intercept `Cls.alloc()` for sx-defined classes — emit the
|
|
// inline alloc-and-init sequence using the caller's `context.allocator`
|
|
// instead of going through `objc_msgSend` (which would land in the
|
|
// +alloc IMP and use `__sx_default_context.allocator`). This honors
|
|
// a surrounding `push Context.{ allocator = ... }`.
|
|
if (!fcd.is_foreign and
|
|
fcd.runtime == .objc_class and
|
|
method_args.len == 0 and
|
|
std.mem.eql(u8, method.name, "alloc"))
|
|
{
|
|
const ctx_addr = if (self.current_ctx_ref != Ref.none)
|
|
self.current_ctx_ref
|
|
else blk: {
|
|
// Fallback: no current ctx (e.g. compiler-internal callers).
|
|
// Use the default context — same as the IMP would.
|
|
const default_ctx_gi = self.program_index.global_names.get("__sx_default_context") orelse {
|
|
if (self.diagnostics) |d| {
|
|
d.addFmt(.err, span, "Cls.alloc() on sx-defined class '{s}': no current context and __sx_default_context missing", .{fcd.name});
|
|
}
|
|
return Ref.none;
|
|
};
|
|
break :blk self.builder.emit(.{ .global_addr = default_ctx_gi.id }, vptr_ty);
|
|
};
|
|
const instance = self.emitObjcDefinedAllocAndInit(fcd, class_obj, ctx_addr) orelse return Ref.none;
|
|
// class_createInstance returns *void; bitcast to the method's
|
|
// declared return type (typically `*<Cls>` or `?*<Cls>`) so
|
|
// downstream `let f := Cls.alloc();` binds f at the right type
|
|
// (lowerVarDecl reads the Ref's IR type when no annotation is
|
|
// present). coerceToType is a no-op for ptr→ptr; we need an
|
|
// explicit bitcast IR op to retype the Ref.
|
|
if (ret_ty == vptr_ty) return instance;
|
|
// Optional-wrapped returns (e.g. `-> ?*Cls`): emit optional_wrap.
|
|
if (!ret_ty.isBuiltin()) {
|
|
const ret_info = self.module.types.get(ret_ty);
|
|
if (ret_info == .optional) {
|
|
const inner = ret_info.optional.child;
|
|
const cast = if (inner == vptr_ty)
|
|
instance
|
|
else
|
|
self.builder.emit(.{ .bitcast = .{ .operand = instance, .from = vptr_ty, .to = inner } }, inner);
|
|
return self.builder.optionalWrap(cast, ret_ty);
|
|
}
|
|
}
|
|
return self.builder.emit(.{ .bitcast = .{ .operand = instance, .from = vptr_ty, .to = ret_ty } }, ret_ty);
|
|
}
|
|
|
|
// Load the SEL from its slot.
|
|
const sel_slot_gid = self.internObjcSelector(derived.sel);
|
|
const sel_slot_ptr = self.builder.emit(.{ .global_addr = sel_slot_gid }, self.module.types.ptrTo(vptr_ty));
|
|
const sel = self.builder.emit(.{ .load = .{ .operand = sel_slot_ptr } }, vptr_ty);
|
|
|
|
const args_owned = self.alloc.dupe(Ref, method_args) catch unreachable;
|
|
return self.builder.emit(.{ .objc_msg_send = .{
|
|
.recv = class_obj,
|
|
.sel = sel,
|
|
.args = args_owned,
|
|
} }, ret_ty);
|
|
}
|
|
|
|
/// Lower `Alias.new(args)` where `Alias` is a foreign-class identifier
|
|
/// with `static new :: (...) -> *Self;` — JNI constructor dispatch:
|
|
/// `FindClass + GetMethodID("<init>", "(args)V") + NewObject(env,
|
|
/// clazz, mid, args...)`. Returns the new jobject.
|
|
///
|
|
/// Non-`new` static methods aren't supported via this path yet — the
|
|
/// user can use `#jni_static_call(T)(class, "name", sig, args...)`
|
|
/// for those. Constructor is the common case for #jni_main bodies
|
|
/// that need to instantiate Android classes (SurfaceView, etc.).
|
|
fn lowerForeignStaticCall(
|
|
self: *Lowering,
|
|
fcd: *const ast.ForeignClassDecl,
|
|
method: ast.ForeignMethodDecl,
|
|
method_args: []const Ref,
|
|
span: ast.Span,
|
|
) Ref {
|
|
// Obj-C static dispatch (Phase 3 step 3.1). `Cls.static_method(args)`
|
|
// on an `#objc_class` alias loads the class object through a
|
|
// module-scoped cached slot (populated once per module via
|
|
// `objc_getClass`) and dispatches with the derived selector.
|
|
if (fcd.runtime == .objc_class or fcd.runtime == .objc_protocol) {
|
|
return self.lowerObjcStaticCall(fcd, method, method_args, span);
|
|
}
|
|
if (fcd.runtime != .jni_class and fcd.runtime != .jni_interface) {
|
|
if (self.diagnostics) |d| d.addFmt(.err, span, "static calls on '{s}' runtime not yet supported (Phase 3/4)", .{@tagName(fcd.runtime)});
|
|
return Ref.none;
|
|
}
|
|
if (!std.mem.eql(u8, method.name, "new")) {
|
|
if (self.diagnostics) |d| d.addFmt(.err, span, "static foreign-class call '{s}.{s}' not yet supported via `Alias.method()` syntax \u{2014} only `new` is wired today; use `#jni_static_call` directly for other static methods", .{ fcd.name, method.name });
|
|
return Ref.none;
|
|
}
|
|
|
|
if (self.jni_env_stack.items.len <= self.jni_env_stack_base) {
|
|
if (self.diagnostics) |d| d.addFmt(.err, span, "constructor `{s}.new(...)` requires an enclosing `#jni_env` scope (or `#jni_main` body)", .{fcd.name});
|
|
return Ref.none;
|
|
}
|
|
const env_ref = self.jni_env_stack.items[self.jni_env_stack.items.len - 1];
|
|
|
|
// Build class registry snapshot for `*Foo` cross-class refs.
|
|
var registry = jni_descriptor.ClassRegistry.init(self.alloc);
|
|
defer registry.deinit();
|
|
var it = self.program_index.foreign_class_map.iterator();
|
|
while (it.next()) |entry| {
|
|
registry.put(entry.key_ptr.*, entry.value_ptr.*.foreign_path) catch {};
|
|
}
|
|
|
|
// For `new`, the JNI descriptor's return position is `V` (the
|
|
// constructor returns void; the new jobject comes back from
|
|
// `NewObject` itself). Patch the AST by overriding return_type
|
|
// to null during derivation.
|
|
const m_for_desc: ast.ForeignMethodDecl = .{
|
|
.name = method.name,
|
|
.params = method.params,
|
|
.param_names = method.param_names,
|
|
.return_type = null,
|
|
.is_static = method.is_static,
|
|
.jni_descriptor_override = method.jni_descriptor_override,
|
|
.body = method.body,
|
|
};
|
|
|
|
const descriptor = jni_descriptor.deriveMethod(self.alloc, .{
|
|
.enclosing_path = fcd.foreign_path,
|
|
.classes = ®istry,
|
|
}, m_for_desc) catch |err| {
|
|
if (self.diagnostics) |d| d.addFmt(.err, span, "JNI descriptor derivation failed for '{s}.new': {s}", .{ fcd.name, @errorName(err) });
|
|
return Ref.none;
|
|
};
|
|
|
|
// sx-side return type is `*Self` — resolve to a pointer to the
|
|
// foreign-class struct type so method dispatch on the new
|
|
// jobject works (`view := SurfaceView.new(ctx); view.getHolder()`).
|
|
// At LLVM level still ptr; the sx type table is what method
|
|
// resolution consults.
|
|
const self_struct_name = self.module.types.internString(fcd.name);
|
|
const self_struct_id = if (self.module.types.findByName(self_struct_name)) |existing|
|
|
existing
|
|
else blk: {
|
|
const info: types.TypeInfo = .{ .@"struct" = .{ .name = self_struct_name, .fields = &.{} } };
|
|
break :blk self.module.types.intern(info);
|
|
};
|
|
const ret_ty = self.module.types.ptrTo(self_struct_id);
|
|
|
|
const name_sid = self.module.types.internString("<init>");
|
|
const name_ref = self.builder.constString(name_sid);
|
|
const sig_sid = self.module.types.internString(descriptor);
|
|
const sig_ref = self.builder.constString(sig_sid);
|
|
|
|
const args_owned = self.alloc.dupe(Ref, method_args) catch unreachable;
|
|
return self.builder.emit(.{ .jni_msg_send = .{
|
|
.env = env_ref,
|
|
.target = Ref.none, // unused for ctor — class is resolved via parent_class_path
|
|
.name = name_ref,
|
|
.sig = sig_ref,
|
|
.args = args_owned,
|
|
.is_static = false,
|
|
.is_constructor = true,
|
|
.parent_class_path = self.alloc.dupe(u8, fcd.foreign_path) catch fcd.foreign_path,
|
|
.cache_key = null,
|
|
} }, ret_ty);
|
|
}
|
|
|
|
/// Lower `super.method(args)` inside a `#jni_main` / sx-defined
|
|
/// `#jni_class` bodied method. Resolves the parent class from the
|
|
/// enclosing fcd's `#extends` clause (default `android.app.Activity`)
|
|
/// and emits a `JniMsgSend` with `is_nonvirtual=true`, which
|
|
/// emit_llvm expands into a `FindClass(parent) + GetMethodID +
|
|
/// CallNonvirtual<T>Method` chain.
|
|
///
|
|
/// Signature derivation: when `method_name` matches the enclosing
|
|
/// method's name (the common case — `super.onCreate(b)` from inside
|
|
/// `onCreate :: (self, b)` override), the enclosing method's
|
|
/// signature is reused. Other method names require the parent class
|
|
/// to be declared via `#foreign #jni_class` so the signature can be
|
|
/// looked up.
|
|
fn lowerSuperCall(
|
|
self: *Lowering,
|
|
method_name: []const u8,
|
|
method_args: []const Ref,
|
|
span: ast.Span,
|
|
) Ref {
|
|
const fcd = self.current_foreign_class orelse {
|
|
if (self.diagnostics) |d| d.addFmt(.err, span, "'super' is only valid inside a `#jni_class` method body", .{});
|
|
return Ref.none;
|
|
};
|
|
|
|
// Resolve parent foreign_path from the fcd's `#extends`. Default to
|
|
// android.app.Activity to match the jni_java_emit default.
|
|
var parent_path: []const u8 = "android/app/Activity";
|
|
for (fcd.members) |m| switch (m) {
|
|
.extends => |alias| {
|
|
if (self.program_index.foreign_class_map.get(alias)) |parent_fcd| {
|
|
parent_path = parent_fcd.foreign_path;
|
|
} else {
|
|
parent_path = alias;
|
|
}
|
|
break;
|
|
},
|
|
else => {},
|
|
};
|
|
|
|
// Resolve method signature. Same-name fast path reuses the
|
|
// enclosing method's descriptor; cross-method super calls require
|
|
// the parent class to be declared via `#foreign #jni_class`.
|
|
var descriptor: []const u8 = "";
|
|
var resolved_method: ?ast.ForeignMethodDecl = null;
|
|
if (self.current_foreign_method) |em| {
|
|
if (std.mem.eql(u8, em.name, method_name)) {
|
|
resolved_method = em;
|
|
}
|
|
}
|
|
if (resolved_method == null) {
|
|
const parent_fcd = blk: for (fcd.members) |m| switch (m) {
|
|
.extends => |alias| if (self.program_index.foreign_class_map.get(alias)) |pf| break :blk pf else continue,
|
|
else => {},
|
|
} else null;
|
|
if (parent_fcd) |pf| {
|
|
for (pf.members) |pm| switch (pm) {
|
|
.method => |pmd| if (std.mem.eql(u8, pmd.name, method_name)) {
|
|
resolved_method = pmd;
|
|
break;
|
|
},
|
|
else => {},
|
|
};
|
|
}
|
|
}
|
|
const method = resolved_method orelse {
|
|
if (self.diagnostics) |d| d.addFmt(.err, span, "no method '{s}' found for `super.{s}(...)` — declare the parent class via `#foreign #jni_class` to make cross-method super calls available", .{ method_name, method_name });
|
|
return Ref.none;
|
|
};
|
|
|
|
// Derive descriptor against the parent path (used as enclosing_path
|
|
// for `*Self` resolution).
|
|
var registry = jni_descriptor.ClassRegistry.init(self.alloc);
|
|
defer registry.deinit();
|
|
var it = self.program_index.foreign_class_map.iterator();
|
|
while (it.next()) |entry| {
|
|
registry.put(entry.key_ptr.*, entry.value_ptr.*.foreign_path) catch {};
|
|
}
|
|
descriptor = jni_descriptor.deriveMethod(self.alloc, .{
|
|
.enclosing_path = parent_path,
|
|
.classes = ®istry,
|
|
}, method) catch |err| {
|
|
if (self.diagnostics) |d| d.addFmt(.err, span, "super-call descriptor derivation failed for '{s}.{s}': {s}", .{ parent_path, method_name, @errorName(err) });
|
|
return Ref.none;
|
|
};
|
|
|
|
// env from the lexical stack (pushed by synthesizeJniMainStub).
|
|
if (self.jni_env_stack.items.len <= self.jni_env_stack_base) {
|
|
if (self.diagnostics) |d| d.addFmt(.err, span, "`super.{s}(...)` requires an enclosing `#jni_main` method scope (env is unavailable)", .{method_name});
|
|
return Ref.none;
|
|
}
|
|
const env_ref = self.jni_env_stack.items[self.jni_env_stack.items.len - 1];
|
|
|
|
// `self` is the first param of the synthesized `Java_*` fn. Bound
|
|
// in scope as `self` by synthesizeJniMainStub.
|
|
const self_binding = if (self.scope) |s| s.lookup("self") else null;
|
|
const self_ref = if (self_binding) |b| (if (b.is_alloca) self.builder.load(b.ref, b.ty) else b.ref) else Ref.none;
|
|
|
|
const name_sid = self.module.types.internString(method_name);
|
|
const name_ref = self.builder.constString(name_sid);
|
|
const sig_sid = self.module.types.internString(descriptor);
|
|
const sig_ref = self.builder.constString(sig_sid);
|
|
|
|
const ret_ty = if (method.return_type) |rt| self.resolveType(rt) else .void;
|
|
|
|
const args_owned = self.alloc.dupe(Ref, method_args) catch unreachable;
|
|
return self.builder.emit(.{ .jni_msg_send = .{
|
|
.env = env_ref,
|
|
.target = self_ref,
|
|
.name = name_ref,
|
|
.sig = sig_ref,
|
|
.args = args_owned,
|
|
.is_static = false,
|
|
.is_nonvirtual = true,
|
|
.parent_class_path = self.alloc.dupe(u8, parent_path) catch parent_path,
|
|
.cache_key = null, // per-call FindClass + GetMethodID; caching is a follow-up
|
|
} }, ret_ty);
|
|
}
|
|
|
|
// ── Calls ───────────────────────────────────────────────────────
|
|
|
|
fn lowerCall(self: *Lowering, c_in: *const ast.Call) Ref {
|
|
var c = c_in;
|
|
// A bare reserved-type-name spelling in call position parses as a
|
|
// `.type_expr` (e.g. `s2(4)`), but if a function of that name is in
|
|
// scope — a backtick-declared sx fn or a `#import c` foreign fn whose C
|
|
// name collides with a reserved type spelling — it is a CALL to that
|
|
// function. `TypeName(val)` is not a cast (casts are `cast(T, val)`), so
|
|
// there is no ambiguity. Rewrite the callee to an identifier so the
|
|
// normal call machinery resolves it, symmetric to the bare-value
|
|
// reference that already resolves via scope/globals (issue 0089).
|
|
//
|
|
// Scoped to RAW provenance: only a backtick (`is_raw`) or `#import c`
|
|
// foreign fn declaration may legally carry a reserved-name spelling
|
|
// (the decl check rejects every bare reserved-name sx fn). Refusing the
|
|
// rewrite for a non-raw match keeps a genuine reserved type spelling a
|
|
// type — belt-and-suspenders should any future path ever reintroduce a
|
|
// non-raw reserved-name callee.
|
|
if (c.callee.data == .type_expr) {
|
|
const tname = c.callee.data.type_expr.name;
|
|
const eff = if (self.scope) |scope| scope.lookupFn(tname) orelse tname else tname;
|
|
const fd: ?*const ast.FnDecl = self.program_index.fn_ast_map.get(eff) orelse
|
|
self.program_index.fn_ast_map.get(tname);
|
|
if (fd) |decl| if (decl.is_raw) {
|
|
const id_node = self.alloc.create(Node) catch unreachable;
|
|
id_node.* = .{ .span = c.callee.span, .data = .{ .identifier = .{ .name = tname, .is_raw = true } } };
|
|
const rewritten = self.alloc.create(ast.Call) catch unreachable;
|
|
rewritten.* = .{ .callee = id_node, .args = c.args };
|
|
c = rewritten;
|
|
};
|
|
}
|
|
// fix-0102 F2 / R5 §C: select the bare / value-UFCS same-name call author
|
|
// ONCE, via `CallResolver.selectedFreeAuthor` — the SINGLE producer of
|
|
// this verdict, the exact same one `CallResolver.plan` consumes for typing.
|
|
// The call-path consumers (default expansion, param typing, dispatch) all
|
|
// read THIS one author object, so plan-typing and lowering-dispatch can no
|
|
// longer disagree about which same-name function the call names, and the
|
|
// shadow's FuncId is materialized at most once (into `author_verdict`).
|
|
// `selectedFreeAuthor` is side-effect-free (it only runs the author
|
|
// selector — no return-type inference / type-arg resolution), so computing
|
|
// it eagerly here can't emit a premature diagnostic the way the full plan
|
|
// would.
|
|
var author_verdict = self.callResolver().selectedFreeAuthor(c);
|
|
const sel_author: ?*SelectedFunc = switch (author_verdict) {
|
|
.func => |*sf| sf,
|
|
else => null,
|
|
};
|
|
const author_ambiguous = author_verdict == .ambiguous;
|
|
// Expand default parameter values for bare identifier callees:
|
|
// when the caller omits trailing positional args, fill them in
|
|
// from the callee's `param: T = expr` declarations.
|
|
if (self.expandCallDefaults(c, sel_author, author_ambiguous)) |expanded| c = expanded;
|
|
// Check reflection builtins first (before lowering args — some args are type names, not values)
|
|
if (c.callee.data == .identifier) {
|
|
if (self.tryLowerReflectionCall(c.callee.data.identifier.name, c)) |ref| return ref;
|
|
}
|
|
|
|
// Check for runtime dispatch pattern BEFORE lowering args.
|
|
// lowerRuntimeDispatchCall handles its own arg lowering, and pre-lowering
|
|
// cast(type) val would produce a dead `call_builtin cast : void`.
|
|
if (c.callee.data == .identifier) {
|
|
const id_name = c.callee.data.identifier.name;
|
|
const eff_name = blk: {
|
|
const scoped = if (self.scope) |scope| scope.lookupFn(id_name) orelse id_name else id_name;
|
|
if (self.program_index.ufcs_alias_map.get(id_name)) |target| {
|
|
break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target;
|
|
}
|
|
break :blk scoped;
|
|
};
|
|
// C-import visibility: deny calls to C fn_decls not in the caller's module scope
|
|
if (!self.isCImportVisible(eff_name)) {
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, c.callee.span, "C function '{s}' not visible; add #import for the module that declares it", .{eff_name});
|
|
return Ref.none;
|
|
}
|
|
// Non-transitive `#import` visibility check. Apply only when the
|
|
// user-typed name resolved as-is to a top-level fn — local-scope
|
|
// mangling (eff_name != id_name) and UFCS alias rewriting are
|
|
// compiler indirections and stay exempt.
|
|
if (std.mem.eql(u8, eff_name, id_name) and
|
|
self.program_index.ufcs_alias_map.get(id_name) == null and
|
|
self.program_index.fn_ast_map.contains(eff_name) and
|
|
!self.isNameVisible(eff_name))
|
|
{
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, c.callee.span, "'{s}' is not visible; #import the module that declares it", .{eff_name});
|
|
return Ref.none;
|
|
}
|
|
if (self.program_index.fn_ast_map.get(eff_name)) |fd| {
|
|
if (self.current_match_tags) |tags| {
|
|
if (tags.len > 0 and self.hasCastWithRuntimeType(c)) {
|
|
return self.lowerRuntimeDispatchCall(fd, eff_name, c, tags);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle closure(fn_or_lambda) — wrap bare functions into closures
|
|
if (c.callee.data == .identifier and std.mem.eql(u8, c.callee.data.identifier.name, "closure")) {
|
|
if (c.args.len >= 1) {
|
|
const arg = c.args[0];
|
|
// If argument is a bare function name, create a proper closure from it
|
|
if (arg.data == .identifier) {
|
|
const fn_name = arg.data.identifier.name;
|
|
// fix-0102d site 2: `closure(fn)` over a genuine flat same-name
|
|
// collision must capture the RESOLVED author's FuncId, not the
|
|
// first-wins winner's. Plain bare name only; `.ambiguous`
|
|
// → loud diagnostic; `.none` → existing first-wins path.
|
|
const closure_fid: ?FuncId = blk_cl: {
|
|
if (self.program_index.ufcs_alias_map.get(fn_name) == null and
|
|
(if (self.scope) |scope| scope.lookup(fn_name) == null else true))
|
|
{
|
|
if (self.current_source_file) |caller_file| {
|
|
switch (self.selectPlainCallableAuthor(fn_name, caller_file)) {
|
|
.func => |sf| {
|
|
var selected = sf;
|
|
break :blk_cl self.selectedFuncId(&selected, fn_name);
|
|
},
|
|
.ambiguous => {
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, arg.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{fn_name});
|
|
return Ref.none;
|
|
},
|
|
.none => {},
|
|
}
|
|
}
|
|
}
|
|
if (!self.lowered_functions.contains(fn_name)) {
|
|
self.lazyLowerFunction(fn_name);
|
|
}
|
|
break :blk_cl self.resolveFuncByName(fn_name);
|
|
};
|
|
if (closure_fid) |fid| {
|
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
|
// Build closure type from user-visible params only —
|
|
// skip the implicit __sx_ctx param.
|
|
var param_types_list = std.ArrayList(TypeId).empty;
|
|
defer param_types_list.deinit(self.alloc);
|
|
const skip: usize = if (func.has_implicit_ctx) 1 else 0;
|
|
for (func.params[skip..]) |p| {
|
|
param_types_list.append(self.alloc, p.ty) catch unreachable;
|
|
}
|
|
const closure_ty = self.module.types.closureType(param_types_list.items, func.ret);
|
|
const closure_info = self.module.types.get(closure_ty).closure;
|
|
const tramp_id = self.createBareFnTrampoline(fid, closure_info);
|
|
return self.builder.closureCreate(tramp_id, Ref.none, closure_ty);
|
|
}
|
|
}
|
|
// Lambda or other expression — already produces closure_create
|
|
return self.lowerExpr(arg);
|
|
}
|
|
}
|
|
|
|
// Early detection of comptime-expanded calls (e.g. print) — skip arg evaluation
|
|
// since lowerComptimeCall re-evaluates args from AST (avoiding double evaluation)
|
|
if (c.callee.data == .identifier) {
|
|
const early_name = blk: {
|
|
const id_name = c.callee.data.identifier.name;
|
|
const scoped = if (self.scope) |scope| scope.lookupFn(id_name) orelse id_name else id_name;
|
|
if (self.program_index.ufcs_alias_map.get(id_name)) |target| {
|
|
break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target;
|
|
}
|
|
break :blk scoped;
|
|
};
|
|
// fix-0102 F2 / R5 §C: the early pack/comptime/generic dispatch reads
|
|
// the SAME author the call resolver SELECTED — not the first-wins
|
|
// winner — whenever a genuine flat same-name collision rerouted the
|
|
// call (`sel_author != null`). The selector only ever returns a plain
|
|
// free fn (`isPlainFreeFn` rejects type-params / comptime / pack), so
|
|
// `sel_author.decl` matches none of the arms below and the early path
|
|
// falls through to the main dispatch, which CONSUMES `sel_author` and
|
|
// binds that author. Without this the early path would dispatch the
|
|
// first-wins winner (e.g. a pack `(..$args)`) and disagree with the
|
|
// main dispatch — the selected plain author's bare call would invoke
|
|
// the wrong function. On the common path (`sel_author == null`) this
|
|
// reads the winner exactly as before — byte-identical, since the
|
|
// selector reroutes nothing there.
|
|
const early_fd: ?*const ast.FnDecl = if (sel_author) |sf| sf.decl else self.program_index.fn_ast_map.get(early_name);
|
|
if (early_fd) |fd| {
|
|
if (isPackFn(fd)) {
|
|
// Protocol packs (`..xs: P`) and comptime type-packs
|
|
// (`..$args`) both monomorphize per call shape.
|
|
return self.lowerPackFnCall(fd, c);
|
|
}
|
|
if (hasComptimeParams(fd)) {
|
|
return self.lowerComptimeCall(fd, c);
|
|
}
|
|
// Early detection of generic function calls — skip arg lowering for type params
|
|
// because lowerGenericCall resolves type params from AST nodes, not lowered refs.
|
|
// Only if the name is NOT shadowed by a local variable (closure, fn ptr, etc.).
|
|
// A selected author is never generic (`isPlainFreeFn` excludes
|
|
// `type_params > 0`), so this branch fires only on the winner.
|
|
const shadowed = if (self.scope) |scope| scope.lookup(c.callee.data.identifier.name) != null else false;
|
|
if (fd.type_params.len > 0 and !shadowed) {
|
|
// Types are explicit when call args match param count (e.g., are_equal(Point, p1, p2))
|
|
// Types are inferred when call args < param count (e.g., are_equal(p1, p2))
|
|
const types_explicit = c.args.len == fd.params.len;
|
|
var lowered_args = std.ArrayList(Ref).empty;
|
|
defer lowered_args.deinit(self.alloc);
|
|
for (c.args, 0..) |arg, ai| {
|
|
// Skip type param args only when types are passed explicitly
|
|
if (types_explicit and ai < fd.params.len and isTypeParamDecl(&fd.params[ai], fd.type_params)) {
|
|
lowered_args.append(self.alloc, Ref.none) catch unreachable;
|
|
} else {
|
|
const saved_target = self.target_type;
|
|
lowered_args.append(self.alloc, self.lowerExpr(arg)) catch unreachable;
|
|
self.target_type = saved_target;
|
|
}
|
|
}
|
|
return self.lowerGenericCall(fd, early_name, c, lowered_args.items);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lower args (with target type propagation for xx conversions)
|
|
var args = std.ArrayList(Ref).empty;
|
|
defer args.deinit(self.alloc);
|
|
// Try to resolve param types for target_type context
|
|
const param_types = self.resolveCallParamTypes(c, sel_author);
|
|
// For enum_literal callees (.Variant(payload)), resolve the payload target type
|
|
// from the union field type so struct literal fields get proper coercion
|
|
var enum_payload_ty: ?TypeId = null;
|
|
if (c.callee.data == .enum_literal) {
|
|
const target = self.target_type orelse .unresolved;
|
|
if (!target.isBuiltin()) {
|
|
const info = self.module.types.get(target);
|
|
if (info == .tagged_union) {
|
|
const tag = self.resolveVariantIndex(target, c.callee.data.enum_literal.name);
|
|
if (tag < info.tagged_union.fields.len) {
|
|
enum_payload_ty = info.tagged_union.fields[tag].ty;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for (c.args, 0..) |arg, ai| {
|
|
if (arg.data == .spread_expr) {
|
|
// Pack spread `..xs` / `..xs.method` → expand to N positional
|
|
// args here. A runtime-slice spread (`..arr`) is left as a
|
|
// placeholder for the slice-variadic path (packVariadicCallArgs).
|
|
if (self.packSpreadRefs(arg.data.spread_expr.operand, arg.span)) |elems| {
|
|
defer self.alloc.free(elems);
|
|
for (elems) |e| args.append(self.alloc, e) catch unreachable;
|
|
continue;
|
|
}
|
|
args.append(self.alloc, Ref.none) catch unreachable;
|
|
continue;
|
|
}
|
|
const saved_target = self.target_type;
|
|
if (ai < param_types.len) {
|
|
self.target_type = param_types[ai];
|
|
}
|
|
if (enum_payload_ty) |ept| {
|
|
if (ai == 0) self.target_type = ept;
|
|
}
|
|
// Implicit float→int narrowing of a compile-time float argument
|
|
// (incl. an expanded `param: T = expr` default) follows the unified
|
|
// rule: an integral comptime float folds, a non-integral one errors.
|
|
// A runtime float / `xx` cast is unaffected and coerces as before.
|
|
if (ai < param_types.len) {
|
|
if (self.foldComptimeFloatInit(arg, param_types[ai])) |folded| {
|
|
args.append(self.alloc, folded) catch unreachable;
|
|
self.target_type = saved_target;
|
|
continue;
|
|
}
|
|
}
|
|
// Implicit address-of: when param expects *T and arg is an identifier
|
|
// with an alloca of type T, pass the alloca pointer directly (reference
|
|
// semantics, so mutations through the pointer are visible to the caller).
|
|
if (ai < param_types.len and arg.data == .identifier) {
|
|
const pt = param_types[ai];
|
|
if (!pt.isBuiltin()) {
|
|
const pti = self.module.types.get(pt);
|
|
if (pti == .pointer) {
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(arg.data.identifier.name)) |binding| {
|
|
// Only apply when the binding type matches the pointee type
|
|
if (binding.is_alloca and binding.ty == pti.pointer.pointee) {
|
|
const ptr_ty = self.module.types.ptrTo(binding.ty);
|
|
args.append(self.alloc, self.builder.emit(.{ .addr_of = .{ .operand = binding.ref } }, ptr_ty)) catch unreachable;
|
|
self.target_type = saved_target;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Implicit address-of for compound lvalues (field access / index /
|
|
// deref): when the param expects `*T` and the arg is an addressable
|
|
// lvalue of type `T`, pass the lvalue's real address (GEP) — same
|
|
// reference semantics as the identifier case above. Without this the
|
|
// arg would be loaded into a temporary and the callee would mutate a
|
|
// throwaway copy (silent data loss — e.g. `make_move(self.board, m)`).
|
|
if (ai < param_types.len and (arg.data == .field_access or arg.data == .index_expr or arg.data == .deref_expr)) {
|
|
const pt = param_types[ai];
|
|
if (!pt.isBuiltin()) {
|
|
const pti = self.module.types.get(pt);
|
|
if (pti == .pointer and self.inferExprType(arg) == pti.pointer.pointee) {
|
|
// `lowerExprAsPtr` yields the lvalue's address, typed
|
|
// either as `*T` already (index/deref) or as the pointee
|
|
// `T` (a field "place" ref); normalize to `*T` — exactly
|
|
// what `@field_access` does.
|
|
const place = self.lowerExprAsPtr(arg);
|
|
const place_ty = self.builder.getRefType(place);
|
|
const ref: ?Ref = if (place_ty == pt)
|
|
place
|
|
else if (place_ty == pti.pointer.pointee)
|
|
self.builder.emit(.{ .addr_of = .{ .operand = place } }, pt)
|
|
else
|
|
null;
|
|
if (ref) |r| {
|
|
args.append(self.alloc, r) catch unreachable;
|
|
self.target_type = saved_target;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const val = self.lowerExpr(arg);
|
|
self.target_type = saved_target;
|
|
// Passing a `*T` where a `T` value is expected — a by-reference loop
|
|
// capture (`for xs: (*m)`), a `*T` parameter, or any pointer local —
|
|
// otherwise slips through to LLVM as an opaque "call parameter type
|
|
// does not match function signature" verifier error. Flag it at the
|
|
// call site with a `.*` fix-it.
|
|
if (ai < param_types.len) {
|
|
const vt = self.builder.getRefType(val);
|
|
const vti = self.module.types.get(vt);
|
|
if (vti == .pointer and vti.pointer.pointee == param_types[ai]) {
|
|
if (self.diagnostics) |d| {
|
|
const tn = self.formatTypeName(param_types[ai]);
|
|
if (arg.data == .identifier) {
|
|
const nm = arg.data.identifier.name;
|
|
const lead: []const u8 = if (self.refCapturePointee(arg) != null) "by-reference loop capture" else "argument";
|
|
const fix = std.fmt.allocPrint(self.alloc, "{s}.*", .{nm}) catch nm;
|
|
const pid = d.addFmtId(.err, arg.span, "{s} '{s}' has type '*{s}', but '{s}' is expected here", .{ lead, nm, tn, tn });
|
|
d.addHelpFmt(pid, arg.span, fix, "dereference it to pass the value: `{s}`", .{fix});
|
|
} else {
|
|
const pid = d.addFmtId(.err, arg.span, "this argument has type '*{s}', but '{s}' is expected here", .{ tn, tn });
|
|
d.addHelpFmt(pid, arg.span, null, "dereference it with `.*` to pass the value", .{});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
args.append(self.alloc, val) catch unreachable;
|
|
}
|
|
|
|
switch (c.callee.data) {
|
|
.identifier => |id| {
|
|
// Resolve local function name (bare → mangled) and UFCS aliases
|
|
const func_name = blk: {
|
|
// First try scope lookup for mangled local fn names
|
|
const scoped = if (self.scope) |scope| scope.lookupFn(id.name) orelse id.name else id.name;
|
|
// Then try UFCS alias on bare name
|
|
if (self.program_index.ufcs_alias_map.get(id.name)) |target| {
|
|
// Resolve the alias target through scope too (target may be mangled)
|
|
break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target;
|
|
}
|
|
break :blk scoped;
|
|
};
|
|
|
|
// Handle cast(TargetType, val) — emit conversion instructions
|
|
// Only for compile-time known types (type_expr or known type names)
|
|
if (std.mem.eql(u8, id.name, "cast") and c.args.len >= 2) {
|
|
const type_arg = c.args[0];
|
|
const is_static_type = blk: {
|
|
if (type_arg.data == .type_expr) break :blk true;
|
|
if (type_arg.data == .identifier) {
|
|
const tname = type_arg.data.identifier.name;
|
|
// Check if it's a known type name (not a runtime variable)
|
|
if (type_bridge.resolveTypePrimitive(tname) != null) break :blk true;
|
|
if (self.type_bindings) |bindings| {
|
|
if (bindings.get(tname) != null) break :blk true;
|
|
}
|
|
// Check if it's a registered struct/enum type name
|
|
const name_id = self.module.types.internString(tname);
|
|
if (self.module.types.findByName(name_id) != null) break :blk true;
|
|
}
|
|
break :blk false;
|
|
};
|
|
if (is_static_type) {
|
|
const dst_ty = self.resolveTypeArg(c.args[0]);
|
|
const val = args.items[1]; // already lowered
|
|
const src_ty = self.inferExprType(c.args[1]);
|
|
// Unbox Any → concrete type
|
|
if (src_ty == .any) {
|
|
return self.builder.emit(.{ .unbox_any = .{ .operand = val } }, dst_ty);
|
|
}
|
|
return self.coerceExplicit(val, src_ty, dst_ty);
|
|
}
|
|
// Runtime cast — fall through to builtin handling
|
|
}
|
|
// Check builtins first (these are handled natively by interpreter and emitter)
|
|
if (resolveBuiltin(id.name)) |bid| {
|
|
const ret_ty: TypeId = switch (bid) {
|
|
.size_of, .align_of => .s64,
|
|
.sqrt, .sin, .cos, .floor => blk: {
|
|
// Math builtins: return type matches argument type ($T -> T)
|
|
if (c.args.len > 0) {
|
|
const arg_ty = self.inferExprType(c.args[0]);
|
|
if (arg_ty == .f32) break :blk TypeId.f32;
|
|
}
|
|
break :blk TypeId.f64;
|
|
},
|
|
else => .void,
|
|
};
|
|
return self.builder.callBuiltin(bid, args.items, ret_ty);
|
|
}
|
|
// Check scope first: local variables (closures, fn ptrs) shadow global functions
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(id.name)) |binding| {
|
|
if (!binding.ty.isBuiltin()) {
|
|
const ty_info = self.module.types.get(binding.ty);
|
|
if (ty_info == .closure) {
|
|
const callee_ref = if (binding.is_alloca) self.builder.load(binding.ref, binding.ty) else binding.ref;
|
|
// Closure trampolines carry `__sx_ctx` at
|
|
// slot 0; emit_llvm's `call_closure` builds
|
|
// the call as [ctx, env, user_args], so we
|
|
// prepend ctx here. args[0] becomes ctx.
|
|
const owned = if (self.implicit_ctx_enabled) blk: {
|
|
const arr = self.alloc.alloc(Ref, args.items.len + 1) catch unreachable;
|
|
arr[0] = self.current_ctx_ref;
|
|
@memcpy(arr[1..], args.items);
|
|
break :blk arr;
|
|
} else self.alloc.dupe(Ref, args.items) catch unreachable;
|
|
const ret_ty = ty_info.closure.ret;
|
|
return self.builder.emit(.{ .call_closure = .{ .callee = callee_ref, .args = owned } }, ret_ty);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// fix-0102c / R5 §C: a genuine flat same-name collision — bind the
|
|
// author the call resolver selected (own-author-wins, or the single
|
|
// flat-reachable author), or reject a bare call to a name ≥2
|
|
// imported modules author. `selectedFreeAuthor` (computed once
|
|
// above, and the exact verdict `plan` consumes for typing) is the
|
|
// single producer; lowering CONSUMES it rather than re-resolving
|
|
// the name, so typing and dispatch read the SAME author and can't
|
|
// disagree (fix-0102 F2). Reached only for an identifier callee, so
|
|
// `sel_author` / `author_ambiguous` here are the bare verdict.
|
|
if (author_ambiguous) {
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, c.callee.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{func_name});
|
|
return Ref.none;
|
|
}
|
|
if (sel_author) |sf| {
|
|
const fid = self.selectedFuncId(sf, func_name);
|
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
|
const ret_ty = func.ret;
|
|
const params = func.params;
|
|
// The RESOLVED author's decl drives variadic packing — not a
|
|
// first-wins re-lookup by name, whose variadic shape may
|
|
// differ (fix-0102c F1).
|
|
self.packVariadicCallArgs(sf.decl, c, &args);
|
|
const final_args = self.prependCtxIfNeeded(func, args.items);
|
|
self.coerceCallArgs(final_args, params);
|
|
if (func.is_variadic) self.promoteCVariadicArgs(final_args, params.len);
|
|
return self.builder.call(fid, final_args, ret_ty);
|
|
}
|
|
// Check for comptime-expanded or generic functions
|
|
if (self.program_index.fn_ast_map.get(func_name)) |fd| {
|
|
if (hasComptimeParams(fd)) {
|
|
return self.lowerComptimeCall(fd, c);
|
|
}
|
|
if (fd.type_params.len > 0) {
|
|
// Runtime dispatch already handled above (before arg lowering)
|
|
return self.lowerGenericCall(fd, func_name, c, args.items);
|
|
}
|
|
}
|
|
// Check for #compiler free functions
|
|
if (self.program_index.fn_ast_map.get(func_name)) |fd_check| {
|
|
if (fd_check.body.data == .compiler_expr) {
|
|
const ret_ty = if (fd_check.return_type) |rt| type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map) else TypeId.void;
|
|
return self.builder.compilerCall(func_name, args.items, ret_ty);
|
|
}
|
|
}
|
|
|
|
// Look up declared/extern function — try lazy lowering if not yet lowered
|
|
{
|
|
// First attempt: function may already be declared (from scanDecls)
|
|
// but not yet lowered. Try lazy lowering if needed.
|
|
if (self.program_index.fn_ast_map.contains(func_name) and !self.lowered_functions.contains(func_name)) {
|
|
self.lazyLowerFunction(func_name);
|
|
}
|
|
if (self.resolveFuncByName(func_name)) |fid| {
|
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
|
const ret_ty = func.ret;
|
|
const params = func.params;
|
|
// Pack variadic args into a slice if the function has a variadic param
|
|
if (self.program_index.fn_ast_map.get(func_name)) |fd| {
|
|
self.packVariadicCallArgs(fd, c, &args);
|
|
}
|
|
const final_args = self.prependCtxIfNeeded(func, args.items);
|
|
// Coerce arguments to match parameter types
|
|
self.coerceCallArgs(final_args, params);
|
|
if (func.is_variadic) self.promoteCVariadicArgs(final_args, params.len);
|
|
return self.builder.call(fid, final_args, ret_ty);
|
|
}
|
|
}
|
|
// May be a variable holding a function pointer (non-closure)
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(id.name)) |binding| {
|
|
const callee_ref = if (binding.is_alloca) self.builder.load(binding.ref, binding.ty) else binding.ref;
|
|
const ret_ty = if (!binding.ty.isBuiltin()) blk: {
|
|
const bti = self.module.types.get(binding.ty);
|
|
break :blk if (bti == .function) bti.function.ret else .s64;
|
|
} else .s64;
|
|
var final_args = std.ArrayList(Ref).empty;
|
|
defer final_args.deinit(self.alloc);
|
|
if (self.fnPtrTypeWantsCtx(binding.ty)) {
|
|
final_args.append(self.alloc, self.current_ctx_ref) catch unreachable;
|
|
}
|
|
final_args.appendSlice(self.alloc, args.items) catch unreachable;
|
|
const owned = self.alloc.dupe(Ref, final_args.items) catch unreachable;
|
|
return self.builder.emit(.{ .call_indirect = .{ .callee = callee_ref, .args = owned } }, ret_ty);
|
|
}
|
|
}
|
|
// May be a global variable holding a function pointer
|
|
if (self.program_index.global_names.get(id.name)) |gi| {
|
|
if (!gi.ty.isBuiltin()) {
|
|
const gti = self.module.types.get(gi.ty);
|
|
if (gti == .function) {
|
|
const callee_ref = self.builder.emit(.{ .global_get = gi.id }, gi.ty);
|
|
// Coerce args to match fn-ptr param types (including implicit address-of)
|
|
for (args.items, 0..) |*arg, ai| {
|
|
if (ai < gti.function.params.len) {
|
|
const dst_ty = gti.function.params[ai];
|
|
const src_ty = self.inferExprType(c.args[ai]);
|
|
// Implicit address-of: passing T where *T expected
|
|
if (!dst_ty.isBuiltin()) {
|
|
const dti = self.module.types.get(dst_ty);
|
|
if (dti == .pointer and dti.pointer.pointee == src_ty and src_ty != .void) {
|
|
// For identifier args, pass the alloca directly (reference semantics)
|
|
if (c.args[ai].data == .identifier) {
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(c.args[ai].data.identifier.name)) |binding| {
|
|
if (binding.is_alloca) {
|
|
arg.* = self.builder.emit(.{ .addr_of = .{ .operand = binding.ref } }, dst_ty);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// For other expressions, copy semantics
|
|
const slot = self.builder.alloca(src_ty);
|
|
self.builder.store(slot, arg.*);
|
|
arg.* = slot;
|
|
continue;
|
|
}
|
|
}
|
|
arg.* = self.coerceToType(arg.*, src_ty, dst_ty);
|
|
}
|
|
}
|
|
var final_args = std.ArrayList(Ref).empty;
|
|
defer final_args.deinit(self.alloc);
|
|
if (self.fnPtrTypeWantsCtx(gi.ty)) {
|
|
final_args.append(self.alloc, self.current_ctx_ref) catch unreachable;
|
|
}
|
|
final_args.appendSlice(self.alloc, args.items) catch unreachable;
|
|
const owned = self.alloc.dupe(Ref, final_args.items) catch unreachable;
|
|
return self.builder.emit(.{ .call_indirect = .{ .callee = callee_ref, .args = owned } }, gti.function.ret);
|
|
}
|
|
}
|
|
}
|
|
// Unresolved function call
|
|
return self.emitError(id.name, c.callee.span);
|
|
},
|
|
.field_access => |fa| {
|
|
// `super.method(args)` from inside a `#jni_main` (or any
|
|
// sx-defined `#jni_class`) bodied method. Dispatch via
|
|
// CallNonvirtual<T>Method against the parent class
|
|
// resolved from the enclosing fcd's `#extends` clause.
|
|
if (fa.object.data == .identifier and
|
|
std.mem.eql(u8, fa.object.data.identifier.name, "super"))
|
|
{
|
|
return self.lowerSuperCall(fa.field, args.items, c.callee.span);
|
|
}
|
|
|
|
// `Alias.method(args)` where Alias is a foreign-class
|
|
// identifier and `method` is a `static` member — JNI
|
|
// dispatch via FindClass + GetStaticMethodID + CallStatic*,
|
|
// OR (for `new`) via FindClass + GetMethodID("<init>") +
|
|
// NewObject. Falls through to existing paths when no match.
|
|
if (fa.object.data == .identifier) {
|
|
const alias = fa.object.data.identifier.name;
|
|
if (self.program_index.foreign_class_map.get(alias)) |fcd| {
|
|
for (fcd.members) |m| switch (m) {
|
|
.method => |md| if (md.is_static and std.mem.eql(u8, md.name, fa.field)) {
|
|
return self.lowerForeignStaticCall(fcd, md, args.items, c.callee.span);
|
|
},
|
|
else => {},
|
|
};
|
|
}
|
|
}
|
|
|
|
// Type constructor call: Sx(f32).user(0.5) — obj is a call that returns a type
|
|
if (fa.object.data == .call) {
|
|
const inner_call = &fa.object.data.call;
|
|
if (inner_call.callee.data == .identifier) {
|
|
const inner_name = inner_call.callee.data.identifier.name;
|
|
const resolved = if (self.scope) |scope| (scope.lookupFn(inner_name) orelse inner_name) else inner_name;
|
|
|
|
// Generic struct static method: Animated(Size).make(...)
|
|
if (self.program_index.struct_template_map.getPtr(resolved)) |tmpl| {
|
|
const inst_ty = self.instantiateGenericStruct(tmpl, inner_call.args);
|
|
const inst_name = self.formatTypeName(inst_ty);
|
|
// Look up template method, monomorphize, and call
|
|
if (self.struct_instance_template.get(inst_name)) |tmpl_name| {
|
|
const tmpl_qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tmpl_name, fa.field }) catch fa.field;
|
|
if (self.program_index.fn_ast_map.get(tmpl_qualified)) |fd| {
|
|
if (self.struct_instance_bindings.getPtr(inst_name)) |bindings| {
|
|
const mangled = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ inst_name, fa.field }) catch fa.field;
|
|
if (!self.lowered_functions.contains(mangled)) {
|
|
self.monomorphizeFunction(fd, mangled, bindings);
|
|
}
|
|
if (self.resolveFuncByName(mangled)) |fid| {
|
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
|
const final_args = self.prependCtxIfNeeded(func, args.items);
|
|
self.coerceCallArgs(final_args, func.params);
|
|
return self.builder.call(fid, final_args, func.ret);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (self.program_index.fn_ast_map.get(resolved)) |fd| {
|
|
if (fd.type_params.len > 0) {
|
|
// Try instantiate as type function
|
|
if (self.instantiateTypeFunction(inner_name, inner_name, fd, inner_call.args)) |result_ty| {
|
|
const type_info = self.module.types.get(result_ty);
|
|
if (type_info == .tagged_union) {
|
|
// Qualified enum construction: Type.variant(payload)
|
|
const tag = self.resolveVariantIndex(result_ty, fa.field);
|
|
var payload = if (args.items.len > 0) args.items[0] else Ref.none;
|
|
if (!payload.isNone()) {
|
|
const fields = type_info.tagged_union.fields;
|
|
if (tag < fields.len) {
|
|
const field_ty = fields[tag].ty;
|
|
if (field_ty != .void) {
|
|
const payload_ty = self.inferExprType(c.args[0]);
|
|
if (field_ty != payload_ty) {
|
|
payload = self.coerceToType(payload, payload_ty, field_ty);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return self.builder.enumInit(tag, payload, result_ty);
|
|
}
|
|
if (type_info == .@"enum") {
|
|
const tag = self.resolveVariantIndex(result_ty, fa.field);
|
|
return self.builder.enumInit(tag, Ref.none, result_ty);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Namespace-qualified call (e.g. `std.print`) vs method / UFCS
|
|
// call on a value (`recv.method`). This boundary decides whether
|
|
// the receiver is prepended, so it MUST agree with the call
|
|
// plan's `free_fn_ufcs` (prepends) vs `namespace_fn` (does not)
|
|
// classification — source it from the single definition in
|
|
// `CallResolver` rather than re-deriving it here.
|
|
const is_namespace = !self.callResolver().objectIsValue(fa.object);
|
|
|
|
if (is_namespace) {
|
|
// Namespace call: module.func(args) — don't prepend object
|
|
const func_name = fa.field;
|
|
// Also try qualified name: Namespace.method (for struct methods)
|
|
const ns_name: ?[]const u8 = switch (fa.object.data) {
|
|
.identifier => |id| id.name,
|
|
.type_expr => |te| te.name,
|
|
else => null,
|
|
};
|
|
const qualified_name = if (ns_name) |n|
|
|
std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ n, fa.field }) catch func_name
|
|
else
|
|
func_name;
|
|
// Check for comptime-expanded or generic functions (try both names)
|
|
const effective_name = if (self.program_index.fn_ast_map.get(qualified_name) != null) qualified_name else func_name;
|
|
if (self.program_index.fn_ast_map.get(effective_name)) |fd| {
|
|
if (hasComptimeParams(fd)) {
|
|
return self.lowerComptimeCall(fd, c);
|
|
}
|
|
if (fd.type_params.len > 0) {
|
|
return self.lowerGenericCall(fd, effective_name, c, args.items);
|
|
}
|
|
}
|
|
if (self.program_index.fn_ast_map.contains(effective_name) and !self.lowered_functions.contains(effective_name)) {
|
|
self.lazyLowerFunction(effective_name);
|
|
}
|
|
if (self.resolveFuncByName(effective_name)) |fid| {
|
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
|
const ret_ty = func.ret;
|
|
const params = func.params;
|
|
if (self.program_index.fn_ast_map.get(effective_name)) |fd| {
|
|
self.packVariadicCallArgs(fd, c, &args);
|
|
}
|
|
const final_args = self.prependCtxIfNeeded(func, args.items);
|
|
self.coerceCallArgs(final_args, params);
|
|
if (func.is_variadic) self.promoteCVariadicArgs(final_args, params.len);
|
|
return self.builder.call(fid, final_args, ret_ty);
|
|
}
|
|
// Check if this is Type.variant(payload) — qualified enum construction
|
|
if (ns_name) |type_name| {
|
|
const type_name_id = self.module.types.internString(type_name);
|
|
if (self.module.types.findByName(type_name_id)) |union_ty| {
|
|
const type_info = self.module.types.get(union_ty);
|
|
if (type_info == .tagged_union) {
|
|
const tag = self.resolveVariantIndex(union_ty, func_name);
|
|
var payload = if (args.items.len > 0) args.items[0] else Ref.none;
|
|
// Coerce payload to match field type
|
|
if (!payload.isNone()) {
|
|
const fields = type_info.tagged_union.fields;
|
|
if (tag < fields.len) {
|
|
const field_ty = fields[tag].ty;
|
|
const payload_ty = self.inferExprType(c.args[0]);
|
|
if (field_ty != payload_ty) {
|
|
payload = self.coerceToType(payload, payload_ty, field_ty);
|
|
}
|
|
}
|
|
}
|
|
return self.builder.enumInit(tag, payload, union_ty);
|
|
}
|
|
if (type_info == .@"enum") {
|
|
const tag = self.resolveVariantIndex(union_ty, func_name);
|
|
return self.builder.enumInit(tag, Ref.none, union_ty);
|
|
}
|
|
}
|
|
}
|
|
return self.emitError(func_name, c.callee.span);
|
|
}
|
|
|
|
// Method call: obj.method(args) → prepend obj (or &obj for *Self receivers)
|
|
// For ptr.*.method(): pass the pointer directly instead of loading + re-addressing.
|
|
// This ensures mutations through self: *T are visible after the call.
|
|
var obj_ty: TypeId = undefined;
|
|
var obj: Ref = undefined;
|
|
var effective_obj_node: *const Node = fa.object;
|
|
if (fa.object.data == .deref_expr) {
|
|
effective_obj_node = fa.object.data.deref_expr.operand;
|
|
obj_ty = self.inferExprType(effective_obj_node);
|
|
obj = self.lowerExpr(effective_obj_node);
|
|
} else {
|
|
obj_ty = self.inferExprType(fa.object);
|
|
obj = self.lowerExpr(fa.object);
|
|
}
|
|
|
|
// Check if field is a closure type — call as closure, not method
|
|
if (!obj_ty.isBuiltin()) {
|
|
const field_name_id = self.module.types.internString(fa.field);
|
|
const struct_fields = self.getStructFields(obj_ty);
|
|
for (struct_fields, 0..) |f, fi| {
|
|
if (f.name == field_name_id and !f.ty.isBuiltin()) {
|
|
const fti = self.module.types.get(f.ty);
|
|
if (fti == .closure) {
|
|
// structGet requires an aggregate value; if obj is *T, load through it first.
|
|
var agg = obj;
|
|
const oi = self.module.types.get(obj_ty);
|
|
if (oi == .pointer) {
|
|
agg = self.builder.load(obj, oi.pointer.pointee);
|
|
}
|
|
const closure_val = self.builder.structGet(agg, @intCast(fi), f.ty);
|
|
// Prepend ctx for sx-side closure call ABI.
|
|
const owned = if (self.implicit_ctx_enabled) blk: {
|
|
const arr = self.alloc.alloc(Ref, args.items.len + 1) catch unreachable;
|
|
arr[0] = self.current_ctx_ref;
|
|
@memcpy(arr[1..], args.items);
|
|
break :blk arr;
|
|
} else self.alloc.dupe(Ref, args.items) catch unreachable;
|
|
return self.builder.emit(.{ .call_closure = .{ .callee = closure_val, .args = owned } }, fti.closure.ret);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if receiver is a protocol type → dispatch through vtable/fn_ptrs
|
|
if (self.getProtocolInfo(obj_ty)) |proto_info| {
|
|
return self.emitProtocolDispatch(obj, proto_info, fa.field, args.items, obj_ty);
|
|
}
|
|
|
|
// Check if receiver is `?Protocol` — for sentinel-shaped
|
|
// optionals (Protocol has ctx as first ptr field, and a
|
|
// null ctx is the "none" state) the unwrap is a no-op
|
|
// structurally. Treat the optional value as the protocol
|
|
// value and dispatch. Calling a method on a null protocol
|
|
// is undefined (same as derefing a null pointer); user
|
|
// guards with `if x != null` first.
|
|
if (!obj_ty.isBuiltin()) {
|
|
const opt_info = self.module.types.get(obj_ty);
|
|
if (opt_info == .optional) {
|
|
const pay_ty = opt_info.optional.child;
|
|
if (self.getProtocolInfo(pay_ty)) |proto_info| {
|
|
return self.emitProtocolDispatch(obj, proto_info, fa.field, args.items, pay_ty);
|
|
}
|
|
}
|
|
}
|
|
|
|
var method_args = std.ArrayList(Ref).empty;
|
|
defer method_args.deinit(self.alloc);
|
|
method_args.append(self.alloc, obj) catch unreachable;
|
|
for (args.items) |a| {
|
|
method_args.append(self.alloc, a) catch unreachable;
|
|
}
|
|
|
|
// Foreign-class DSL: `inst.method(args)` where `inst`'s
|
|
// type is an alias declared by `#jni_class("...") { ... }`
|
|
// (or its parallel forms). Routes to the JNI dispatch
|
|
// shape, descriptor derived from the sx signature.
|
|
const struct_name = self.getStructTypeName(obj_ty);
|
|
if (struct_name) |sname_for_foreign| {
|
|
if (self.program_index.foreign_class_map.get(sname_for_foreign)) |fcd| {
|
|
return self.lowerForeignMethodCall(fcd, fa.field, obj, args.items, c.callee.span);
|
|
}
|
|
}
|
|
|
|
// Try to resolve the method by struct type name
|
|
if (struct_name) |sname| {
|
|
// Try direct qualified name: StructName.method
|
|
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sname, fa.field }) catch fa.field;
|
|
|
|
// Generic #compiler method dispatch
|
|
if (self.program_index.fn_ast_map.get(qualified)) |method_fd| {
|
|
if (method_fd.body.data == .compiler_expr) {
|
|
const ret_ty = if (method_fd.return_type) |rt|
|
|
type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map)
|
|
else
|
|
.void;
|
|
return self.builder.compilerCall(qualified, method_args.items, ret_ty);
|
|
}
|
|
}
|
|
|
|
// Check for generic struct template method
|
|
if (self.struct_instance_template.get(sname)) |tmpl_name| {
|
|
// This is an instantiated generic struct — look up template method
|
|
const tmpl_qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tmpl_name, fa.field }) catch fa.field;
|
|
if (self.program_index.fn_ast_map.get(tmpl_qualified)) |fd| {
|
|
// Get the stored type bindings for this instance
|
|
if (self.struct_instance_bindings.getPtr(sname)) |bindings| {
|
|
// Monomorphize the method with the struct's type bindings
|
|
const mangled = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sname, fa.field }) catch fa.field;
|
|
if (!self.lowered_functions.contains(mangled)) {
|
|
self.monomorphizeFunction(fd, mangled, bindings);
|
|
}
|
|
if (self.resolveFuncByName(mangled)) |fid| {
|
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
|
const ret_ty = func.ret;
|
|
const params = func.params;
|
|
self.fixupMethodReceiver(&method_args, func, effective_obj_node, obj_ty);
|
|
self.appendDefaultArgs(fd, &method_args);
|
|
const final_args = self.prependCtxIfNeeded(func, method_args.items);
|
|
self.coerceCallArgs(final_args, params);
|
|
return self.builder.call(fid, final_args, ret_ty);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Generic method on a non-template struct: `obj.method($T, ...)`
|
|
// or inferred form `obj.method(val)` where val's type pins $T.
|
|
if (self.program_index.fn_ast_map.get(qualified)) |gen_fd| {
|
|
if (gen_fd.type_params.len > 0 and gen_fd.body.data != .compiler_expr) {
|
|
// Effective AST args: prepend receiver so positions
|
|
// line up with fd.params (which has self at index 0).
|
|
var eff_args = std.ArrayList(*const Node).empty;
|
|
defer eff_args.deinit(self.alloc);
|
|
eff_args.append(self.alloc, effective_obj_node) catch unreachable;
|
|
for (c.args) |a| eff_args.append(self.alloc, a) catch unreachable;
|
|
|
|
var gbindings = self.genericResolver().buildTypeBindings(gen_fd, eff_args.items);
|
|
defer gbindings.deinit();
|
|
|
|
const gmangled = self.genericResolver().mangleGenericName(qualified, gen_fd, &gbindings);
|
|
if (!self.lowered_functions.contains(gmangled)) {
|
|
self.monomorphizeFunction(gen_fd, gmangled, &gbindings);
|
|
}
|
|
if (self.resolveFuncByName(gmangled)) |gfid| {
|
|
const gfunc = &self.module.functions.items[@intFromEnum(gfid)];
|
|
const gret_ty = gfunc.ret;
|
|
const gparams = gfunc.params;
|
|
// Strip type-decl slots from method_args. method_args[0] is the
|
|
// receiver (corresponds to fd.params[0] = self, never a type decl).
|
|
// Walk fd.params[1..], advance arg_idx through method_args[1..].
|
|
var gvalue_args = std.ArrayList(Ref).empty;
|
|
defer gvalue_args.deinit(self.alloc);
|
|
gvalue_args.append(self.alloc, method_args.items[0]) catch unreachable;
|
|
const types_explicit = method_args.items.len == gen_fd.params.len;
|
|
var arg_idx: usize = 1;
|
|
for (gen_fd.params[1..]) |p| {
|
|
if (isTypeParamDecl(&p, gen_fd.type_params)) {
|
|
if (types_explicit) arg_idx += 1;
|
|
continue;
|
|
}
|
|
if (arg_idx < method_args.items.len) {
|
|
gvalue_args.append(self.alloc, method_args.items[arg_idx]) catch unreachable;
|
|
}
|
|
arg_idx += 1;
|
|
}
|
|
self.fixupMethodReceiver(&gvalue_args, gfunc, effective_obj_node, obj_ty);
|
|
const final_args = self.prependCtxIfNeeded(gfunc, gvalue_args.items);
|
|
self.coerceCallArgs(final_args, gparams);
|
|
return self.builder.call(gfid, final_args, gret_ty);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try non-generic qualified method
|
|
if (self.program_index.fn_ast_map.get(qualified)) |fd| {
|
|
if (!self.lowered_functions.contains(qualified)) {
|
|
self.lazyLowerFunction(qualified);
|
|
}
|
|
_ = fd;
|
|
}
|
|
if (self.resolveFuncByName(qualified)) |fid| {
|
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
|
const ret_ty = func.ret;
|
|
const params = func.params;
|
|
const has_ctx = func.has_implicit_ctx;
|
|
self.fixupMethodReceiver(&method_args, func, effective_obj_node, obj_ty);
|
|
// Note: coerceCallArgs can trigger protocol thunk creation
|
|
// (module.addFunction), invalidating func pointer.
|
|
// Use pre-extracted params/ret_ty (+ has_ctx) instead of
|
|
// func.* after this.
|
|
const final_args = blk: {
|
|
if (!has_ctx) break :blk method_args.items;
|
|
const new_args = self.alloc.alloc(Ref, method_args.items.len + 1) catch break :blk method_args.items;
|
|
new_args[0] = self.current_ctx_ref;
|
|
@memcpy(new_args[1..], method_args.items);
|
|
break :blk new_args;
|
|
};
|
|
self.coerceCallArgs(final_args, params);
|
|
return self.builder.call(fid, final_args, ret_ty);
|
|
}
|
|
}
|
|
|
|
// Try to resolve as bare function name (free-function UFCS:
|
|
// `recv.fn(args)` → `fn(recv, args)`). Lazily lower the body —
|
|
// a function reached ONLY via UFCS would otherwise be declared
|
|
// but never emitted (issue 0063: undefined symbol at link).
|
|
//
|
|
// fix-0102d site 3 / R5 §C: a free-function UFCS target with a
|
|
// genuine flat same-name collision dispatches to the author the
|
|
// call PLAN selected for the receiver's source — the SAME author
|
|
// plan typed the call's result as, so dispatch and typing can't
|
|
// disagree (fix-0102 F2; without this, a string-typed winner over
|
|
// an s64 shadow boxes a raw int as a string pointer → segfault).
|
|
// The plan is the single producer; lowering consumes its verdict
|
|
// (`sel_author` / `cplan.ambiguous_collision`, computed once above)
|
|
// rather than re-resolving the field name. `.ambiguous` → loud
|
|
// diagnostic; otherwise the existing first-wins lazy path.
|
|
const ufcs_fid: ?FuncId = blk_uf: {
|
|
if (author_ambiguous) {
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, c.callee.span, "'{s}' is ambiguous; declared by multiple imported modules — qualify the call", .{fa.field});
|
|
return Ref.none;
|
|
}
|
|
if (sel_author) |sf| {
|
|
break :blk_uf self.selectedFuncId(sf, fa.field);
|
|
}
|
|
if (self.program_index.fn_ast_map.get(fa.field)) |_| {
|
|
if (!self.lowered_functions.contains(fa.field)) {
|
|
self.lazyLowerFunction(fa.field);
|
|
}
|
|
}
|
|
break :blk_uf self.resolveFuncByName(fa.field);
|
|
};
|
|
if (ufcs_fid) |fid| {
|
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
|
const ret_ty = func.ret;
|
|
const params = func.params;
|
|
// Same implicit address-of as a struct-defined method: if the
|
|
// free function's first param is `*T` and the receiver is a
|
|
// value `T`, pass its address instead of a by-value copy
|
|
// (issue 0063).
|
|
self.fixupMethodReceiver(&method_args, func, effective_obj_node, obj_ty);
|
|
const final_args = self.prependCtxIfNeeded(func, method_args.items);
|
|
self.coerceCallArgs(final_args, params);
|
|
return self.builder.call(fid, final_args, ret_ty);
|
|
}
|
|
return self.emitError(fa.field, c.callee.span);
|
|
},
|
|
.enum_literal => |el| {
|
|
const target_opt: ?TypeId = self.target_type;
|
|
|
|
// Try struct-method dispatch first: .{...}.method() where target is a struct
|
|
if (target_opt) |tgt| {
|
|
if (!tgt.isBuiltin()) {
|
|
const target_info = self.module.types.get(tgt);
|
|
if (target_info == .@"struct") {
|
|
const struct_name = self.module.types.typeName(tgt);
|
|
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ struct_name, el.name }) catch el.name;
|
|
if (self.program_index.fn_ast_map.get(qualified)) |fd| {
|
|
if (fd.type_params.len > 0) {
|
|
return self.lowerGenericCall(fd, qualified, c, args.items);
|
|
}
|
|
if (!self.lowered_functions.contains(qualified)) {
|
|
self.lazyLowerFunction(qualified);
|
|
}
|
|
}
|
|
if (self.resolveFuncByName(qualified)) |fid| {
|
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
|
const ret_ty = func.ret;
|
|
const params = func.params;
|
|
const final_args = self.prependCtxIfNeeded(func, args.items);
|
|
self.coerceCallArgs(final_args, params);
|
|
return self.builder.call(fid, final_args, ret_ty);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// .Variant(payload) — tagged enum construction. Requires target to be a tagged union.
|
|
const target = blk: {
|
|
if (target_opt) |tgt| {
|
|
if (!tgt.isBuiltin() and self.module.types.get(tgt) == .tagged_union) break :blk tgt;
|
|
}
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, c.callee.span, "cannot infer enum type for '.{s}' \u{2014} use an explicit type or assign to a typed variable", .{el.name});
|
|
}
|
|
return self.emitPlaceholder(el.name);
|
|
};
|
|
const tag = self.resolveVariantIndex(target, el.name);
|
|
var payload = if (args.items.len > 0) args.items[0] else Ref.none;
|
|
// Coerce payload to match the field type
|
|
if (!payload.isNone() and !target.isBuiltin()) {
|
|
const info = self.module.types.get(target);
|
|
if (info == .tagged_union) {
|
|
const fields = info.tagged_union.fields;
|
|
if (tag < fields.len) {
|
|
const field_ty = fields[tag].ty;
|
|
const payload_ty = self.inferExprType(c.args[0]);
|
|
if (field_ty != payload_ty) {
|
|
payload = self.coerceToType(payload, payload_ty, field_ty);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return self.builder.enumInit(tag, payload, target);
|
|
},
|
|
else => {
|
|
// Indirect call through expression
|
|
const callee_ref = self.lowerExpr(c.callee);
|
|
const owned = self.alloc.dupe(Ref, args.items) catch unreachable;
|
|
return self.builder.emit(.{ .call_indirect = .{ .callee = callee_ref, .args = owned } }, .s64);
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Emit a diagnostic for code that needs `Context` (allocator
|
|
/// protocol, `push Context.{...}`, the `context` identifier) when
|
|
/// the program hasn't registered the type — i.e. doesn't transitively
|
|
/// import `modules/std.sx`. Returns a placeholder Ref so the lowering
|
|
/// can keep going and surface any additional errors.
|
|
fn diagnoseMissingContext(self: *Lowering, what: []const u8) Ref {
|
|
if (self.diagnostics) |d| {
|
|
const span = ast.Span{ .start = 0, .end = 0 };
|
|
d.addFmt(.err, span, "{s} requires the Context type — add `#import \"modules/std.sx\";` (or a module that imports it)", .{what});
|
|
}
|
|
return self.emitPlaceholder("missing-context");
|
|
}
|
|
|
|
/// Emit `context.allocator.alloc(size)` dispatch — used by internal
|
|
/// compiler-driven heap copies (e.g. the `xx value` protocol-erasure
|
|
/// path in `buildProtocolValue`). Routes through whatever allocator is
|
|
/// currently installed in `context`, so a surrounding
|
|
/// `push Context.{ allocator = my_alloc, ... }` actually backs every
|
|
/// allocation including the ones the compiler inserts.
|
|
///
|
|
/// If `Context` isn't registered (the program doesn't import std.sx),
|
|
/// emits a diagnostic and returns a placeholder. We deliberately do
|
|
/// NOT fall back to a direct libc malloc — that was the silent escape
|
|
/// hatch that bit us through the implicit-context refactor (see the
|
|
/// "Silent unimplemented arms" REJECTED PATTERN in CLAUDE.md).
|
|
fn allocViaContext(self: *Lowering, size_ref: Ref, void_ptr_ty: TypeId) Ref {
|
|
if (!self.implicit_ctx_enabled or self.current_ctx_ref == Ref.none) {
|
|
return self.diagnoseMissingContext("heap allocation");
|
|
}
|
|
const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse {
|
|
return self.diagnoseMissingContext("heap allocation");
|
|
};
|
|
const ctx_ty_info = self.module.types.get(ctx_ty);
|
|
if (ctx_ty_info != .@"struct" or ctx_ty_info.@"struct".fields.len < 1) {
|
|
return self.diagnoseMissingContext("heap allocation");
|
|
}
|
|
const allocator_ty = ctx_ty_info.@"struct".fields[0].ty;
|
|
const ctx = self.builder.load(self.current_ctx_ref, ctx_ty);
|
|
const allocator = self.builder.structGet(ctx, 0, allocator_ty);
|
|
// #inline Allocator protocol layout: { ctx, alloc_fn_ptr, dealloc_fn_ptr }.
|
|
// field 0 = receiver ctx, field 1 = alloc fn-ptr.
|
|
const alloc_ctx = self.builder.structGet(allocator, 0, void_ptr_ty);
|
|
const fn_ptr = self.builder.structGet(allocator, 1, void_ptr_ty);
|
|
// Allocator thunks are sx-side and carry the implicit __sx_ctx at
|
|
// slot 0. Forward our caller's current_ctx_ref so the thunk's body
|
|
// (and the concrete alloc method it forwards to) has a real
|
|
// Context to thread on.
|
|
const args = if (self.implicit_ctx_enabled)
|
|
self.alloc.dupe(Ref, &.{ self.current_ctx_ref, alloc_ctx, size_ref }) catch unreachable
|
|
else
|
|
self.alloc.dupe(Ref, &.{ alloc_ctx, size_ref }) catch unreachable;
|
|
return self.builder.emit(.{ .call_indirect = .{
|
|
.callee = fn_ptr,
|
|
.args = args,
|
|
} }, void_ptr_ty);
|
|
}
|
|
|
|
/// Emit a call to a foreign-declared function looked up by name.
|
|
/// Used for the compiler-internal byte-copy in the protocol-erasure
|
|
/// heap path and the closure env-copy path, both of which need
|
|
/// libc `memcpy` after the `#builtin` form was dropped.
|
|
fn callForeign(self: *Lowering, name: []const u8, args: []const Ref, ret_ty: TypeId) Ref {
|
|
const fid = self.resolveFuncByName(name) orelse @panic("foreign symbol missing — std.sx not imported?");
|
|
return self.builder.call(fid, args, ret_ty);
|
|
}
|
|
|
|
/// Prepend the caller's current `__sx_ctx` to `args` when the callee
|
|
/// has the implicit context param. Returns either the original `args`
|
|
/// (when no prepend is needed) or a newly-allocated slice with ctx at
|
|
/// slot 0. The returned slice is mutable so callers can pass it
|
|
/// straight into `coerceCallArgs`. Direct callers that built the args
|
|
/// themselves with __sx_ctx already prepended (protocol thunks, FFI
|
|
/// wrappers in Step 4) should NOT call this — they already manage
|
|
/// slot 0.
|
|
fn prependCtxIfNeeded(self: *Lowering, callee: *const Function, args: []Ref) []Ref {
|
|
if (!callee.has_implicit_ctx) return args;
|
|
const new_args = self.alloc.alloc(Ref, args.len + 1) catch return args;
|
|
new_args[0] = self.current_ctx_ref;
|
|
@memcpy(new_args[1..], args);
|
|
return new_args;
|
|
}
|
|
|
|
pub fn resolveFuncByName(self: *Lowering, name: []const u8) ?FuncId {
|
|
// Check foreign name map first (e.g., "c_abs" → "abs")
|
|
const effective_name = self.foreign_name_map.get(name) orelse name;
|
|
const name_id = self.module.types.internString(effective_name);
|
|
for (self.module.functions.items, 0..) |func, i| {
|
|
if (func.name == name_id) return FuncId.fromIndex(@intCast(i));
|
|
}
|
|
return null;
|
|
}
|
|
|
|
pub fn resolveBuiltin(name: []const u8) ?inst_mod.BuiltinId {
|
|
const builtins = .{
|
|
// Note: "print" is NOT here — it's a comptime-expanded function, not a simple builtin
|
|
.{ "out", inst_mod.BuiltinId.out },
|
|
.{ "sqrt", inst_mod.BuiltinId.sqrt },
|
|
.{ "sin", inst_mod.BuiltinId.sin },
|
|
.{ "cos", inst_mod.BuiltinId.cos },
|
|
.{ "floor", inst_mod.BuiltinId.floor },
|
|
.{ "size_of", inst_mod.BuiltinId.size_of },
|
|
.{ "align_of", inst_mod.BuiltinId.align_of },
|
|
.{ "cast", inst_mod.BuiltinId.cast },
|
|
};
|
|
inline for (builtins) |entry| {
|
|
if (std.mem.eql(u8, name, entry[0])) return entry[1];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// ── Lambda/closure ────────────────────────────────────────────
|
|
|
|
const CaptureInfo = struct {
|
|
name: []const u8,
|
|
ty: TypeId,
|
|
ref: Ref, // alloca or value ref in the parent scope
|
|
is_alloca: bool,
|
|
};
|
|
|
|
fn lowerLambda(self: *Lowering, lam: *const ast.Lambda) Ref {
|
|
// Lower the lambda body as a new anonymous function
|
|
var buf: [64]u8 = undefined;
|
|
const name = std.fmt.bufPrint(&buf, "__lambda_{d}", .{self.block_counter}) catch "__lambda";
|
|
self.block_counter += 1;
|
|
|
|
// Collect lambda param names for exclusion from captures
|
|
var param_names = std.StringHashMap(void).init(self.alloc);
|
|
defer param_names.deinit();
|
|
for (lam.params) |p| {
|
|
param_names.put(p.name, {}) catch {};
|
|
}
|
|
|
|
// Pre-scan lambda body AST for free variables (captures)
|
|
var captures = std.ArrayList(CaptureInfo).empty;
|
|
defer captures.deinit(self.alloc);
|
|
self.collectCaptures(lam.body, ¶m_names, &captures);
|
|
|
|
// Deduplicate captures
|
|
var seen = std.StringHashMap(void).init(self.alloc);
|
|
defer seen.deinit();
|
|
var deduped = std.ArrayList(CaptureInfo).empty;
|
|
defer deduped.deinit(self.alloc);
|
|
for (captures.items) |cap| {
|
|
if (!seen.contains(cap.name)) {
|
|
seen.put(cap.name, {}) catch {};
|
|
deduped.append(self.alloc, cap) catch {};
|
|
}
|
|
}
|
|
const capture_list = deduped.items;
|
|
|
|
// Build env struct type if there are captures
|
|
var env_struct_ty: TypeId = .void;
|
|
if (capture_list.len > 0) {
|
|
const env_field_data = self.alloc.alloc(types.TypeInfo.StructInfo.Field, capture_list.len) catch unreachable;
|
|
for (capture_list, 0..) |cap, i| {
|
|
var nbuf: [32]u8 = undefined;
|
|
const fname = std.fmt.bufPrint(&nbuf, "cap_{d}", .{i}) catch "cap";
|
|
env_field_data[i] = .{
|
|
.name = self.module.types.internString(fname),
|
|
.ty = cap.ty,
|
|
};
|
|
}
|
|
const env_name = std.fmt.bufPrint(&buf, "__env_{d}", .{self.block_counter}) catch "__env";
|
|
const env_name_id = self.module.types.internString(env_name);
|
|
env_struct_ty = self.module.types.intern(.{ .@"struct" = .{
|
|
.name = env_name_id,
|
|
.fields = env_field_data,
|
|
} });
|
|
}
|
|
|
|
// Save current builder state
|
|
const saved_func = self.builder.func;
|
|
const saved_block = self.builder.current_block;
|
|
const saved_counter = self.builder.inst_counter;
|
|
const saved_scope = self.scope;
|
|
|
|
// Build param list. Convention when implicit_ctx is enabled:
|
|
// slot 0 = __sx_ctx: *void
|
|
// slot 1 = env: *void
|
|
// slot 2+ = user params
|
|
// Without implicit_ctx, env is slot 0 and user params follow.
|
|
var params = std.ArrayList(Function.Param).empty;
|
|
const env_ptr_ty = self.module.types.ptrTo(.void);
|
|
const lambda_wants_ctx = self.implicit_ctx_enabled and lam.call_conv != .c;
|
|
if (lambda_wants_ctx) {
|
|
params.append(self.alloc, .{
|
|
.name = self.module.types.internString("__sx_ctx"),
|
|
.ty = env_ptr_ty,
|
|
}) catch unreachable;
|
|
}
|
|
params.append(self.alloc, .{
|
|
.name = self.module.types.internString("env"),
|
|
.ty = env_ptr_ty,
|
|
}) catch unreachable;
|
|
// Get target closure param types for inference (from Closure(T1, T2) -> R annotations)
|
|
const target_closure_params: ?[]const TypeId = if (self.target_type) |tt| blk: {
|
|
if (!tt.isBuiltin()) {
|
|
const tti = self.module.types.get(tt);
|
|
if (tti == .closure) break :blk tti.closure.params;
|
|
// Unwrap ?Closure(...) → Closure(...)
|
|
if (tti == .optional) {
|
|
const inner = tti.optional.child;
|
|
if (!inner.isBuiltin()) {
|
|
const inner_info = self.module.types.get(inner);
|
|
if (inner_info == .closure) break :blk inner_info.closure.params;
|
|
}
|
|
}
|
|
}
|
|
break :blk null;
|
|
} else null;
|
|
// User params follow the ctx (optional) + env slots in `params`.
|
|
const user_param_base: usize = (if (lambda_wants_ctx) @as(usize, 1) else 0) + 1;
|
|
for (lam.params, 0..) |p, pi| {
|
|
const pty: TypeId = blk: {
|
|
// Unannotated lambda params take their type positionally from
|
|
// the target `Closure(T0, …)` signature. Resolve them here so
|
|
// `resolveParamType` (which would diagnose a missing annotation)
|
|
// is only called for params that carry one.
|
|
if (p.type_expr.data == .inferred_type) {
|
|
if (target_closure_params != null and pi < target_closure_params.?.len) {
|
|
break :blk target_closure_params.?[pi];
|
|
}
|
|
if (self.diagnostics) |d| {
|
|
d.addFmt(.err, p.type_expr.span, "cannot infer type of lambda parameter '{s}'; annotate it or use the lambda where a closure type is expected", .{p.name});
|
|
}
|
|
break :blk .unresolved;
|
|
}
|
|
break :blk self.resolveParamType(&p);
|
|
};
|
|
params.append(self.alloc, .{
|
|
.name = self.module.types.internString(p.name),
|
|
.ty = pty,
|
|
}) catch unreachable;
|
|
}
|
|
|
|
const ret_ty = blk: {
|
|
if (lam.return_type) |rt| {
|
|
break :blk type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
|
}
|
|
// Use target closure return type if available — but only when it's
|
|
// a resolved type. An `.unresolved` ret comes from an unbound
|
|
// generic (`Closure(..) -> $R`); fall through to infer it from the
|
|
// body so the concrete return drives `$R` inference at the call site.
|
|
if (self.target_type) |tt| {
|
|
if (!tt.isBuiltin()) {
|
|
const tti = self.module.types.get(tt);
|
|
if (tti == .closure and tti.closure.ret != .unresolved) break :blk tti.closure.ret;
|
|
// Unwrap ?Closure(...) → Closure(...)
|
|
if (tti == .optional) {
|
|
const inner = tti.optional.child;
|
|
if (!inner.isBuiltin()) {
|
|
const inner_info = self.module.types.get(inner);
|
|
if (inner_info == .closure and inner_info.closure.ret != .unresolved) break :blk inner_info.closure.ret;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Arrow lambda without explicit return type — infer from body expression
|
|
// Temporarily bind params in scope so inferExprType can resolve param types
|
|
var temp_scope = Scope.init(self.alloc, self.scope);
|
|
const saved = self.scope;
|
|
self.scope = &temp_scope;
|
|
for (lam.params, 0..) |p, i| {
|
|
const pty = params.items[user_param_base + i].ty;
|
|
temp_scope.put(p.name, .{ .ref = @enumFromInt(0), .ty = pty, .is_alloca = false });
|
|
}
|
|
const inferred = self.inferExprType(lam.body);
|
|
self.scope = saved;
|
|
temp_scope.deinit();
|
|
break :blk inferred;
|
|
};
|
|
const name_id = self.module.types.internString(name);
|
|
const func_id = self.builder.beginFunction(name_id, params.items, ret_ty);
|
|
if (lam.call_conv == .c) {
|
|
self.module.getFunctionMut(func_id).call_conv = .c;
|
|
}
|
|
self.builder.currentFunc().has_implicit_ctx = lambda_wants_ctx;
|
|
|
|
// Param-slot layout: ctx at 0 (if present), env at ctx_slots,
|
|
// user args at ctx_slots+1.
|
|
const lambda_ctx_slots: u32 = if (lambda_wants_ctx) 1 else 0;
|
|
const env_param_idx: u32 = lambda_ctx_slots;
|
|
const user_param_base_lam: u32 = lambda_ctx_slots + 1;
|
|
|
|
// Save + rebind current_ctx_ref so the body's sx-to-sx calls
|
|
// forward the trampoline's own ctx (slot 0).
|
|
const saved_ctx_ref_lam = self.current_ctx_ref;
|
|
defer self.current_ctx_ref = saved_ctx_ref_lam;
|
|
if (lambda_wants_ctx) self.current_ctx_ref = Ref.fromIndex(0);
|
|
|
|
// A lambda is its own function: its `return` must drain only ITS OWN
|
|
// `defer`s, not the enclosing function's. Open a fresh defer window
|
|
// (like `lowerFunction`/`monomorphizeFunction`) and restore on exit —
|
|
// otherwise lowering a closure literal inside a `defer` body re-enters
|
|
// the enclosing function's defer drain (infinite recursion — issue 0073).
|
|
const saved_func_defer_base = self.func_defer_base;
|
|
const saved_defer_len = self.defer_stack.items.len;
|
|
defer {
|
|
self.func_defer_base = saved_func_defer_base;
|
|
self.defer_stack.shrinkRetainingCapacity(saved_defer_len);
|
|
}
|
|
self.func_defer_base = saved_defer_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 WITHOUT parent — captures are bound from env, not parent scope
|
|
var lambda_scope = Scope.init(self.alloc, null);
|
|
self.scope = &lambda_scope;
|
|
|
|
// Bind captures from env struct (at env_param_idx)
|
|
if (capture_list.len > 0) {
|
|
const env_param_ref = Ref.fromIndex(env_param_idx);
|
|
// Alloca env struct locally so struct_gep can resolve the type
|
|
const env_local = self.builder.alloca(env_struct_ty);
|
|
// Compute env size
|
|
const env_byte_size_inner = self.computeEnvSize(capture_list);
|
|
const env_size_val = self.builder.constInt(@intCast(env_byte_size_inner), .s64);
|
|
// memcpy(local_alloca, env_param, size)
|
|
_ = self.callForeign("memcpy", &.{ env_local, env_param_ref, env_size_val }, self.module.types.ptrTo(.void));
|
|
|
|
for (capture_list, 0..) |cap, i| {
|
|
// GEP into env struct to get field pointer
|
|
const field_ptr = self.builder.structGepTyped(env_local, @intCast(i), self.module.types.ptrTo(cap.ty), env_struct_ty);
|
|
// Load the captured value into a local alloca
|
|
const loaded = self.builder.load(field_ptr, cap.ty);
|
|
const slot = self.builder.alloca(cap.ty);
|
|
self.builder.store(slot, loaded);
|
|
lambda_scope.put(cap.name, .{ .ref = slot, .ty = cap.ty, .is_alloca = true });
|
|
}
|
|
}
|
|
|
|
// Also need parent scope for function lookups (but not variable lookups)
|
|
// Set up fn_names from parent scope chain
|
|
{
|
|
var s: ?*Scope = saved_scope;
|
|
while (s) |scope| {
|
|
var it = scope.fn_names.iterator();
|
|
while (it.next()) |e| {
|
|
if (!lambda_scope.fn_names.contains(e.key_ptr.*)) {
|
|
lambda_scope.fn_names.put(e.key_ptr.*, e.value_ptr.*) catch {};
|
|
}
|
|
}
|
|
s = scope.parent;
|
|
}
|
|
}
|
|
|
|
// Bind params (user args start at user_param_base_lam, shifted past ctx + env).
|
|
// Use the signature types computed above (`params`), which already
|
|
// applied contextual typing from the target closure to untyped params —
|
|
// `resolveParamType` alone would drop it and default each to s64.
|
|
for (lam.params, 0..) |p, i| {
|
|
const pty = params.items[user_param_base + i].ty;
|
|
const slot = self.builder.alloca(pty);
|
|
const param_ref = Ref.fromIndex(user_param_base_lam + @as(u32, @intCast(i)));
|
|
self.builder.store(slot, param_ref);
|
|
lambda_scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true });
|
|
}
|
|
|
|
// Lower body — capture last expression as return value. The
|
|
// `in_lambda_body` flag scopes the lambda-specific `raise`-not-failable
|
|
// hint; save/restore so a lambda nested inside a regular function (or a
|
|
// lambda inside a lambda) restores the enclosing context.
|
|
const saved_in_lambda = self.in_lambda_body;
|
|
self.in_lambda_body = true;
|
|
if (ret_ty != .void) {
|
|
if (self.lowerBlockValue(lam.body)) |val| {
|
|
if (!self.currentBlockHasTerminator()) {
|
|
const val_ty = self.builder.getRefType(val);
|
|
// A value-carrying failable arrow lambda (`-> (T, !) => expr`)
|
|
// yields the bare success value; the compiler appends the
|
|
// no-error slot (0) — same as a `return v` in a block body.
|
|
if (!ret_ty.isBuiltin() and self.module.types.get(ret_ty) == .tuple and self.errorChannelOf(ret_ty) != null) {
|
|
self.lowerFailableSuccessReturn(val, ret_ty, lam.body.span);
|
|
} else {
|
|
const coerced = if (val_ty != .void) self.coerceToType(val, val_ty, ret_ty) else val;
|
|
self.builder.ret(coerced, ret_ty);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
self.lowerBlock(lam.body);
|
|
}
|
|
self.in_lambda_body = saved_in_lambda;
|
|
self.ensureTerminator(ret_ty);
|
|
self.builder.finalize();
|
|
|
|
// Restore builder state
|
|
self.scope = saved_scope;
|
|
lambda_scope.deinit();
|
|
self.builder.func = saved_func;
|
|
self.builder.current_block = saved_block;
|
|
self.builder.inst_counter = saved_counter;
|
|
// Restore the caller's `current_ctx_ref` BEFORE we emit the env
|
|
// alloc/memcpy below — those run in the caller's scope, and
|
|
// `allocViaContext` reads `current_ctx_ref` to find the
|
|
// installed allocator. Without this, the env_heap dispatch
|
|
// would still see `Ref.fromIndex(0)` (the lambda's own ctx
|
|
// param), which doesn't exist in the caller's frame and
|
|
// silently routes through the default context instead of any
|
|
// surrounding `push Context.{ allocator = ... }`.
|
|
self.current_ctx_ref = saved_ctx_ref_lam;
|
|
|
|
// Closure flowing into a BARE function-pointer slot (`(T) -> U`, no env):
|
|
// the slot is called without the closure env arg, so the closure fn can't
|
|
// be passed directly. For a capture-free closure whose return type matches
|
|
// the slot, emit an adapter with the bare ABI. Reject the cases the bare
|
|
// ABI can't represent: a capturing closure (env has nowhere to live), and
|
|
// a failable closure into a non-failable slot (foreign code can't observe
|
|
// the error channel — ERR E5.1 FFI-boundary rule).
|
|
if (self.target_type) |tt| {
|
|
if (!tt.isBuiltin() and self.module.types.get(tt) == .function) {
|
|
const slot_ret = self.module.types.get(tt).function.ret;
|
|
const widen_ok = self.errorChannelOf(slot_ret) != null and self.errorChannelOf(ret_ty) == null and self.failableSuccessType(slot_ret) == ret_ty;
|
|
if (capture_list.len > 0) {
|
|
if (self.diagnostics) |d| d.addFmt(.err, lam.body.span, "a capturing closure cannot be passed as a bare function pointer; declare the parameter type as `Closure(...)` so its environment is carried", .{});
|
|
} else if (ret_ty == slot_ret or widen_ok) {
|
|
// Matching ABI, or a non-failable closure widening into a
|
|
// failable slot (∅ ⊆ slot set) — the adapter wraps {value, 0}.
|
|
const adapter = self.createClosureToBareFnAdapter(func_id, self.module.types.get(tt).function, ret_ty, lam.body.span);
|
|
return self.builder.emit(.{ .func_ref = adapter }, tt);
|
|
} else if (self.errorChannelOf(ret_ty) != null and self.errorChannelOf(slot_ret) == null) {
|
|
if (self.diagnostics) |d| d.addFmt(.err, lam.body.span, "failable closure cannot be assigned to a non-failable function-type slot; foreign code can't observe the error channel — handle the error in a wrapper closure that absorbs it", .{});
|
|
} else if (self.diagnostics) |d| {
|
|
d.addFmt(.err, lam.body.span, "closure return type does not match the function-type slot", .{});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create proper closure type (user-visible params only — skip ctx + env).
|
|
const skip_count: usize = if (lambda_wants_ctx) 2 else 1;
|
|
var param_types_list = std.ArrayList(TypeId).empty;
|
|
for (params.items[skip_count..]) |p| {
|
|
param_types_list.append(self.alloc, p.ty) catch unreachable;
|
|
}
|
|
const closure_ty = self.module.types.closureType(param_types_list.items, ret_ty);
|
|
|
|
// Build env and closure in the caller's scope
|
|
if (capture_list.len > 0) {
|
|
// Alloca env struct on stack (so struct_gep can resolve the type)
|
|
const env_local = self.builder.alloca(env_struct_ty);
|
|
|
|
// Store captured values into env struct fields
|
|
for (capture_list, 0..) |cap, i| {
|
|
const gep = self.builder.structGepTyped(env_local, @intCast(i), self.module.types.ptrTo(cap.ty), env_struct_ty);
|
|
const val = if (cap.is_alloca)
|
|
self.builder.load(cap.ref, cap.ty)
|
|
else
|
|
cap.ref;
|
|
self.builder.store(gep, val);
|
|
}
|
|
|
|
// Copy env to heap (so it outlives the stack frame).
|
|
// Route through `context.allocator.alloc` rather than calling
|
|
// libc malloc directly so closures respect a surrounding
|
|
// `push Context.{ allocator = ... }` and a tracker / arena
|
|
// counts the env allocation alongside everything else.
|
|
const env_byte_size = self.computeEnvSize(capture_list);
|
|
const env_size = self.builder.constInt(@intCast(env_byte_size), .s64);
|
|
const ptr_void = self.module.types.ptrTo(.void);
|
|
const env_heap = self.allocViaContext(env_size, ptr_void);
|
|
// memcpy(heap, stack_alloca, size)
|
|
_ = self.callForeign("memcpy", &.{ env_heap, env_local, env_size }, ptr_void);
|
|
|
|
return self.builder.closureCreate(func_id, env_heap, closure_ty);
|
|
} else {
|
|
return self.builder.closureCreate(func_id, Ref.none, closure_ty);
|
|
}
|
|
}
|
|
|
|
/// Create a trampoline function that wraps a bare function for closure auto-promotion.
|
|
/// The trampoline has signature `(env: *void, args...) -> ret` and simply calls the
|
|
/// bare function with `(args...)`, ignoring the env parameter.
|
|
fn createBareFnTrampoline(self: *Lowering, bare_func_id: FuncId, closure_info: types.TypeInfo.ClosureInfo) FuncId {
|
|
// Build trampoline params: [__sx_ctx]? + env + closure params.
|
|
// When the program uses Context, every sx-side trampoline carries
|
|
// the implicit ctx at slot 0 and forwards it to the wrapped
|
|
// function (which is also sx-side and expects it at slot 0).
|
|
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
|
defer params.deinit(self.alloc);
|
|
const void_ptr_ty = self.module.types.ptrTo(.void);
|
|
const wants_ctx = self.implicit_ctx_enabled;
|
|
if (wants_ctx) {
|
|
params.append(self.alloc, .{ .name = self.module.types.internString("__sx_ctx"), .ty = void_ptr_ty }) catch unreachable;
|
|
}
|
|
const env_name = self.module.types.internString("env");
|
|
params.append(self.alloc, .{ .name = env_name, .ty = void_ptr_ty }) catch unreachable;
|
|
for (closure_info.params, 0..) |pty, i| {
|
|
var buf: [32]u8 = undefined;
|
|
const pname = std.fmt.bufPrint(&buf, "a{d}", .{i}) catch "arg";
|
|
params.append(self.alloc, .{ .name = self.module.types.internString(pname), .ty = pty }) catch unreachable;
|
|
}
|
|
|
|
// Generate unique trampoline name
|
|
const bare_func = self.module.functions.items[bare_func_id.index()];
|
|
const bare_name = self.module.types.getString(bare_func.name);
|
|
var name_buf: [128]u8 = undefined;
|
|
const tramp_name = std.fmt.bufPrint(&name_buf, "__tramp_{s}", .{bare_name}) catch "__tramp";
|
|
const tramp_name_id = self.module.types.internString(tramp_name);
|
|
|
|
// Save builder state
|
|
const saved_func = self.builder.func;
|
|
const saved_block = self.builder.current_block;
|
|
const saved_counter = self.builder.inst_counter;
|
|
|
|
// Create function
|
|
const owned_params = self.alloc.dupe(inst_mod.Function.Param, params.items) catch unreachable;
|
|
var func = inst_mod.Function.init(tramp_name_id, owned_params, closure_info.ret);
|
|
func.has_implicit_ctx = wants_ctx;
|
|
const func_id = self.module.addFunction(func);
|
|
self.builder.func = func_id;
|
|
self.builder.inst_counter = @intCast(owned_params.len); // params occupy refs 0..N-1
|
|
const entry_name = self.module.types.internString("entry");
|
|
const entry_block = self.builder.appendBlock(entry_name, &.{});
|
|
self.builder.switchToBlock(entry_block);
|
|
|
|
// Build call args: forward [__sx_ctx]? + user_params (skip env).
|
|
// Trampoline slots: 0=ctx (if present), {0|1}=env, then user args.
|
|
const ctx_slots: usize = if (wants_ctx) 1 else 0;
|
|
const user_arg_start: u32 = @intCast(ctx_slots + 1); // skip ctx + env
|
|
var call_args = std.ArrayList(Ref).empty;
|
|
defer call_args.deinit(self.alloc);
|
|
if (wants_ctx and bare_func.has_implicit_ctx) {
|
|
call_args.append(self.alloc, Ref.fromIndex(0)) catch unreachable; // forward our ctx
|
|
}
|
|
for (closure_info.params, 0..) |_, i| {
|
|
call_args.append(self.alloc, Ref.fromIndex(user_arg_start + @as(u32, @intCast(i)))) catch unreachable;
|
|
}
|
|
const owned_args = self.alloc.dupe(Ref, call_args.items) catch unreachable;
|
|
const result = self.builder.emit(.{ .call = .{ .callee = bare_func_id, .args = owned_args } }, closure_info.ret);
|
|
|
|
// Return result (or void)
|
|
if (closure_info.ret != .void) {
|
|
self.builder.ret(result, closure_info.ret);
|
|
} else {
|
|
self.builder.retVoid();
|
|
}
|
|
self.builder.finalize();
|
|
|
|
// Restore builder state
|
|
self.builder.func = saved_func;
|
|
self.builder.current_block = saved_block;
|
|
self.builder.inst_counter = saved_counter;
|
|
|
|
return func_id;
|
|
}
|
|
|
|
/// Adapter for coercing a closure into a BARE function-pointer slot
|
|
/// (`(T) -> U`, no env). The closure's underlying function has signature
|
|
/// `[ctx?] + env + user-params`, but a bare fn-ptr slot is *called* without
|
|
/// the env arg — so the closure fn can't be used directly (the env slot
|
|
/// would swallow the first user arg). This adapter carries the bare ABI
|
|
/// (`[ctx?] + user-params`) and forwards to the closure fn with a null env.
|
|
/// Only sound for capture-free closures (a null env is correct iff the body
|
|
/// reads no captures); the caller rejects capturing closures.
|
|
///
|
|
/// When `closure_ret` differs from `fn_info.ret`, this is the ∅-widening
|
|
/// case (a non-failable closure into a failable slot): the closure returns
|
|
/// the success value and the adapter wraps it into the slot's `{value, 0}`
|
|
/// failable tuple (ERR E5.1 non-failable→failable widening).
|
|
fn createClosureToBareFnAdapter(self: *Lowering, closure_func_id: FuncId, fn_info: types.TypeInfo.FunctionInfo, closure_ret: TypeId, span: ast.Span) FuncId {
|
|
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
|
defer params.deinit(self.alloc);
|
|
const void_ptr_ty = self.module.types.ptrTo(.void);
|
|
const wants_ctx = self.implicit_ctx_enabled;
|
|
if (wants_ctx) {
|
|
params.append(self.alloc, .{ .name = self.module.types.internString("__sx_ctx"), .ty = void_ptr_ty }) catch unreachable;
|
|
}
|
|
for (fn_info.params, 0..) |pty, i| {
|
|
var buf: [32]u8 = undefined;
|
|
const pname = std.fmt.bufPrint(&buf, "a{d}", .{i}) catch "arg";
|
|
params.append(self.alloc, .{ .name = self.module.types.internString(pname), .ty = pty }) catch unreachable;
|
|
}
|
|
|
|
const closure_func = self.module.functions.items[closure_func_id.index()];
|
|
const closure_name = self.module.types.getString(closure_func.name);
|
|
var name_buf: [128]u8 = undefined;
|
|
const adapter_name = std.fmt.bufPrint(&name_buf, "__cl2fn_{s}", .{closure_name}) catch "__cl2fn";
|
|
const adapter_name_id = self.module.types.internString(adapter_name);
|
|
|
|
const saved_func = self.builder.func;
|
|
const saved_block = self.builder.current_block;
|
|
const saved_counter = self.builder.inst_counter;
|
|
|
|
const owned_params = self.alloc.dupe(inst_mod.Function.Param, params.items) catch unreachable;
|
|
var func = inst_mod.Function.init(adapter_name_id, owned_params, fn_info.ret);
|
|
func.has_implicit_ctx = wants_ctx;
|
|
const func_id = self.module.addFunction(func);
|
|
self.builder.func = func_id;
|
|
self.builder.inst_counter = @intCast(owned_params.len);
|
|
const entry_name = self.module.types.internString("entry");
|
|
const entry_block = self.builder.appendBlock(entry_name, &.{});
|
|
self.builder.switchToBlock(entry_block);
|
|
|
|
// Forward [ctx?] + null env + user params to the closure fn.
|
|
const ctx_slots: usize = if (wants_ctx) 1 else 0;
|
|
var call_args = std.ArrayList(Ref).empty;
|
|
defer call_args.deinit(self.alloc);
|
|
if (wants_ctx) call_args.append(self.alloc, Ref.fromIndex(0)) catch unreachable;
|
|
call_args.append(self.alloc, self.builder.constNull(void_ptr_ty)) catch unreachable;
|
|
for (fn_info.params, 0..) |_, i| {
|
|
call_args.append(self.alloc, Ref.fromIndex(@intCast(ctx_slots + i))) catch unreachable;
|
|
}
|
|
const owned_args = self.alloc.dupe(Ref, call_args.items) catch unreachable;
|
|
const result = self.builder.emit(.{ .call = .{ .callee = closure_func_id, .args = owned_args } }, closure_ret);
|
|
if (closure_ret == fn_info.ret) {
|
|
if (fn_info.ret != .void) {
|
|
self.builder.ret(result, fn_info.ret);
|
|
} else {
|
|
self.builder.retVoid();
|
|
}
|
|
} else {
|
|
// ∅-widening: closure returns the success value; wrap `{value, 0}`
|
|
// into the slot's failable tuple.
|
|
self.lowerFailableSuccessReturn(result, fn_info.ret, span);
|
|
}
|
|
self.builder.finalize();
|
|
|
|
self.builder.func = saved_func;
|
|
self.builder.current_block = saved_block;
|
|
self.builder.inst_counter = saved_counter;
|
|
return func_id;
|
|
}
|
|
|
|
/// Walk an AST node and collect free variable references (identifiers that are
|
|
/// in the current scope but not in lambda params).
|
|
fn collectCaptures(self: *Lowering, node: *const Node, param_names: *std.StringHashMap(void), captures: *std.ArrayList(CaptureInfo)) void {
|
|
switch (node.data) {
|
|
.identifier => |id| {
|
|
// Skip lambda params
|
|
if (param_names.contains(id.name)) return;
|
|
// Skip function names
|
|
if (self.program_index.fn_ast_map.contains(id.name)) return;
|
|
// Skip type names
|
|
if (self.program_index.struct_template_map.contains(id.name)) return;
|
|
// Check if it's a variable in the parent scope
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(id.name)) |binding| {
|
|
captures.append(self.alloc, .{
|
|
.name = id.name,
|
|
.ty = binding.ty,
|
|
.ref = binding.ref,
|
|
.is_alloca = binding.is_alloca,
|
|
}) catch {};
|
|
}
|
|
}
|
|
},
|
|
.binary_op => |bo| {
|
|
self.collectCaptures(bo.lhs, param_names, captures);
|
|
self.collectCaptures(bo.rhs, param_names, captures);
|
|
},
|
|
.unary_op => |uo| {
|
|
self.collectCaptures(uo.operand, param_names, captures);
|
|
},
|
|
.call => |cl| {
|
|
self.collectCaptures(cl.callee, param_names, captures);
|
|
for (cl.args) |arg| {
|
|
self.collectCaptures(arg, param_names, captures);
|
|
}
|
|
},
|
|
.block => |blk| {
|
|
for (blk.stmts) |stmt| {
|
|
self.collectCaptures(stmt, param_names, captures);
|
|
}
|
|
},
|
|
.if_expr => |ie| {
|
|
self.collectCaptures(ie.condition, param_names, captures);
|
|
self.collectCaptures(ie.then_branch, param_names, captures);
|
|
if (ie.else_branch) |eb| self.collectCaptures(eb, param_names, captures);
|
|
},
|
|
.while_expr => |we| {
|
|
self.collectCaptures(we.condition, param_names, captures);
|
|
self.collectCaptures(we.body, param_names, captures);
|
|
},
|
|
.return_stmt => |rs| {
|
|
if (rs.value) |v| self.collectCaptures(v, param_names, captures);
|
|
},
|
|
.var_decl => |vd| {
|
|
if (vd.value) |v| self.collectCaptures(v, param_names, captures);
|
|
// Register the local var name so it's not captured
|
|
param_names.put(vd.name, {}) catch {};
|
|
},
|
|
.const_decl => |cd| {
|
|
self.collectCaptures(cd.value, param_names, captures);
|
|
param_names.put(cd.name, {}) catch {};
|
|
},
|
|
.assignment => |a| {
|
|
self.collectCaptures(a.target, param_names, captures);
|
|
self.collectCaptures(a.value, param_names, captures);
|
|
},
|
|
.destructure_decl => |dd| {
|
|
self.collectCaptures(dd.value, param_names, captures);
|
|
for (dd.names) |name| {
|
|
param_names.put(name, {}) catch {};
|
|
}
|
|
},
|
|
.field_access => |fa| {
|
|
self.collectCaptures(fa.object, param_names, captures);
|
|
},
|
|
.index_expr => |ie| {
|
|
self.collectCaptures(ie.object, param_names, captures);
|
|
self.collectCaptures(ie.index, param_names, captures);
|
|
},
|
|
.struct_literal => |sl| {
|
|
for (sl.field_inits) |fi| {
|
|
self.collectCaptures(fi.value, param_names, captures);
|
|
}
|
|
},
|
|
.array_literal => |al| {
|
|
for (al.elements) |elem| {
|
|
self.collectCaptures(elem, param_names, captures);
|
|
}
|
|
},
|
|
.lambda => |inner_lam| {
|
|
// For nested lambdas, the inner lambda captures from our scope too
|
|
// But its own params should be excluded
|
|
var inner_params = std.StringHashMap(void).init(self.alloc);
|
|
defer inner_params.deinit();
|
|
// Copy current param_names
|
|
var it = param_names.iterator();
|
|
while (it.next()) |e| {
|
|
inner_params.put(e.key_ptr.*, {}) catch {};
|
|
}
|
|
for (inner_lam.params) |p| {
|
|
inner_params.put(p.name, {}) catch {};
|
|
}
|
|
self.collectCaptures(inner_lam.body, &inner_params, captures);
|
|
},
|
|
.match_expr => |me| {
|
|
self.collectCaptures(me.subject, param_names, captures);
|
|
for (me.arms) |arm| {
|
|
self.collectCaptures(arm.body, param_names, captures);
|
|
}
|
|
},
|
|
.null_coalesce => |nc| {
|
|
self.collectCaptures(nc.lhs, param_names, captures);
|
|
self.collectCaptures(nc.rhs, param_names, captures);
|
|
},
|
|
.deref_expr => |de| {
|
|
self.collectCaptures(de.operand, param_names, captures);
|
|
},
|
|
.for_expr => |fe| {
|
|
self.collectCaptures(fe.iterable, param_names, captures);
|
|
// Register capture name as local so it's not captured
|
|
param_names.put(fe.capture_name, {}) catch {};
|
|
self.collectCaptures(fe.body, param_names, captures);
|
|
},
|
|
.slice_expr => |se| {
|
|
self.collectCaptures(se.object, param_names, captures);
|
|
if (se.start) |s| self.collectCaptures(s, param_names, captures);
|
|
if (se.end) |e| self.collectCaptures(e, param_names, captures);
|
|
},
|
|
.tuple_literal => |tl| {
|
|
for (tl.elements) |elem| {
|
|
self.collectCaptures(elem.value, param_names, captures);
|
|
}
|
|
},
|
|
.force_unwrap => |fu| {
|
|
self.collectCaptures(fu.operand, param_names, captures);
|
|
},
|
|
.chained_comparison => |cc| {
|
|
for (cc.operands) |op| {
|
|
self.collectCaptures(op, param_names, captures);
|
|
}
|
|
},
|
|
.defer_stmt => |ds| {
|
|
self.collectCaptures(ds.expr, param_names, captures);
|
|
},
|
|
.ffi_intrinsic_call => |fic| {
|
|
self.collectCaptures(fic.return_type, param_names, captures);
|
|
for (fic.args) |arg| {
|
|
self.collectCaptures(arg, param_names, captures);
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
/// Compute the byte size of the env struct based on captured value types.
|
|
fn computeEnvSize(self: *Lowering, capture_list: []const CaptureInfo) usize {
|
|
// Must match LLVM's struct layout: fields are aligned to their natural alignment
|
|
var offset: usize = 0;
|
|
var max_align: usize = 1;
|
|
for (capture_list) |cap| {
|
|
const field_size = self.typeSizeBytes(cap.ty);
|
|
const field_align = self.typeAlignBytes(cap.ty);
|
|
if (field_align > max_align) max_align = field_align;
|
|
// Align offset to field alignment
|
|
offset = (offset + field_align - 1) & ~(field_align - 1);
|
|
offset += field_size;
|
|
}
|
|
// Align total to max field alignment (matches LLVM's struct alignment)
|
|
return (offset + max_align - 1) & ~(max_align - 1);
|
|
}
|
|
|
|
/// Byte size of an IR type matching LLVM's type layout.
|
|
fn typeSizeBytes(self: *Lowering, ty: TypeId) usize {
|
|
return self.module.types.typeSizeBytes(ty);
|
|
}
|
|
|
|
fn typeAlignBytes(self: *Lowering, ty: TypeId) usize {
|
|
return self.module.types.typeAlignBytes(ty);
|
|
}
|
|
|
|
fn resolveReturnType2(self: *Lowering, rt: ?*const Node) TypeId {
|
|
if (rt) |r| return type_bridge.resolveAstType(r, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
|
return .void;
|
|
}
|
|
|
|
// ── Chained comparison ──────────────────────────────────────────
|
|
|
|
fn lowerChainedComparison(self: *Lowering, cc: *const ast.ChainedComparison) Ref {
|
|
// a < b < c → (a < b) and (b < c)
|
|
// Pre-lower all operands so shared ones (e.g., b) aren't evaluated twice.
|
|
if (cc.operands.len < 2 or cc.ops.len == 0) {
|
|
return self.builder.constBool(true);
|
|
}
|
|
|
|
var refs = std.ArrayList(Ref).empty;
|
|
defer refs.deinit(self.alloc);
|
|
for (cc.operands) |op| {
|
|
refs.append(self.alloc, self.lowerExpr(op)) catch unreachable;
|
|
}
|
|
|
|
var result = self.emitCmp(refs.items[0], refs.items[1], cc.ops[0]);
|
|
|
|
var i: usize = 1;
|
|
while (i < cc.ops.len) : (i += 1) {
|
|
const next_cmp = self.emitCmp(refs.items[i], refs.items[i + 1], cc.ops[i]);
|
|
result = self.builder.emit(.{ .bool_and = .{ .lhs = result, .rhs = next_cmp } }, .bool);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
fn emitCmp(self: *Lowering, lhs: Ref, rhs: Ref, op: ast.BinaryOp.Op) Ref {
|
|
return switch (op) {
|
|
.eq => self.builder.cmpEq(lhs, rhs),
|
|
.neq => self.builder.emit(.{ .cmp_ne = .{ .lhs = lhs, .rhs = rhs } }, .bool),
|
|
.lt => self.builder.cmpLt(lhs, rhs),
|
|
.lte => self.builder.emit(.{ .cmp_le = .{ .lhs = lhs, .rhs = rhs } }, .bool),
|
|
.gt => self.builder.cmpGt(lhs, rhs),
|
|
.gte => self.builder.emit(.{ .cmp_ge = .{ .lhs = lhs, .rhs = rhs } }, .bool),
|
|
else => self.builder.constBool(false),
|
|
};
|
|
}
|
|
|
|
// ── Defer/Push/MultiAssign ──────────────────────────────────────
|
|
|
|
fn lowerDefer(self: *Lowering, ds: *const ast.DeferStmt) void {
|
|
// Push deferred expression onto the stack — emitted at every block exit, LIFO.
|
|
self.defer_stack.append(self.alloc, .{ .body = ds.expr, .is_onfail = false }) catch {};
|
|
}
|
|
|
|
/// `onfail [e] BODY` (ERR E1.7) — cleanup that runs only when an error
|
|
/// leaves the enclosing block. Recorded on the shared cleanup stack;
|
|
/// emitted (interleaved with defers, reverse) at error exits by
|
|
/// `emitErrorCleanup`, and discarded — never run — on a success exit.
|
|
fn lowerOnFail(self: *Lowering, ofs: *const ast.OnFailStmt, span: ast.Span) void {
|
|
// `onfail` is only meaningful inside a failable function — a
|
|
// non-failable function never error-exits, so it could never fire.
|
|
const ret_ty = self.effectiveReturnType() orelse {
|
|
self.diagOnFailNotFailable(span);
|
|
return;
|
|
};
|
|
if (self.errorChannelOf(ret_ty) == null) {
|
|
self.diagOnFailNotFailable(span);
|
|
return;
|
|
}
|
|
self.defer_stack.append(self.alloc, .{ .body = ofs.body, .is_onfail = true, .binding = ofs.binding }) catch {};
|
|
}
|
|
|
|
fn diagOnFailNotFailable(self: *Lowering, span: ast.Span) void {
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, span, "`onfail` is only valid inside a failable function (a return type with `!` or `!Named`) — use `defer` for unconditional cleanup", .{});
|
|
}
|
|
}
|
|
|
|
/// Emit cleanups from saved_len..current in reverse (LIFO) order on a
|
|
/// SUCCESS exit: only `defer` entries run; `onfail` entries are skipped
|
|
/// (and discarded by the truncation). Truncates the stack to saved_len.
|
|
fn emitBlockDefers(self: *Lowering, saved_len: usize) void {
|
|
// Guard: if stack was already drained (e.g., by a return that emitted all defers)
|
|
if (saved_len > self.defer_stack.items.len) return;
|
|
if (self.currentBlockHasTerminator()) {
|
|
// Block already terminated (e.g., by return) — cleanups were already emitted
|
|
self.defer_stack.shrinkRetainingCapacity(saved_len);
|
|
return;
|
|
}
|
|
const stack = self.defer_stack.items;
|
|
var i = stack.len;
|
|
while (i > saved_len) {
|
|
i -= 1;
|
|
if (!stack[i].is_onfail) self.lowerCleanupBody(stack[i].body);
|
|
}
|
|
self.defer_stack.shrinkRetainingCapacity(saved_len);
|
|
}
|
|
|
|
/// Run a `defer`/`onfail` cleanup body for its side effects (void context).
|
|
/// A braced body lowers as statements (NOT as a value) so a trailing-`;`
|
|
/// last expression is fine here — cleanup bodies never yield a value.
|
|
fn lowerCleanupBody(self: *Lowering, body: *const Node) void {
|
|
if (body.data == .block) self.lowerBlock(body) else _ = self.lowerExpr(body);
|
|
}
|
|
|
|
/// Emit cleanups from `base`..current in reverse order on an ERROR exit
|
|
/// (raise / try-propagation): BOTH `defer` and `onfail` entries run,
|
|
/// interleaved in reverse declaration order. `err_tag` is the in-flight
|
|
/// error tag, bound to each `onfail e`'s binding. Does not truncate — the
|
|
/// terminating `ret` + the unwinding block-scope `emitBlockDefers` (which
|
|
/// then see the terminator and skip) leave the stack consistent.
|
|
fn emitErrorCleanup(self: *Lowering, base: usize, err_tag: Ref) void {
|
|
if (base > self.defer_stack.items.len) return;
|
|
const tag_ty = self.builder.getRefType(err_tag);
|
|
const stack = self.defer_stack.items;
|
|
var i = stack.len;
|
|
while (i > base) {
|
|
i -= 1;
|
|
const entry = stack[i];
|
|
if (entry.is_onfail) {
|
|
if (entry.binding) |name| {
|
|
var ofscope = Scope.init(self.alloc, self.scope);
|
|
const saved = self.scope;
|
|
self.scope = &ofscope;
|
|
ofscope.put(name, .{ .ref = err_tag, .ty = tag_ty, .is_alloca = false });
|
|
self.lowerCleanupBody(entry.body);
|
|
self.scope = saved;
|
|
ofscope.deinit();
|
|
} else {
|
|
self.lowerCleanupBody(entry.body);
|
|
}
|
|
} else {
|
|
self.lowerCleanupBody(entry.body);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn lowerPush(self: *Lowering, ps: *const ast.PushStmt) void {
|
|
// push Context.{...} { body } — allocates a fresh Context on the
|
|
// stack frame, rebinds the lowering's `current_ctx_ref` to it for
|
|
// the body's lexical scope, then restores. No global, no walk.
|
|
if (!self.implicit_ctx_enabled) {
|
|
_ = self.diagnoseMissingContext("`push Context.{...}`");
|
|
self.lowerBlock(ps.body);
|
|
return;
|
|
}
|
|
const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse {
|
|
_ = self.diagnoseMissingContext("`push Context.{...}`");
|
|
self.lowerBlock(ps.body);
|
|
return;
|
|
};
|
|
const saved_ctx_ref = self.current_ctx_ref;
|
|
defer self.current_ctx_ref = saved_ctx_ref;
|
|
|
|
const saved_target = self.target_type;
|
|
self.target_type = ctx_ty;
|
|
const ctx_val = self.lowerExpr(ps.context_expr);
|
|
self.target_type = saved_target;
|
|
|
|
const slot = self.builder.alloca(ctx_ty);
|
|
self.builder.store(slot, ctx_val);
|
|
self.current_ctx_ref = slot;
|
|
|
|
self.lowerBlock(ps.body);
|
|
}
|
|
|
|
fn lowerMultiAssign(self: *Lowering, ma: *const ast.MultiAssign) void {
|
|
// Evaluate all RHS values first, then assign to LHS targets
|
|
var vals = std.ArrayList(Ref).empty;
|
|
defer vals.deinit(self.alloc);
|
|
for (ma.values) |v| {
|
|
vals.append(self.alloc, self.lowerExpr(v)) catch unreachable;
|
|
}
|
|
|
|
for (ma.targets, 0..) |target, i| {
|
|
if (i >= vals.items.len) break;
|
|
const val = vals.items[i];
|
|
switch (target.data) {
|
|
.identifier => |id| {
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(id.name)) |binding| {
|
|
if (binding.is_alloca) {
|
|
const val_ty = self.builder.getRefType(val);
|
|
const store_val = if (val_ty != binding.ty and val_ty != .void and binding.ty != .void)
|
|
self.coerceToType(val, val_ty, binding.ty)
|
|
else
|
|
val;
|
|
self.builder.store(binding.ref, store_val);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
.index_expr => |ie| {
|
|
const idx = self.lowerExpr(ie.index);
|
|
const obj_ty = self.inferExprType(ie.object);
|
|
const elem_ty = self.getElementType(obj_ty);
|
|
const ptr_ty = self.module.types.ptrTo(elem_ty);
|
|
const val_ty = self.builder.getRefType(val);
|
|
const store_val = if (val_ty != elem_ty and val_ty != .void and elem_ty != .void)
|
|
self.coerceToType(val, val_ty, elem_ty)
|
|
else
|
|
val;
|
|
// For fixed-size arrays, use the alloca pointer directly
|
|
const is_array = !obj_ty.isBuiltin() and self.module.types.get(obj_ty) == .array;
|
|
const obj_alloca = if (is_array) self.getExprAlloca(ie.object) else null;
|
|
if (obj_alloca) |alloca_ref| {
|
|
const gep = self.builder.emit(.{ .index_gep = .{ .lhs = alloca_ref, .rhs = idx } }, ptr_ty);
|
|
self.builder.store(gep, store_val);
|
|
} else {
|
|
const obj = self.lowerExpr(ie.object);
|
|
const gep = self.builder.emit(.{ .index_gep = .{ .lhs = obj, .rhs = idx } }, ptr_ty);
|
|
self.builder.store(gep, store_val);
|
|
}
|
|
},
|
|
.field_access => |fa| {
|
|
const obj_ptr = self.lowerExprAsPtr(fa.object);
|
|
const obj_ty = self.inferExprType(fa.object);
|
|
// Resolve the target field via the shared lvalue resolver —
|
|
// the same one address-of uses — so a missing field emits a
|
|
// diagnostic instead of defaulting to field 0 / field_ty
|
|
// .unresolved, which silently corrupted a neighbouring field
|
|
// (or panicked at LLVM emission) (issue 0094).
|
|
if (self.fieldLvaluePtr(obj_ptr, obj_ty, fa.field)) |r| {
|
|
const val_ty = self.builder.getRefType(val);
|
|
const store_val = if (val_ty != r.ty and val_ty != .void and r.ty != .void)
|
|
self.coerceToType(val, val_ty, r.ty)
|
|
else
|
|
val;
|
|
self.builder.store(r.ptr, store_val);
|
|
} else {
|
|
_ = self.emitFieldError(obj_ty, fa.field, target.span);
|
|
}
|
|
},
|
|
.deref_expr => |de| {
|
|
const ptr = self.lowerExpr(de.operand);
|
|
const pointee_ty = blk: {
|
|
const ptr_ty = self.inferExprType(de.operand);
|
|
if (!ptr_ty.isBuiltin()) {
|
|
const info = self.module.types.get(ptr_ty);
|
|
if (info == .pointer) break :blk info.pointer.pointee;
|
|
}
|
|
break :blk ptr_ty;
|
|
};
|
|
const val_ty = self.builder.getRefType(val);
|
|
const store_val = if (val_ty != pointee_ty and val_ty != .void and pointee_ty != .void)
|
|
self.coerceToType(val, val_ty, pointee_ty)
|
|
else
|
|
val;
|
|
self.builder.store(ptr, store_val);
|
|
},
|
|
else => {
|
|
_ = self.emitError("multi_assign_target", target.span);
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn lowerDestructureDecl(self: *Lowering, dd: *const ast.DestructureDecl) void {
|
|
// Lower the RHS expression (must produce a tuple)
|
|
const saved_fbv = self.force_block_value;
|
|
self.force_block_value = true;
|
|
const ref = self.lowerExpr(dd.value);
|
|
self.force_block_value = saved_fbv;
|
|
const ty = self.builder.getRefType(ref);
|
|
|
|
// Get tuple field info
|
|
if (ty.isBuiltin()) return;
|
|
const ti = self.module.types.get(ty);
|
|
if (ti != .tuple) return;
|
|
const tuple = ti.tuple;
|
|
if (dd.names.len > tuple.fields.len) return;
|
|
|
|
// E1.8 (discard rejection): when the RHS is a value-carrying failable,
|
|
// the error slot (always the LAST tuple field) cannot be dropped. It is
|
|
// dropped when the destructure omits it (fewer names than fields, so the
|
|
// trailing error slot is never reached) or binds it to `_`. The `try` /
|
|
// `catch` / `or value` consumer forms all strip the error channel (their
|
|
// result type is non-failable), so this fires only on a BARE failable
|
|
// destructure — exactly the case that would let an error vanish silently.
|
|
if (self.errorChannelOf(ty) != null) {
|
|
const err_dropped = dd.names.len < tuple.fields.len or
|
|
std.mem.eql(u8, dd.names[dd.names.len - 1], "_");
|
|
if (err_dropped) {
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, dd.value.span, "the error slot of a failable cannot be dropped — bind it (`v, err := …`) and handle it, or use `try` / `catch`", .{});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extract each field and bind to a new variable
|
|
for (dd.names, 0..) |name, i| {
|
|
if (std.mem.eql(u8, name, "_")) continue; // discard
|
|
const field_ty = tuple.fields[i];
|
|
const field_val = self.builder.emit(.{ .tuple_get = .{
|
|
.base = ref,
|
|
.field_index = @intCast(i),
|
|
.base_type = ty,
|
|
} }, field_ty);
|
|
const slot = self.builder.alloca(field_ty);
|
|
self.builder.store(slot, field_val);
|
|
if (self.scope) |scope| {
|
|
scope.put(name, .{ .ref = slot, .ty = field_ty, .is_alloca = true });
|
|
}
|
|
}
|
|
|
|
// Destructuring a failable's result binds the error slot to a variable:
|
|
// the user now owns the error explicitly, so the trace is absorbed
|
|
// (ERR E3.2). A plain (non-failable) tuple destructure clears nothing.
|
|
if (self.errorChannelOf(ty) != null) self.emitTraceClear();
|
|
}
|
|
|
|
// ── Comptime lowering ────────────────────────────────────────────
|
|
|
|
/// Lower a `#run expr` that appears as a top-level constant binding:
|
|
/// NAME :: #run expr;
|
|
/// Creates a comptime function wrapping the expression (for later
|
|
/// interpretation), plus a global constant to hold the result.
|
|
fn lowerComptimeGlobal(self: *Lowering, name: []const u8, expr: *const Node, type_ann: ?*const Node) void {
|
|
// When the user writes `NAME :: #run expr;` with no type annotation,
|
|
// infer the global's type from the comptime expression's return
|
|
// shape. `resolveType(null)` returns `.s64` for legacy reasons —
|
|
// good for primitive helpers, silently wrong for anything else.
|
|
const expr_ty = self.inferExprType(expr);
|
|
// A failable `#run` (bare, no `catch`/`or`): the comptime function
|
|
// returns the full failable tuple so the #run site can inspect the
|
|
// error slot, but the GLOBAL is typed as the success value. On a
|
|
// comptime error the global never materializes — emit halts with a
|
|
// diagnostic + trace (E5.2). A handled `#run … catch/or …` already
|
|
// strips the error channel, so it lands here as non-failable.
|
|
const is_failable = self.errorChannelOf(expr_ty) != null;
|
|
const func_ret: TypeId = if (is_failable)
|
|
expr_ty
|
|
else if (type_ann) |n|
|
|
self.resolveTypeWithBindings(n)
|
|
else
|
|
expr_ty;
|
|
const global_ty: TypeId = if (is_failable) self.failableSuccessType(expr_ty) else func_ret;
|
|
const func_id = self.createComptimeFunction(name, expr, func_ret);
|
|
|
|
// Add a global constant whose initializer will be filled by the interpreter.
|
|
const name_id = self.module.types.internString(name);
|
|
const gid = self.module.addGlobal(.{
|
|
.name = name_id,
|
|
.ty = global_ty,
|
|
.init_val = null, // will be filled by interpreter at emit time
|
|
.is_const = true,
|
|
.comptime_func = func_id,
|
|
});
|
|
|
|
// Register for runtime lookup: identifier resolution emits global_get
|
|
self.putGlobal(self.current_source_file, name, .{ .id = gid, .ty = global_ty });
|
|
}
|
|
|
|
/// Lower a standalone `#run expr;` at the top level (side-effect only).
|
|
/// Creates a comptime function that the interpreter should execute.
|
|
fn lowerComptimeSideEffect(self: *Lowering, expr: *const Node) void {
|
|
// A failable side-effect `#run f();` returns the failable tuple so the
|
|
// emit-time runner can detect an escaping error and halt (E5.2);
|
|
// non-failable side effects stay `void`.
|
|
const expr_ty = self.inferExprType(expr);
|
|
const ret: TypeId = if (self.errorChannelOf(expr_ty) != null) expr_ty else .void;
|
|
_ = self.createComptimeFunction("__run", expr, ret);
|
|
}
|
|
|
|
/// Lower a `#run expr` that appears inline within an expression.
|
|
/// Creates a comptime function and emits a `call` to it, so the
|
|
/// interpreter can evaluate it and replace with the constant result.
|
|
fn lowerInlineComptime(self: *Lowering, expr: *const Node) Ref {
|
|
const ret_ty: TypeId = self.target_type orelse self.inferExprType(expr);
|
|
const func_id = self.createComptimeFunction("__ct", expr, ret_ty);
|
|
// Emit a call to the comptime function. At interpretation time,
|
|
// this will be evaluated and the result inlined as a constant.
|
|
const func = &self.module.functions.items[@intFromEnum(func_id)];
|
|
const final_args: []const Ref = if (func.has_implicit_ctx)
|
|
self.alloc.dupe(Ref, &.{self.current_ctx_ref}) catch &.{}
|
|
else
|
|
&.{};
|
|
return self.builder.call(func_id, final_args, ret_ty);
|
|
}
|
|
|
|
/// Lower a `#insert expr` statement. Evaluates `expr` at compile time to get
|
|
/// a string, parses it as sx code, and lowers each statement inline.
|
|
fn lowerInsertExpr(self: *Lowering, expr: *const Node) void {
|
|
_ = self.lowerInsertExprValue(expr);
|
|
}
|
|
|
|
/// Like lowerInsertExpr but returns the value of the last parsed expression.
|
|
fn lowerInsertExprValue(self: *Lowering, expr: *const Node) Ref {
|
|
// Step 1: Substitute comptime param nodes (e.g., replace $fmt with its literal)
|
|
const substituted = if (self.comptime_param_nodes) |cpn|
|
|
self.substituteComptimeNodes(expr, cpn) catch expr
|
|
else
|
|
expr;
|
|
|
|
// Step 2: Evaluate the expression to get a string
|
|
const code_str = self.evalComptimeString(substituted) orelse return self.builder.constInt(0, .void);
|
|
|
|
// Step 3: Parse the string as sx code and lower each statement
|
|
// The last expression's value is captured as the return value
|
|
var p = parser_mod.Parser.init(self.alloc, code_str);
|
|
var last_val: Ref = self.builder.constInt(0, .void);
|
|
while (p.current.tag != .eof) {
|
|
const stmt = p.parseStmt() catch break;
|
|
if (p.current.tag == .eof) {
|
|
// Last statement — try to capture as expression value
|
|
// Note: tryLowerAsExpr internally calls lowerStmt for statement nodes,
|
|
// so we must NOT call lowerStmt again in the else branch.
|
|
if (self.tryLowerAsExpr(stmt)) |val| {
|
|
last_val = val;
|
|
}
|
|
} else {
|
|
self.lowerStmt(stmt);
|
|
}
|
|
}
|
|
return last_val;
|
|
}
|
|
|
|
/// Evaluate an expression at compile time, returning its string value.
|
|
/// Returns null if evaluation fails.
|
|
fn evalComptimeString(self: *Lowering, expr: *const Node) ?[:0]const u8 {
|
|
// Case 1: String literal — return it directly (no need for interpreter)
|
|
if (expr.data == .string_literal) {
|
|
const lit = expr.data.string_literal;
|
|
const str = if (lit.is_raw)
|
|
lit.raw
|
|
else
|
|
unescape.unescapeString(self.alloc, lit.raw) catch lit.raw;
|
|
return self.alloc.dupeZ(u8, str) catch null;
|
|
}
|
|
|
|
// Case 2: Evaluate via IR interpreter, reusing the parent module.
|
|
// The parent's `scanDecls` pass has already registered every
|
|
// type / protocol / impl / thunk the comptime call may need
|
|
// (Allocator, CAllocator, Context, the per-impl thunks). A
|
|
// fresh empty module would only lazy-lower function ASTs and
|
|
// would miss the type/protocol registrations, which would break
|
|
// `context.allocator.X` — the protocol dispatch chain needs
|
|
// those types to resolve struct field layout and the alloc/
|
|
// dealloc thunks at the bottom of the dispatch.
|
|
const ct_func_id = self.createComptimeFunction("__insert", expr, .string);
|
|
|
|
var interp = interp_mod.Interpreter.init(self.module, self.alloc);
|
|
defer interp.deinit();
|
|
if (self.diagnostics) |d| if (d.import_sources) |sm| interp.setSourceMap(sm);
|
|
|
|
const result = interp.call(ct_func_id, &.{}) catch return null;
|
|
|
|
const str = result.asString(&interp) orelse switch (result) {
|
|
.string => |s| s,
|
|
else => return null,
|
|
};
|
|
|
|
return self.alloc.dupeZ(u8, str) catch null;
|
|
}
|
|
|
|
/// Lower the direct callee of a comptime expression into the ct module.
|
|
/// Transitive dependencies are resolved lazily via the shared fn_ast_map.
|
|
fn lowerComptimeDeps(self: *Lowering, ct: *Lowering, expr: *const Node) void {
|
|
if (expr.data != .call) return;
|
|
if (expr.data.call.callee.data != .identifier) return;
|
|
const name = expr.data.call.callee.data.identifier.name;
|
|
if (resolveBuiltin(name) != null) return;
|
|
if (self.program_index.fn_ast_map.get(name)) |fd| {
|
|
if (ct.resolveFuncByName(name) == null) {
|
|
ct.lowerFunction(fd, name, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Substitute comptime parameter identifiers with their actual AST nodes.
|
|
fn substituteComptimeNodes(self: *Lowering, node: *const Node, cpn: std.StringHashMap(*const Node)) !*const Node {
|
|
// Direct identifier match
|
|
if (node.data == .identifier) {
|
|
if (cpn.get(node.data.identifier.name)) |replacement| {
|
|
return replacement;
|
|
}
|
|
}
|
|
|
|
// Recurse into call arguments
|
|
if (node.data == .call) {
|
|
var changed = false;
|
|
const new_args = try self.alloc.alloc(*Node, node.data.call.args.len);
|
|
for (node.data.call.args, 0..) |arg, i| {
|
|
const substituted = try self.substituteComptimeNodes(arg, cpn);
|
|
new_args[i] = @constCast(substituted);
|
|
if (substituted != arg) changed = true;
|
|
}
|
|
if (changed) {
|
|
const new_node = try self.alloc.create(Node);
|
|
new_node.* = .{
|
|
.span = node.span,
|
|
.data = .{ .call = .{
|
|
.callee = node.data.call.callee,
|
|
.args = new_args,
|
|
} },
|
|
};
|
|
return new_node;
|
|
}
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
/// Lower a call to a function with comptime params by inlining its body.
|
|
/// Comptime params are substituted, `#insert` expressions are evaluated.
|
|
fn lowerComptimeCall(self: *Lowering, fd: *const ast.FnDecl, call_node: *const ast.Call) Ref {
|
|
// Build comptime param substitution map: param_name → call_site AST node
|
|
var cpn = std.StringHashMap(*const Node).init(self.alloc);
|
|
var call_arg_idx: usize = 0;
|
|
// Pack-arg-node registration (step 2 of the variadic heterogeneous
|
|
// type packs feature): when the fn declares a pack param, record
|
|
// the slice of call-site arg nodes under the pack name so the
|
|
// body's `args[$i]` lowering can substitute the i-th arg with
|
|
// its concrete-typed value instead of the `[]Any` slice load.
|
|
var pack_arg_name: ?[]const u8 = null;
|
|
var pack_arg_slice: []const *const Node = &.{};
|
|
|
|
for (fd.params) |param| {
|
|
if (param.is_variadic) {
|
|
// Variadic param: pack remaining call args into []Any slice
|
|
self.lowerVariadicArgs(param.name, call_node.args, call_arg_idx);
|
|
// Only heterogeneous pack form `..$args` (is_comptime AND
|
|
// is_variadic) registers for typed indexing. Plain
|
|
// `args: ..Any` keeps the existing []Any path so stdlib's
|
|
// `format`/`print` continue boxing through Any.
|
|
if (param.is_comptime and call_arg_idx <= call_node.args.len) {
|
|
pack_arg_name = param.name;
|
|
pack_arg_slice = call_node.args[call_arg_idx..];
|
|
// Stamp each pack arg with the caller's source so the
|
|
// body's typed `args[i]` substitution (via packArgNodeAt,
|
|
// lowered under the defining-module pin set below) resolves
|
|
// its bare names in the CALLER's visibility context — the
|
|
// same treatment the fixed comptime params get below.
|
|
// Without it a caller-owned helper passed to an imported
|
|
// metaprogram (`std.print("{}", caller_fn())`) resolves
|
|
// under the callee's module and is reported "not visible".
|
|
for (call_node.args[call_arg_idx..]) |pack_arg| {
|
|
self.stampCallerSource(pack_arg);
|
|
}
|
|
}
|
|
break; // variadic is always the last param
|
|
}
|
|
if (call_arg_idx >= call_node.args.len) break;
|
|
if (param.is_comptime) {
|
|
self.stampCallerSource(call_node.args[call_arg_idx]);
|
|
cpn.put(param.name, call_node.args[call_arg_idx]) catch {};
|
|
call_arg_idx += 1;
|
|
} else {
|
|
const arg_val = self.lowerExpr(call_node.args[call_arg_idx]);
|
|
const pty = self.resolveParamType(¶m);
|
|
const slot = self.builder.alloca(pty);
|
|
self.builder.store(slot, arg_val);
|
|
if (self.scope) |scope| {
|
|
scope.put(param.name, .{ .ref = slot, .ty = pty, .is_alloca = true });
|
|
}
|
|
call_arg_idx += 1;
|
|
}
|
|
}
|
|
|
|
// Also bind comptime params as local string variables (for `fmt` used in runtime code)
|
|
var cpn_iter = cpn.iterator();
|
|
while (cpn_iter.next()) |entry| {
|
|
const param_name = entry.key_ptr.*;
|
|
const param_node = entry.value_ptr.*;
|
|
if (param_node.data == .string_literal) {
|
|
// Create a local string variable with the literal value
|
|
const str_ref = self.lowerExpr(param_node);
|
|
const slot = self.builder.alloca(.string);
|
|
self.builder.store(slot, str_ref);
|
|
if (self.scope) |scope| {
|
|
scope.put(param_name, .{ .ref = slot, .ty = .string, .is_alloca = true });
|
|
}
|
|
}
|
|
}
|
|
|
|
// Install comptime param nodes and lower the function body inline
|
|
const saved_cpn = self.comptime_param_nodes;
|
|
self.comptime_param_nodes = cpn;
|
|
defer self.comptime_param_nodes = saved_cpn;
|
|
|
|
// Install pack-arg-node binding. Mirrors `comptime_param_nodes`:
|
|
// each call owns its own map, nested calls shadow. `lowerIndexExpr`
|
|
// reads the map for `args[<int_literal>]` substitution.
|
|
const saved_pan = self.pack_arg_nodes;
|
|
var pan_map: std.StringHashMap([]const *const Node) = undefined;
|
|
var pan_installed = false;
|
|
if (pack_arg_name) |pn| {
|
|
pan_map = std.StringHashMap([]const *const Node).init(self.alloc);
|
|
pan_map.put(pn, pack_arg_slice) catch {};
|
|
self.pack_arg_nodes = pan_map;
|
|
pan_installed = true;
|
|
}
|
|
defer {
|
|
if (pan_installed) pan_map.deinit();
|
|
self.pack_arg_nodes = saved_pan;
|
|
}
|
|
|
|
// Pin the lowering to the metaprogram's OWN module for the body (and
|
|
// its return type + anything it `#insert`s, e.g. `build_format` / `out`
|
|
// / `emit` inside `std.print` / `log.*`), so those bare names resolve
|
|
// in the defining module's visibility context rather than the call
|
|
// site's (issue 0106). The call-site ARGS above are deliberately lowered
|
|
// BEFORE this, in the caller's context. Mirrors `lowerFunctionBodyInto`,
|
|
// which switches to `func.source_file`. The defining path is stamped on
|
|
// the body node by `resolveImports`; a sourceless body keeps the
|
|
// caller's context.
|
|
const saved_source = self.current_source_file;
|
|
defer self.setCurrentSourceFile(saved_source);
|
|
if (fd.body.source_file) |src| self.setCurrentSourceFile(src);
|
|
|
|
// Lower the body — capture return value for functions with return type
|
|
const ret_ty = self.resolveReturnType(fd);
|
|
if (ret_ty != .void) {
|
|
// Detect whether the body might use `return X;` statements.
|
|
// If so, set up the inline-return slot AND a dedicated
|
|
// "return-done" basic block so each `return X;` stores to
|
|
// the slot and branches to ret_done. After the body lowers,
|
|
// we switch to ret_done and load. Pure tail-expression
|
|
// bodies (arrow form, or a block whose last stmt is an
|
|
// expression) skip the slot+block — keeps the common
|
|
// `format`/`#insert`-style path unchanged.
|
|
const has_return = fnBodyHasReturn(fd.body);
|
|
if (has_return) {
|
|
const ret_slot = self.builder.alloca(ret_ty);
|
|
const ret_done_bb = self.freshBlock("ct.ret_done");
|
|
const saved_iri = self.inline_return_target;
|
|
self.inline_return_target = .{ .slot = ret_slot, .ret_ty = ret_ty, .done_bb = ret_done_bb };
|
|
defer self.inline_return_target = saved_iri;
|
|
|
|
// Lower body. Tail-expression bodies (rare here since
|
|
// has_return == true) produce a tail value we still
|
|
// route through the slot so the load in ret_done picks
|
|
// it up. Block-statement bodies whose last stmt is
|
|
// `return X;` already br to ret_done from inside
|
|
// lowerReturn.
|
|
if (self.lowerBlockValue(fd.body)) |val| {
|
|
if (!self.currentBlockHasTerminator()) {
|
|
const v_ty = self.builder.getRefType(val);
|
|
const coerced = if (v_ty != ret_ty)
|
|
self.coerceToType(val, v_ty, ret_ty)
|
|
else
|
|
val;
|
|
self.builder.store(ret_slot, coerced);
|
|
self.builder.br(ret_done_bb, &.{});
|
|
}
|
|
} else if (!self.currentBlockHasTerminator()) {
|
|
// Body fell through without producing a tail value
|
|
// AND without branching to ret_done — this only
|
|
// happens for bodies whose last stmt is a void
|
|
// statement (e.g. side-effecting). Slot is
|
|
// uninitialised on this path; safer to br anyway
|
|
// so the CFG is well-formed. The load in ret_done
|
|
// will read uninit, which is the same garbage
|
|
// behaviour the regular fn-body lowering would
|
|
// produce for a missing return.
|
|
self.builder.br(ret_done_bb, &.{});
|
|
}
|
|
|
|
self.builder.switchToBlock(ret_done_bb);
|
|
return self.builder.load(ret_slot, ret_ty);
|
|
} else {
|
|
if (self.lowerBlockValue(fd.body)) |val| {
|
|
return val;
|
|
}
|
|
}
|
|
} else {
|
|
self.lowerBlock(fd.body);
|
|
}
|
|
|
|
return self.builder.constInt(0, .void);
|
|
}
|
|
|
|
/// True if `node` (a fn body) contains any top-level `return` statement.
|
|
/// Used by inline-comptime lowering to decide whether to allocate a
|
|
/// result slot — pure tail-expression bodies skip the slot. Walks past
|
|
/// `if`/`while`/`for`/`match` arms (early-return inside a conditional
|
|
/// counts) but stops at nested fn/lambda bodies (those have their own
|
|
/// return contexts).
|
|
fn fnBodyHasReturn(node: *const Node) bool {
|
|
return switch (node.data) {
|
|
.return_stmt => true,
|
|
.block => |b| blk: {
|
|
for (b.stmts) |s| if (fnBodyHasReturn(s)) break :blk true;
|
|
break :blk false;
|
|
},
|
|
.if_expr => |ie| blk: {
|
|
if (fnBodyHasReturn(ie.then_branch)) break :blk true;
|
|
if (ie.else_branch) |eb| if (fnBodyHasReturn(eb)) break :blk true;
|
|
break :blk false;
|
|
},
|
|
.while_expr => |we| fnBodyHasReturn(we.body),
|
|
.for_expr => |fe| fnBodyHasReturn(fe.body),
|
|
.match_expr => |me| blk: {
|
|
for (me.arms) |arm| if (fnBodyHasReturn(arm.body)) break :blk true;
|
|
break :blk false;
|
|
},
|
|
.defer_stmt => |ds| fnBodyHasReturn(ds.expr),
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
/// Pack variadic arguments into a []Any slice. Each arg is boxed as Any {tag, value},
|
|
/// stored into a stack-allocated array, and the slice {ptr, len} is bound to param_name.
|
|
fn lowerVariadicArgs(self: *Lowering, param_name: []const u8, call_args: []const *const Node, start_idx: usize) void {
|
|
const any_slice_ty = self.module.types.sliceOf(.any);
|
|
const n = if (call_args.len > start_idx) call_args.len - start_idx else 0;
|
|
|
|
if (n == 0) {
|
|
// Empty slice: {null, 0}
|
|
const null_ptr = self.builder.constNull(self.module.types.ptrTo(.any));
|
|
const zero_len = self.builder.constInt(0, .s64);
|
|
const slice_slot = self.builder.alloca(any_slice_ty);
|
|
// Store ptr (field 0) and len (field 1) into the slice alloca
|
|
const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(.any), any_slice_ty);
|
|
self.builder.store(ptr_gep, null_ptr);
|
|
const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty);
|
|
self.builder.store(len_gep, zero_len);
|
|
if (self.scope) |scope| {
|
|
scope.put(param_name, .{ .ref = slice_slot, .ty = any_slice_ty, .is_alloca = true });
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Allocate stack array [N x Any]
|
|
const array_ty = self.module.types.arrayOf(.any, @intCast(n));
|
|
const array_slot = self.builder.alloca(array_ty);
|
|
|
|
// Box each arg and store into array
|
|
for (call_args[start_idx..], 0..) |arg, i| {
|
|
var val = self.lowerExpr(arg);
|
|
var source_ty = self.inferExprType(arg);
|
|
// If AST-based inference falls back to .s64 but the lowered ref is a string/struct, use that
|
|
if (source_ty == .unresolved) {
|
|
const ref_ty = self.builder.getRefType(val);
|
|
if (ref_ty == .string or ref_ty == .f32 or ref_ty == .f64 or ref_ty == .bool) {
|
|
source_ty = ref_ty;
|
|
} else if (!ref_ty.isBuiltin()) {
|
|
const ri = self.module.types.get(ref_ty);
|
|
if (ri == .@"struct" or ri == .slice or ri == .optional or ri == .closure or ri == .tuple) {
|
|
source_ty = ref_ty;
|
|
}
|
|
}
|
|
}
|
|
// Auto-unwrap optionals: box inner value if present, else box string "null"
|
|
if (!source_ty.isBuiltin()) {
|
|
const opt_info = self.module.types.get(source_ty);
|
|
if (opt_info == .optional) {
|
|
const child_ty = opt_info.optional.child;
|
|
const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = val } }, .bool);
|
|
const some_bb = self.freshBlock("opt.some");
|
|
const none_bb = self.freshBlock("opt.none");
|
|
const merge_bb = self.freshBlockWithParams("opt.merge", &.{TypeId.any});
|
|
self.builder.condBr(has_val, some_bb, &.{}, none_bb, &.{});
|
|
self.builder.switchToBlock(some_bb);
|
|
const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = val } }, child_ty);
|
|
const boxed_inner = self.builder.boxAny(unwrapped, child_ty);
|
|
self.builder.br(merge_bb, &.{boxed_inner});
|
|
self.builder.switchToBlock(none_bb);
|
|
const null_str_id = self.module.types.internString("null");
|
|
const null_str = self.builder.constString(null_str_id);
|
|
const boxed_null = self.builder.boxAny(null_str, .string);
|
|
self.builder.br(merge_bb, &.{boxed_null});
|
|
self.builder.switchToBlock(merge_bb);
|
|
val = self.builder.blockParam(merge_bb, 0, TypeId.any);
|
|
source_ty = .any;
|
|
}
|
|
}
|
|
const boxed = if (source_ty == .any) val else self.builder.boxAny(val, source_ty);
|
|
// GEP to array[i] and store
|
|
const idx_ref = self.builder.constInt(@intCast(i), .s64);
|
|
const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = idx_ref } }, self.module.types.ptrTo(.any));
|
|
self.builder.store(elem_ptr, boxed);
|
|
}
|
|
|
|
// Build slice {ptr_to_first_element, len}
|
|
const slice_slot = self.builder.alloca(any_slice_ty);
|
|
// Get pointer to first element (array_slot is *[N x Any], GEP to element 0 gives *Any)
|
|
const zero = self.builder.constInt(0, .s64);
|
|
const data_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = zero } }, self.module.types.ptrTo(.any));
|
|
const len_ref = self.builder.constInt(@intCast(n), .s64);
|
|
// Store into slice fields
|
|
const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(.any), any_slice_ty);
|
|
self.builder.store(ptr_gep, data_ptr);
|
|
const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty);
|
|
self.builder.store(len_gep, len_ref);
|
|
|
|
if (self.scope) |scope| {
|
|
scope.put(param_name, .{ .ref = slice_slot, .ty = any_slice_ty, .is_alloca = true });
|
|
}
|
|
}
|
|
|
|
/// Pack variadic args into a slice for regular function calls.
|
|
/// Detects variadic params in the function decl, packs remaining args into a typed slice,
|
|
/// and replaces the args list with [fixed_args..., slice_ref].
|
|
fn packVariadicCallArgs(self: *Lowering, fd: *const ast.FnDecl, c: *const ast.Call, args: *std.ArrayList(Ref)) void {
|
|
// `#foreign` variadic uses the C calling convention's `...` tail —
|
|
// extras are passed through directly with default argument promotion
|
|
// (handled at the call site), not packed into an sx slice.
|
|
if (fd.body.data == .foreign_expr and fd.params.len > 0 and fd.params[fd.params.len - 1].is_variadic) {
|
|
return;
|
|
}
|
|
// Find variadic param index. The two surface forms differ in
|
|
// what `p.type_expr` resolves to: legacy `name: ..T` declares T
|
|
// (element type), new `..name: []T` declares []T (already a
|
|
// slice). Unwrap the latter so the per-element packing below
|
|
// sees T in both cases.
|
|
var variadic_idx: ?usize = null;
|
|
var elem_ty: TypeId = .any;
|
|
for (fd.params, 0..) |p, i| {
|
|
if (p.is_variadic) {
|
|
variadic_idx = i;
|
|
const declared = self.resolveTypeWithBindings(p.type_expr);
|
|
elem_ty = declared;
|
|
if (!declared.isBuiltin()) {
|
|
const info = self.module.types.get(declared);
|
|
if (info == .slice) elem_ty = info.slice.element;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
const vi = variadic_idx orelse return; // no variadic param
|
|
|
|
// Number of non-variadic args
|
|
const fixed_count = vi;
|
|
const variadic_count = if (args.items.len > fixed_count) args.items.len - fixed_count else 0;
|
|
const slice_ty = self.module.types.sliceOf(elem_ty);
|
|
|
|
// Check for spread operator: sum(..arr) — single spread arg becomes the slice directly
|
|
if (variadic_count == 1 and fixed_count < c.args.len) {
|
|
const arg_node = c.args[fixed_count];
|
|
if (arg_node.data == .spread_expr) {
|
|
const spread = arg_node.data.spread_expr;
|
|
const arr_val = self.lowerExpr(spread.operand);
|
|
const arr_ty = self.inferExprType(spread.operand);
|
|
const arr_info = self.module.types.get(arr_ty);
|
|
// Convert array to slice
|
|
const slice_val = switch (arr_info) {
|
|
.array => self.builder.emit(.{ .array_to_slice = .{ .operand = arr_val } }, slice_ty),
|
|
.slice => arr_val,
|
|
else => arr_val,
|
|
};
|
|
args.shrinkRetainingCapacity(fixed_count);
|
|
args.append(self.alloc, slice_val) catch unreachable;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (variadic_count == 0) {
|
|
// Empty slice
|
|
const null_ptr = self.builder.constNull(self.module.types.ptrTo(elem_ty));
|
|
const zero_len = self.builder.constInt(0, .s64);
|
|
const slice_slot = self.builder.alloca(slice_ty);
|
|
const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(elem_ty), slice_ty);
|
|
self.builder.store(ptr_gep, null_ptr);
|
|
const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, slice_ty);
|
|
self.builder.store(len_gep, zero_len);
|
|
const slice_val = self.builder.load(slice_slot, slice_ty);
|
|
// Replace args: keep fixed args, append slice
|
|
args.shrinkRetainingCapacity(fixed_count);
|
|
args.append(self.alloc, slice_val) catch unreachable;
|
|
return;
|
|
}
|
|
|
|
// Determine if we need to box as Any (for ..Any params) or use raw type
|
|
const is_any = (elem_ty == .any);
|
|
// `..xs: []P` (slice of a protocol): each concrete arg must be erased to
|
|
// a protocol value {ctx, vtable}, not stored raw (which would be a
|
|
// size/type mismatch — a heap of garbage vtables → crash on dispatch).
|
|
const elem_is_protocol = blk: {
|
|
if (elem_ty.isBuiltin()) break :blk false;
|
|
const ei = self.module.types.get(elem_ty);
|
|
break :blk ei == .@"struct" and ei.@"struct".is_protocol;
|
|
};
|
|
|
|
// Allocate stack array [N x ElemType]
|
|
const array_elem = if (is_any) TypeId.any else elem_ty;
|
|
const array_ty = self.module.types.arrayOf(array_elem, @intCast(variadic_count));
|
|
const array_slot = self.builder.alloca(array_ty);
|
|
|
|
// Store each variadic arg into array
|
|
for (0..variadic_count) |i| {
|
|
var val = args.items[fixed_count + i];
|
|
if (is_any) {
|
|
var source_ty = self.inferExprType(c.args[fixed_count + i]);
|
|
// If AST-based inference falls back to .s64 but the lowered ref has a richer type, use that
|
|
if (source_ty == .unresolved) {
|
|
const ref_ty = self.builder.getRefType(val);
|
|
if (ref_ty != .unresolved and ref_ty != .void) source_ty = ref_ty;
|
|
}
|
|
// Auto-unwrap optionals: box inner value if present, else box string "null"
|
|
if (!source_ty.isBuiltin()) {
|
|
const opt_info = self.module.types.get(source_ty);
|
|
if (opt_info == .optional) {
|
|
const child_ty = opt_info.optional.child;
|
|
// Branch: has_value? → box inner : box "null"
|
|
const has_val = self.builder.emit(.{ .optional_has_value = .{ .operand = val } }, .bool);
|
|
const some_bb = self.freshBlock("opt.some");
|
|
const none_bb = self.freshBlock("opt.none");
|
|
const merge_bb = self.freshBlockWithParams("opt.merge", &.{TypeId.any});
|
|
self.builder.condBr(has_val, some_bb, &.{}, none_bb, &.{});
|
|
// Some: unwrap and box inner value
|
|
self.builder.switchToBlock(some_bb);
|
|
const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = val } }, child_ty);
|
|
const boxed_inner = self.builder.boxAny(unwrapped, child_ty);
|
|
self.builder.br(merge_bb, &.{boxed_inner});
|
|
// None: box the string "null"
|
|
self.builder.switchToBlock(none_bb);
|
|
const null_str_id = self.module.types.internString("null");
|
|
const null_str = self.builder.constString(null_str_id);
|
|
const boxed_null = self.builder.boxAny(null_str, .string);
|
|
self.builder.br(merge_bb, &.{boxed_null});
|
|
// Merge
|
|
self.builder.switchToBlock(merge_bb);
|
|
val = self.builder.blockParam(merge_bb, 0, TypeId.any);
|
|
source_ty = .any; // already boxed
|
|
}
|
|
}
|
|
if (source_ty != .any) {
|
|
val = self.builder.boxAny(val, source_ty);
|
|
}
|
|
} else if (elem_is_protocol) {
|
|
// Erase each concrete arg to the protocol value via the same
|
|
// impl-driven `xx` machinery, so the runtime `[]P` holds real
|
|
// {ctx, vtable} values and `xs[i].method()` dispatches.
|
|
const arg_node = c.args[fixed_count + i];
|
|
var source_ty = self.inferExprType(arg_node);
|
|
if (source_ty == .unresolved) source_ty = self.builder.getRefType(val);
|
|
if (source_ty != elem_ty) {
|
|
val = self.buildProtocolErasure(val, arg_node, source_ty, elem_ty);
|
|
}
|
|
}
|
|
const idx_ref = self.builder.constInt(@intCast(i), .s64);
|
|
const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = idx_ref } }, self.module.types.ptrTo(array_elem));
|
|
self.builder.store(elem_ptr, val);
|
|
}
|
|
|
|
// Build slice {ptr, len}
|
|
const slice_slot = self.builder.alloca(slice_ty);
|
|
const zero = self.builder.constInt(0, .s64);
|
|
const data_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = zero } }, self.module.types.ptrTo(array_elem));
|
|
const len_ref = self.builder.constInt(@intCast(variadic_count), .s64);
|
|
const ptr_gep = self.builder.structGepTyped(slice_slot, 0, self.module.types.ptrTo(array_elem), slice_ty);
|
|
self.builder.store(ptr_gep, data_ptr);
|
|
const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, slice_ty);
|
|
self.builder.store(len_gep, len_ref);
|
|
const slice_val = self.builder.load(slice_slot, slice_ty);
|
|
|
|
// Replace args: keep fixed args, append slice
|
|
args.shrinkRetainingCapacity(fixed_count);
|
|
args.append(self.alloc, slice_val) catch unreachable;
|
|
}
|
|
|
|
// ── Generic monomorphization ──────────────────────────────────
|
|
|
|
/// Build `tp.name -> TypeId` bindings for a generic call.
|
|
/// `args_ast` must be parallel to `fd.params`; for dot-calls the caller
|
|
/// prepends the receiver's AST node so positions align with `fd.params[0] = self`.
|
|
/// Caller owns the returned map and must call `.deinit()`.
|
|
/// Lower a call to a generic function by monomorphizing it with inferred type arguments.
|
|
fn lowerGenericCall(self: *Lowering, fd: *const ast.FnDecl, base_name: []const u8, call_node: *const ast.Call, lowered_args: []Ref) Ref {
|
|
var bindings = self.genericResolver().buildTypeBindings(fd, call_node.args);
|
|
defer bindings.deinit();
|
|
|
|
const types_passed_explicitly = call_node.args.len == fd.params.len;
|
|
const mangled_name = self.genericResolver().mangleGenericName(base_name, fd, &bindings);
|
|
|
|
if (!self.lowered_functions.contains(mangled_name)) {
|
|
self.monomorphizeFunction(fd, mangled_name, &bindings);
|
|
}
|
|
|
|
if (self.resolveFuncByName(mangled_name)) |fid| {
|
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
|
const ret_ty = func.ret;
|
|
const params = func.params;
|
|
// Build value-only args (skip type param declaration args)
|
|
var value_args = std.ArrayList(Ref).empty;
|
|
defer value_args.deinit(self.alloc);
|
|
var arg_idx: usize = 0;
|
|
for (fd.params) |p| {
|
|
if (isTypeParamDecl(&p, fd.type_params)) {
|
|
if (types_passed_explicitly) arg_idx += 1;
|
|
continue;
|
|
}
|
|
if (arg_idx < lowered_args.len) {
|
|
value_args.append(self.alloc, lowered_args[arg_idx]) catch unreachable;
|
|
}
|
|
arg_idx += 1;
|
|
}
|
|
const final_args = self.prependCtxIfNeeded(func, value_args.items);
|
|
self.coerceCallArgs(final_args, params);
|
|
return self.builder.call(fid, final_args, ret_ty);
|
|
}
|
|
|
|
return self.emitError(base_name, call_node.callee.span);
|
|
}
|
|
|
|
/// Create a monomorphized instance of a generic function.
|
|
/// Check if a call has a `cast(runtime_var, val)` argument (runtime type dispatch pattern).
|
|
fn hasCastWithRuntimeType(self: *Lowering, c: *const ast.Call) bool {
|
|
for (c.args) |arg| {
|
|
if (arg.data == .call) {
|
|
if (arg.data.call.callee.data == .identifier) {
|
|
const name = arg.data.call.callee.data.identifier.name;
|
|
if (std.mem.eql(u8, name, "cast") and arg.data.call.args.len == 2) {
|
|
const type_arg = arg.data.call.args[0];
|
|
if (type_arg.data == .identifier) {
|
|
// It's a runtime type if it's in scope as a variable
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(type_arg.data.identifier.name) != null) return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Generate runtime dispatch for a generic call inside a type-match arm.
|
|
/// For each type tag in match_tags, monomorphizes the generic function and calls it.
|
|
fn lowerRuntimeDispatchCall(
|
|
self: *Lowering,
|
|
fd: *const ast.FnDecl,
|
|
base_name: []const u8,
|
|
call_node: *const ast.Call,
|
|
match_tags: []const u64,
|
|
) Ref {
|
|
// Find the cast arg: cast(type_var, any_val)
|
|
var cast_arg_idx: usize = 0;
|
|
var type_tag_node: ?*const Node = null;
|
|
var any_val_node: ?*const Node = null;
|
|
for (call_node.args, 0..) |arg, i| {
|
|
if (arg.data == .call and arg.data.call.callee.data == .identifier) {
|
|
const name = arg.data.call.callee.data.identifier.name;
|
|
if (std.mem.eql(u8, name, "cast") and arg.data.call.args.len == 2) {
|
|
cast_arg_idx = i;
|
|
type_tag_node = arg.data.call.args[0];
|
|
any_val_node = arg.data.call.args[1];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lower the type tag (runtime value) and Any value BEFORE the switch
|
|
const type_tag_raw = self.lowerExpr(type_tag_node orelse return self.emitError("dispatch", call_node.callee.span));
|
|
const type_tag_node_ty = self.inferExprType(type_tag_node.?);
|
|
const type_tag = if (type_tag_node_ty == .any)
|
|
self.builder.emit(.{ .unbox_any = .{ .operand = type_tag_raw } }, .s64)
|
|
else
|
|
type_tag_raw;
|
|
const any_val = self.lowerExpr(any_val_node orelse return self.emitError("dispatch", call_node.callee.span));
|
|
|
|
// Lower non-cast arguments once (before the switch)
|
|
var other_args = std.ArrayList(?Ref).empty;
|
|
defer other_args.deinit(self.alloc);
|
|
for (call_node.args, 0..) |arg, i| {
|
|
if (i == cast_arg_idx) {
|
|
other_args.append(self.alloc, null) catch unreachable; // placeholder
|
|
} else {
|
|
other_args.append(self.alloc, self.lowerExpr(arg)) catch unreachable;
|
|
}
|
|
}
|
|
|
|
// Resolve return type (using first available binding)
|
|
const ret_ty: TypeId = blk: {
|
|
if (fd.return_type) |rt| {
|
|
if (rt.data == .type_expr) {
|
|
if (type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map) != .unresolved) {
|
|
break :blk type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
|
}
|
|
}
|
|
}
|
|
break :blk .string; // default for to_string functions
|
|
};
|
|
|
|
const merge_bb = self.freshBlock("dispatch.merge");
|
|
const default_bb = self.freshBlock("dispatch.default");
|
|
|
|
// Build switch cases
|
|
var cases = std.ArrayList(inst_mod.SwitchBranch.Case).empty;
|
|
defer cases.deinit(self.alloc);
|
|
|
|
// For each type tag, create a case block
|
|
var case_blocks = std.ArrayList(BlockId).empty;
|
|
defer case_blocks.deinit(self.alloc);
|
|
|
|
for (match_tags) |tag| {
|
|
const case_bb = self.freshBlock("dispatch.case");
|
|
case_blocks.append(self.alloc, case_bb) catch unreachable;
|
|
cases.append(self.alloc, .{
|
|
.value = @intCast(tag),
|
|
.target = case_bb,
|
|
.args = &.{},
|
|
}) catch unreachable;
|
|
}
|
|
|
|
// Create a result alloca BEFORE the switch (must be before terminator)
|
|
var result_slot: ?Ref = null;
|
|
if (ret_ty != .void) {
|
|
result_slot = self.builder.alloca(ret_ty);
|
|
}
|
|
|
|
self.builder.switchBr(type_tag, cases.items, default_bb, &.{});
|
|
|
|
for (match_tags, 0..) |tag, ti| {
|
|
self.builder.switchToBlock(case_blocks.items[ti]);
|
|
|
|
const ty_id = TypeId.fromIndex(@intCast(tag));
|
|
|
|
// Unbox the Any value to the concrete type
|
|
const unboxed = self.builder.emit(.{ .unbox_any = .{
|
|
.operand = any_val,
|
|
} }, ty_id);
|
|
|
|
if (fd.type_params.len > 0) {
|
|
// Generic function: build type bindings + monomorphize
|
|
var bindings = std.StringHashMap(TypeId).init(self.alloc);
|
|
defer bindings.deinit();
|
|
|
|
// Find which type param the cast arg corresponds to
|
|
if (cast_arg_idx < fd.params.len) {
|
|
const param_te = fd.params[cast_arg_idx].type_expr;
|
|
if (param_te.data == .type_expr) {
|
|
// Direct: `param: $T` → T = ty_id
|
|
const tp_name = param_te.data.type_expr.name;
|
|
for (fd.type_params) |tp| {
|
|
if (std.mem.eql(u8, tp.name, tp_name)) {
|
|
bindings.put(tp.name, ty_id) catch {};
|
|
break;
|
|
}
|
|
}
|
|
} else if (param_te.data == .slice_type_expr) {
|
|
// Compound: `param: []$T` → T = element type of ty_id
|
|
const elem_te = param_te.data.slice_type_expr.element_type;
|
|
if (elem_te.data == .type_expr) {
|
|
const tp_name = elem_te.data.type_expr.name;
|
|
for (fd.type_params) |tp| {
|
|
if (std.mem.eql(u8, tp.name, tp_name)) {
|
|
const elem_ty = self.getElementType(ty_id);
|
|
bindings.put(tp.name, if (elem_ty != .void) elem_ty else ty_id) catch {};
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else if (param_te.data == .pointer_type_expr) {
|
|
// Compound: `param: *$T` → T = pointee type of ty_id
|
|
const pointee_te = param_te.data.pointer_type_expr.pointee_type;
|
|
if (pointee_te.data == .type_expr) {
|
|
const tp_name = pointee_te.data.type_expr.name;
|
|
for (fd.type_params) |tp| {
|
|
if (std.mem.eql(u8, tp.name, tp_name)) {
|
|
if (!ty_id.isBuiltin()) {
|
|
const pinfo = self.module.types.get(ty_id);
|
|
if (pinfo == .pointer) {
|
|
bindings.put(tp.name, pinfo.pointer.pointee) catch {};
|
|
break;
|
|
}
|
|
}
|
|
bindings.put(tp.name, ty_id) catch {};
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build mangled name
|
|
var mangled_buf: [256]u8 = undefined;
|
|
var mangled_len: usize = 0;
|
|
for (base_name) |ch| {
|
|
if (mangled_len < mangled_buf.len) { mangled_buf[mangled_len] = ch; mangled_len += 1; }
|
|
}
|
|
for (fd.type_params) |tp| {
|
|
for ("__") |ch| {
|
|
if (mangled_len < mangled_buf.len) { mangled_buf[mangled_len] = ch; mangled_len += 1; }
|
|
}
|
|
const bound_ty = bindings.get(tp.name) orelse ty_id;
|
|
const type_name_str = self.mangleTypeName(bound_ty);
|
|
for (type_name_str) |ch| {
|
|
if (mangled_len < mangled_buf.len) { mangled_buf[mangled_len] = ch; mangled_len += 1; }
|
|
}
|
|
}
|
|
const mangled_name = mangled_buf[0..mangled_len];
|
|
|
|
// Monomorphize if not already done
|
|
if (!self.lowered_functions.contains(mangled_name)) {
|
|
self.monomorphizeFunction(fd, mangled_name, &bindings);
|
|
}
|
|
|
|
// Build call args (replace cast arg with unboxed value, skip type param decl args)
|
|
if (self.resolveFuncByName(mangled_name)) |fid| {
|
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
|
const callee_ret = func.ret;
|
|
const callee_params = func.params;
|
|
var call_args = std.ArrayList(Ref).empty;
|
|
defer call_args.deinit(self.alloc);
|
|
for (fd.params, 0..) |p, pi| {
|
|
if (isTypeParamDecl(&p, fd.type_params)) continue;
|
|
if (pi == cast_arg_idx) {
|
|
call_args.append(self.alloc, unboxed) catch unreachable;
|
|
} else if (pi < other_args.items.len) {
|
|
if (other_args.items[pi]) |ref| {
|
|
call_args.append(self.alloc, ref) catch unreachable;
|
|
}
|
|
}
|
|
}
|
|
const final_args = self.prependCtxIfNeeded(func, call_args.items);
|
|
self.coerceCallArgs(final_args, callee_params);
|
|
const result = self.builder.call(fid, final_args, callee_ret);
|
|
if (result_slot) |slot| {
|
|
self.builder.store(slot, result);
|
|
}
|
|
}
|
|
} else {
|
|
// Non-generic function: call directly with per-tag unboxing + coercion
|
|
const resolve_name = base_name;
|
|
if (!self.lowered_functions.contains(resolve_name)) {
|
|
self.lazyLowerFunction(resolve_name);
|
|
}
|
|
if (self.resolveFuncByName(resolve_name)) |fid| {
|
|
const callee_func = &self.module.functions.items[@intFromEnum(fid)];
|
|
const callee_ret = callee_func.ret;
|
|
const callee_params = callee_func.params;
|
|
const callee_has_ctx = callee_func.has_implicit_ctx;
|
|
var call_args = std.ArrayList(Ref).empty;
|
|
defer call_args.deinit(self.alloc);
|
|
for (fd.params, 0..) |_, pi| {
|
|
if (pi == cast_arg_idx) {
|
|
// Coerce unboxed value (typed as ty_id) to param type
|
|
var arg = unboxed;
|
|
// callee param index shifts by +1 if it carries __sx_ctx
|
|
const callee_pi = pi + @as(usize, if (callee_has_ctx) 1 else 0);
|
|
if (callee_pi < callee_params.len) {
|
|
arg = self.coerceToType(arg, ty_id, callee_params[callee_pi].ty);
|
|
}
|
|
call_args.append(self.alloc, arg) catch unreachable;
|
|
} else if (pi < other_args.items.len) {
|
|
if (other_args.items[pi]) |ref| {
|
|
call_args.append(self.alloc, ref) catch unreachable;
|
|
}
|
|
}
|
|
}
|
|
// Prepend __sx_ctx if needed BEFORE coercion so indices line up.
|
|
var final_call_args: []Ref = call_args.items;
|
|
if (callee_has_ctx) {
|
|
final_call_args = self.alloc.alloc(Ref, call_args.items.len + 1) catch call_args.items;
|
|
if (final_call_args.len == call_args.items.len + 1) {
|
|
final_call_args[0] = self.current_ctx_ref;
|
|
@memcpy(final_call_args[1..], call_args.items);
|
|
}
|
|
}
|
|
// Coerce non-cast args (source type unknown, use s64 default).
|
|
// cast_arg_idx is in user-space (skips __sx_ctx); offset by ctx_slots.
|
|
const ctx_slots: usize = if (callee_has_ctx) 1 else 0;
|
|
for (0..@min(final_call_args.len, callee_params.len)) |ci| {
|
|
if (ci < ctx_slots) continue; // skip __sx_ctx slot
|
|
if ((ci - ctx_slots) != cast_arg_idx) {
|
|
final_call_args[ci] = self.coerceToType(final_call_args[ci], .s64, callee_params[ci].ty);
|
|
}
|
|
}
|
|
const result = self.builder.call(fid, final_call_args, callee_ret);
|
|
if (result_slot) |slot| {
|
|
self.builder.store(slot, result);
|
|
}
|
|
}
|
|
}
|
|
|
|
self.builder.br(merge_bb, &.{});
|
|
}
|
|
|
|
// Default block: store a default value and branch to merge
|
|
self.builder.switchToBlock(default_bb);
|
|
if (result_slot) |slot| {
|
|
const empty_id = self.module.types.internString("");
|
|
const default_val = if (ret_ty == .string) self.builder.constString(empty_id) else self.zeroValue(ret_ty);
|
|
self.builder.store(slot, default_val);
|
|
}
|
|
self.builder.br(merge_bb, &.{});
|
|
|
|
// Merge block: load result
|
|
self.builder.switchToBlock(merge_bb);
|
|
if (result_slot) |slot| {
|
|
return self.builder.load(slot, ret_ty);
|
|
}
|
|
return self.builder.constInt(0, .void);
|
|
}
|
|
|
|
/// Build an `[]Any` slice value from the mono's pack params and
|
|
/// bind it to the pack name in scope. Each pack-param slot is
|
|
/// loaded, boxed via `boxAny`, and stored into a stack [N x Any]
|
|
/// array; the slice {data_ptr, len} is then bound. Used by
|
|
/// `monomorphizePackFn` so bodies that reference `args` bare or
|
|
/// index it with a runtime int resolve through the slice (with
|
|
/// element type `Any`). Literal-indexed accesses keep the
|
|
/// concrete per-position types via `packArgNodeAt`.
|
|
/// Build a `[]Type` slice VALUE for a bare `$<pack>` reference.
|
|
/// Differs from `materialisePackSlice` (which boxes each pack
|
|
/// element as Any so the body's `args[i]` reads an Any) — this
|
|
/// helper stores raw `.type_tag` Values via `const_type`, so the
|
|
/// slice is a list-of-Types that builder fns walk at interp time.
|
|
/// Slice IR type is `[]Any` (since `Type → .any`); the interp
|
|
/// stores whichever Value the elements actually carry.
|
|
fn buildPackSliceValue(self: *Lowering, arg_types: []const TypeId) Ref {
|
|
const any_slice_ty = self.module.types.sliceOf(.any);
|
|
const any_ptr_ty = self.module.types.ptrTo(.any);
|
|
|
|
if (arg_types.len == 0) {
|
|
const null_ptr = self.builder.constNull(any_ptr_ty);
|
|
const zero_len = self.builder.constInt(0, .s64);
|
|
const slice_slot = self.builder.alloca(any_slice_ty);
|
|
const ptr_gep = self.builder.structGepTyped(slice_slot, 0, any_ptr_ty, any_slice_ty);
|
|
self.builder.store(ptr_gep, null_ptr);
|
|
const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty);
|
|
self.builder.store(len_gep, zero_len);
|
|
return self.builder.load(slice_slot, any_slice_ty);
|
|
}
|
|
|
|
const array_ty = self.module.types.arrayOf(.any, @intCast(arg_types.len));
|
|
const array_slot = self.builder.alloca(array_ty);
|
|
|
|
for (arg_types, 0..) |ty, i| {
|
|
// `const_type` produces an `.any`-typed Type value
|
|
// (`{tag=.any, value=tid}`) — already the canonical Any
|
|
// shape, so no re-box needed.
|
|
const type_val = self.builder.constType(ty);
|
|
const idx_ref = self.builder.constInt(@intCast(i), .s64);
|
|
const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = idx_ref } }, any_ptr_ty);
|
|
self.builder.store(elem_ptr, type_val);
|
|
}
|
|
|
|
const slice_slot = self.builder.alloca(any_slice_ty);
|
|
const zero = self.builder.constInt(0, .s64);
|
|
const data_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = zero } }, any_ptr_ty);
|
|
const len_ref = self.builder.constInt(@intCast(arg_types.len), .s64);
|
|
const ptr_gep = self.builder.structGepTyped(slice_slot, 0, any_ptr_ty, any_slice_ty);
|
|
self.builder.store(ptr_gep, data_ptr);
|
|
const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty);
|
|
self.builder.store(len_gep, len_ref);
|
|
return self.builder.load(slice_slot, any_slice_ty);
|
|
}
|
|
|
|
fn materialisePackSlice(
|
|
self: *Lowering,
|
|
scope: *Scope,
|
|
pack_name: []const u8,
|
|
slot_refs: []const Ref,
|
|
arg_types: []const TypeId,
|
|
) void {
|
|
const any_slice_ty = self.module.types.sliceOf(.any);
|
|
const any_ptr_ty = self.module.types.ptrTo(.any);
|
|
|
|
if (arg_types.len == 0) {
|
|
const null_ptr = self.builder.constNull(any_ptr_ty);
|
|
const zero_len = self.builder.constInt(0, .s64);
|
|
const slice_slot = self.builder.alloca(any_slice_ty);
|
|
const ptr_gep = self.builder.structGepTyped(slice_slot, 0, any_ptr_ty, any_slice_ty);
|
|
self.builder.store(ptr_gep, null_ptr);
|
|
const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty);
|
|
self.builder.store(len_gep, zero_len);
|
|
scope.put(pack_name, .{ .ref = slice_slot, .ty = any_slice_ty, .is_alloca = true });
|
|
return;
|
|
}
|
|
|
|
const array_ty = self.module.types.arrayOf(.any, @intCast(arg_types.len));
|
|
const array_slot = self.builder.alloca(array_ty);
|
|
|
|
for (slot_refs, arg_types, 0..) |slot, ty, i| {
|
|
const val = self.builder.load(slot, ty);
|
|
const boxed = if (ty == .any) val else self.builder.boxAny(val, ty);
|
|
const idx_ref = self.builder.constInt(@intCast(i), .s64);
|
|
const elem_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = idx_ref } }, any_ptr_ty);
|
|
self.builder.store(elem_ptr, boxed);
|
|
}
|
|
|
|
const slice_slot = self.builder.alloca(any_slice_ty);
|
|
const zero = self.builder.constInt(0, .s64);
|
|
const data_ptr = self.builder.emit(.{ .index_gep = .{ .lhs = array_slot, .rhs = zero } }, any_ptr_ty);
|
|
const len_ref = self.builder.constInt(@intCast(arg_types.len), .s64);
|
|
const ptr_gep = self.builder.structGepTyped(slice_slot, 0, any_ptr_ty, any_slice_ty);
|
|
self.builder.store(ptr_gep, data_ptr);
|
|
const len_gep = self.builder.structGepTyped(slice_slot, 1, .s64, any_slice_ty);
|
|
self.builder.store(len_gep, len_ref);
|
|
scope.put(pack_name, .{ .ref = slice_slot, .ty = any_slice_ty, .is_alloca = true });
|
|
}
|
|
|
|
/// Infer the return type of a pack-fn body for the generic-`$R`
|
|
/// case. Walks the body looking for the first concrete return
|
|
/// type: a `return X;` statement's value type, or — failing that —
|
|
/// the tail expression of an arrow-form body. Caller must have
|
|
/// `pack_arg_nodes` installed so `args[<lit>]` substitutes during
|
|
/// inference. Falls back to `.s64` if nothing concrete is found
|
|
/// (matches the broader "default to .s64" convention elsewhere).
|
|
fn inferPackBodyReturnType(self: *Lowering, body: *const Node) TypeId {
|
|
// First try explicit `return X;` — walks past structured
|
|
// control flow but stops at nested fn / lambda bodies.
|
|
if (self.findReturnValueType(body)) |ty| return ty;
|
|
// Arrow-form / tail-expression body: the body IS the value.
|
|
// For block bodies whose last stmt is an expression, walk down.
|
|
if (body.data == .block) {
|
|
const stmts = body.data.block.stmts;
|
|
if (stmts.len == 0) return .void;
|
|
return self.inferExprType(stmts[stmts.len - 1]);
|
|
}
|
|
return self.inferExprType(body);
|
|
}
|
|
|
|
/// Per-call-shape monomorphisation entry for pack-fns
|
|
/// (`isPackFn(fd) == true`). Computes a mangled name from the
|
|
/// call-site arg types, builds the mono if it's not cached, and
|
|
/// emits a direct call. Pack params expand into N positional IR
|
|
/// params with concrete types; the body's `args[<lit>]` and
|
|
/// `args.len` resolve to those params via the pack bindings.
|
|
fn lowerPackFnCall(self: *Lowering, fd: *const ast.FnDecl, call_node: *const ast.Call) Ref {
|
|
// Split call args along the fd.params boundary:
|
|
// - non-comptime non-pack params → consume one call arg as a
|
|
// runtime IR param.
|
|
// - comptime non-pack params → consume one call arg, fold its
|
|
// value into the mangle (NOT a runtime IR param).
|
|
// - pack param (always last) → consume the remaining call args
|
|
// as the pack expansion.
|
|
var pack_arg_types = std.ArrayList(TypeId).empty;
|
|
defer pack_arg_types.deinit(self.alloc);
|
|
var pack_start: usize = call_node.args.len;
|
|
// Constraint protocol of the pack param (`..xs: P`), if any. The
|
|
// comptime type-pack `..$args` has no constraint to check.
|
|
var pack_protocol: ?[]const u8 = null;
|
|
var pack_is_comptime = false;
|
|
var pack_name: []const u8 = "";
|
|
{
|
|
var fi: usize = 0;
|
|
for (fd.params) |p| {
|
|
if (isPackParam(p)) {
|
|
pack_start = fi;
|
|
pack_is_comptime = p.is_comptime;
|
|
pack_name = p.name;
|
|
if (p.is_pack and p.type_expr.data == .type_expr) {
|
|
pack_protocol = p.type_expr.data.type_expr.name;
|
|
}
|
|
break;
|
|
}
|
|
if (fi >= call_node.args.len) break;
|
|
fi += 1;
|
|
}
|
|
}
|
|
|
|
// Lower the PACK args first, taking each type from the lowered value
|
|
// (`getRefType`) — never a pre-lowering `inferExprType` guess. Knowing
|
|
// the pack element types up front lets the prefix args (e.g.
|
|
// `mapper: Closure(..sources.T) -> $R`) resolve against them, so a
|
|
// lambda arg types its params from the projected closure signature.
|
|
// (A comptime `..$args` pack keeps `inferExprType` — its args may be
|
|
// type-position.)
|
|
// A pack arg is independently typed — it takes its natural type and
|
|
// (for a comptime `..$args` pack) auto-boxes to `Any` at the call
|
|
// boundary. It is NEVER coerced to a leftover outer `target_type`, so
|
|
// clear it: otherwise an `xx <expr>` pack arg (whose result type IS
|
|
// `target_type`) would cast to the stale target — e.g. `format("…", xx i)`
|
|
// inside a `-> string` fn mis-typed the arg as `string`, monomorphizing
|
|
// `__pack_string` and ABI-coercing the 4-byte int as a 16-byte fat
|
|
// pointer → memory corruption (issue 0057).
|
|
const saved_pack_tt = self.target_type;
|
|
self.target_type = null;
|
|
var pack_refs = std.ArrayList(Ref).empty;
|
|
defer pack_refs.deinit(self.alloc);
|
|
for (call_node.args[pack_start..]) |a| {
|
|
const r = self.lowerExpr(a);
|
|
pack_refs.append(self.alloc, r) catch return self.builder.constInt(0, .void);
|
|
if (pack_is_comptime) {
|
|
const it = self.inferExprType(a);
|
|
pack_arg_types.append(self.alloc, if (it == .unresolved) self.builder.getRefType(r) else it) catch return self.builder.constInt(0, .void);
|
|
} else {
|
|
pack_arg_types.append(self.alloc, self.builder.getRefType(r)) catch return self.builder.constInt(0, .void);
|
|
}
|
|
}
|
|
self.target_type = saved_pack_tt;
|
|
|
|
// Install the pack's element types + constraint so prefix-arg param
|
|
// types like `Closure(..sources.T)` resolve while lowering the prefix.
|
|
var pat_map = std.StringHashMap([]const TypeId).init(self.alloc);
|
|
defer pat_map.deinit();
|
|
pat_map.put(pack_name, pack_arg_types.items) catch {};
|
|
var pcon_map = std.StringHashMap([]const u8).init(self.alloc);
|
|
defer pcon_map.deinit();
|
|
if (pack_protocol) |proto| pcon_map.put(pack_name, proto) catch {};
|
|
const saved_pat = self.pack_arg_types;
|
|
const saved_pcon = self.pack_constraint;
|
|
self.pack_arg_types = pat_map;
|
|
if (pack_protocol != null) self.pack_constraint = pcon_map;
|
|
|
|
var args = std.ArrayList(Ref).empty;
|
|
defer args.deinit(self.alloc);
|
|
{
|
|
var ri: usize = 0;
|
|
for (fd.params) |p| {
|
|
if (isPackParam(p)) break;
|
|
if (ri >= call_node.args.len) break;
|
|
if (!p.is_comptime) {
|
|
// Contextually type the arg from the param (so a lambda arg
|
|
// `(x) => …` takes its param types from a `Closure(...)` param).
|
|
// The param type is resolved under the pack fn's OWN source
|
|
// (E4): a fixed-prefix type bare-visible only in the defining
|
|
// module must resolve there, not the caller's. The arg itself
|
|
// is lowered AFTER, in the caller's context.
|
|
const saved_tt = self.target_type;
|
|
const pty = self.resolveParamTypeInSource(fd.body.source_file, &p);
|
|
if (pty != .unresolved) self.target_type = pty;
|
|
args.append(self.alloc, self.lowerExpr(call_node.args[ri])) catch return self.builder.constInt(0, .void);
|
|
self.target_type = saved_tt;
|
|
}
|
|
ri += 1;
|
|
}
|
|
}
|
|
self.pack_arg_types = saved_pat;
|
|
self.pack_constraint = saved_pcon;
|
|
|
|
// Infer type-param bindings (e.g. `$R` in `mapper: Closure(..) -> $R`)
|
|
// from the lowered prefix args. `args.items` holds the non-comptime
|
|
// prefix refs in declaration order; match each prefix param's declared
|
|
// type against its arg's concrete type to bind the function's
|
|
// type-params. These flow into the mangle and the mono's
|
|
// `self.type_bindings` so `-> VL($R)` / `Combined($R, ..)` resolve.
|
|
var tparam_bindings = std.StringHashMap(TypeId).init(self.alloc);
|
|
defer tparam_bindings.deinit();
|
|
if (fd.type_params.len > 0) {
|
|
var pref_ref_idx: usize = 0;
|
|
for (fd.params) |p| {
|
|
if (isPackParam(p)) break;
|
|
if (p.is_comptime) continue;
|
|
if (pref_ref_idx >= args.items.len) break;
|
|
const arg_ty = self.builder.getRefType(args.items[pref_ref_idx]);
|
|
for (fd.type_params) |tp| {
|
|
if (tparam_bindings.contains(tp.name)) continue;
|
|
if (self.extractTypeParam(p.type_expr, arg_ty, tp.name)) |ety| {
|
|
if (ety != .unresolved) tparam_bindings.put(tp.name, ety) catch {};
|
|
}
|
|
}
|
|
pref_ref_idx += 1;
|
|
}
|
|
}
|
|
|
|
// Append the (already-lowered) pack args after the prefix args.
|
|
for (pack_refs.items) |r| args.append(self.alloc, r) catch return self.builder.constInt(0, .void);
|
|
|
|
// Per-position conformance: each pack arg must impl the constraint
|
|
// protocol. Only enforced for a known protocol constraint — an unknown
|
|
// name (e.g. a plain type used as a pack constraint) is left alone.
|
|
if (pack_protocol) |proto| {
|
|
if (self.program_index.protocol_ast_map.contains(proto)) {
|
|
for (call_node.args[pack_start..], pack_arg_types.items) |arg_node, arg_ty| {
|
|
if (!self.protocolResolver().packArgConformsTo(proto, arg_ty)) {
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, arg_node.span, "pack argument of type '{s}' does not conform to protocol '{s}'", .{ self.formatTypeName(arg_ty), proto });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mangle: `<fn_name>__pack__<arg_types>` with comptime values
|
|
// (if any) folded into a `__ct_<value>` segment per non-pack
|
|
// comptime param. Distinct call shapes — including different
|
|
// comptime VALUES — get distinct symbols.
|
|
var name_buf = std.ArrayList(u8).empty;
|
|
defer name_buf.deinit(self.alloc);
|
|
name_buf.appendSlice(self.alloc, fd.name) catch return self.builder.constInt(0, .void);
|
|
// Comptime values first (deterministic by fd.params order).
|
|
var ct_fi: usize = 0;
|
|
for (fd.params) |p| {
|
|
if (isPackParam(p)) break;
|
|
if (ct_fi >= call_node.args.len) break;
|
|
if (p.is_comptime) {
|
|
name_buf.appendSlice(self.alloc, "__ct_") catch return self.builder.constInt(0, .void);
|
|
self.genericResolver().appendComptimeValueMangle(&name_buf, call_node.args[ct_fi]);
|
|
}
|
|
ct_fi += 1;
|
|
}
|
|
// Inferred type-param bindings (deterministic by fd.type_params order).
|
|
for (fd.type_params) |tp| {
|
|
if (tparam_bindings.get(tp.name)) |ty| {
|
|
name_buf.appendSlice(self.alloc, "__tp_") catch return self.builder.constInt(0, .void);
|
|
name_buf.appendSlice(self.alloc, self.mangleTypeName(ty)) catch return self.builder.constInt(0, .void);
|
|
}
|
|
}
|
|
name_buf.appendSlice(self.alloc, "__pack") catch return self.builder.constInt(0, .void);
|
|
for (pack_arg_types.items) |t| {
|
|
name_buf.append(self.alloc, '_') catch return self.builder.constInt(0, .void);
|
|
name_buf.appendSlice(self.alloc, self.mangleTypeName(t)) catch return self.builder.constInt(0, .void);
|
|
}
|
|
const mangled = name_buf.items;
|
|
|
|
if (!self.lowered_functions.contains(mangled)) {
|
|
self.monomorphizePackFn(fd, mangled, pack_arg_types.items, call_node, &tparam_bindings);
|
|
}
|
|
|
|
const fid = self.resolveFuncByName(mangled) orelse return self.builder.constInt(0, .void);
|
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
|
const ret_ty = func.ret;
|
|
const params = func.params;
|
|
const final_args = self.prependCtxIfNeeded(func, args.items);
|
|
self.coerceCallArgs(final_args, params);
|
|
return self.builder.call(fid, final_args, ret_ty);
|
|
}
|
|
|
|
|
|
/// Build a single mono fn for the given pack-fn + concrete arg types.
|
|
/// The mono carries N positional pack-params (synthesised names
|
|
/// `__pack_<name>_<i>`) plus any fixed-prefix non-pack params from
|
|
/// the original declaration. The body lowers normally — real
|
|
/// `return X;` emits real `ret X`; `args[<lit>]` substitutes via
|
|
/// `pack_arg_nodes`; `args.len` resolves via `pack_param_count`.
|
|
fn monomorphizePackFn(
|
|
self: *Lowering,
|
|
fd: *const ast.FnDecl,
|
|
mangled_name: []const u8,
|
|
arg_types: []const TypeId,
|
|
call_node: *const ast.Call,
|
|
type_bindings: *const std.StringHashMap(TypeId),
|
|
) void {
|
|
const owned_name = self.alloc.dupe(u8, mangled_name) catch return;
|
|
self.lowered_functions.put(owned_name, {}) catch {};
|
|
|
|
// Find the pack param's name and position in fd.params, plus its
|
|
// constraint protocol (`..xs: Box` ⇒ "Box"; comptime `..$args` has none).
|
|
var pack_name: []const u8 = "";
|
|
var pack_param_idx: usize = std.math.maxInt(usize);
|
|
var pack_proto: ?[]const u8 = null;
|
|
for (fd.params, 0..) |p, i| {
|
|
if (isPackParam(p)) {
|
|
pack_name = p.name;
|
|
pack_param_idx = i;
|
|
if (p.is_pack and p.type_expr.data == .type_expr) {
|
|
pack_proto = p.type_expr.data.type_expr.name;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (pack_param_idx == std.math.maxInt(usize)) return;
|
|
|
|
// Save state — mirrors monomorphizeFunction but also captures
|
|
// pack/inline-return state since the mono body must NOT route
|
|
// returns through any caller's inline slot.
|
|
const saved_func = self.builder.func;
|
|
const saved_block = self.builder.current_block;
|
|
const saved_counter = self.builder.inst_counter;
|
|
const saved_scope = self.scope;
|
|
const saved_defer_base = self.func_defer_base;
|
|
const saved_block_terminated = self.block_terminated;
|
|
const saved_target = self.target_type;
|
|
const saved_pan = self.pack_arg_nodes;
|
|
const saved_ppc = self.pack_param_count;
|
|
const saved_pat = self.pack_arg_types;
|
|
const saved_pcon = self.pack_constraint;
|
|
const saved_iri = self.inline_return_target;
|
|
const saved_ctx_ref = self.current_ctx_ref;
|
|
const saved_type_bindings = self.type_bindings;
|
|
self.func_defer_base = self.defer_stack.items.len;
|
|
self.block_terminated = false;
|
|
self.inline_return_target = null;
|
|
// Generic type-params inferred at the call site (e.g. `$R` from the
|
|
// mapper's closure return). Installed for the whole mono so
|
|
// return-type resolution and body lowering substitute them.
|
|
self.type_bindings = type_bindings.*;
|
|
defer {
|
|
self.type_bindings = saved_type_bindings;
|
|
self.scope = saved_scope;
|
|
self.func_defer_base = saved_defer_base;
|
|
self.block_terminated = saved_block_terminated;
|
|
self.target_type = saved_target;
|
|
self.pack_arg_nodes = saved_pan;
|
|
self.pack_param_count = saved_ppc;
|
|
self.pack_arg_types = saved_pat;
|
|
self.pack_constraint = saved_pcon;
|
|
self.inline_return_target = saved_iri;
|
|
self.current_ctx_ref = saved_ctx_ref;
|
|
self.builder.func = saved_func;
|
|
self.builder.current_block = saved_block;
|
|
self.builder.inst_counter = saved_counter;
|
|
}
|
|
|
|
const wants_ctx = self.funcWantsImplicitCtx(fd);
|
|
|
|
// Synthesise pack-param names + AST ident nodes used to bind
|
|
// `args[<lit>]` substitutions during body lowering. Built
|
|
// BEFORE return-type resolution so the generic-`$R` path can
|
|
// pre-install the binding for type inference.
|
|
var pack_synth_names = std.ArrayList([]const u8).empty;
|
|
defer pack_synth_names.deinit(self.alloc);
|
|
var pack_arg_idents = std.ArrayList(*const Node).empty;
|
|
defer pack_arg_idents.deinit(self.alloc);
|
|
for (arg_types, 0..) |_, i| {
|
|
const synth_name = std.fmt.allocPrint(self.alloc, "__pack_{s}_{d}", .{ pack_name, i }) catch return;
|
|
pack_synth_names.append(self.alloc, synth_name) catch return;
|
|
const ident_node = self.alloc.create(Node) catch return;
|
|
ident_node.* = .{
|
|
.span = fd.body.span,
|
|
.data = .{ .identifier = .{ .name = synth_name } },
|
|
};
|
|
pack_arg_idents.append(self.alloc, ident_node) catch return;
|
|
}
|
|
|
|
// Resolve return type. When the declared type is a generic
|
|
// name (e.g. `(..$args) -> $R`), `resolveReturnType` would
|
|
// return an opaque struct TypeId and the mono's signature
|
|
// would be wrong. Pre-install the pack bindings + infer the
|
|
// ret type from the body's tail expression / first explicit
|
|
// `return X;` instead.
|
|
var pre_pan = std.StringHashMap([]const *const Node).init(self.alloc);
|
|
defer pre_pan.deinit();
|
|
pre_pan.put(pack_name, pack_arg_idents.items) catch return;
|
|
var pre_ppc = std.StringHashMap(u32).init(self.alloc);
|
|
defer pre_ppc.deinit();
|
|
pre_ppc.put(pack_name, @intCast(arg_types.len)) catch return;
|
|
var pre_pat = std.StringHashMap([]const TypeId).init(self.alloc);
|
|
defer pre_pat.deinit();
|
|
pre_pat.put(pack_name, arg_types) catch return;
|
|
var pre_pcon = std.StringHashMap([]const u8).init(self.alloc);
|
|
defer pre_pcon.deinit();
|
|
if (pack_proto) |proto| pre_pcon.put(pack_name, proto) catch return;
|
|
self.pack_arg_nodes = pre_pan;
|
|
self.pack_param_count = pre_ppc;
|
|
self.pack_arg_types = pre_pat;
|
|
self.pack_constraint = if (pack_proto != null) pre_pcon else null;
|
|
|
|
// Resolve the declared return + fixed-prefix param types in the pack fn's
|
|
// OWN module (E4), so a 2-flat-hop library type named in the signature is
|
|
// bare-visible — mirrors the body pin further down and the
|
|
// `monomorphizeFunction` pin. The comptime call-site args below are
|
|
// lowered AFTER this restore, in the caller's context (issue 0106).
|
|
const saved_sig_src = self.current_source_file;
|
|
if (fd.body.source_file) |src| self.setCurrentSourceFile(src);
|
|
|
|
const declared_is_generic_ret = blk: {
|
|
const rt = fd.return_type orelse break :blk false;
|
|
if (rt.data != .type_expr) break :blk false;
|
|
break :blk rt.data.type_expr.is_generic;
|
|
};
|
|
const ret_ty: TypeId = if (declared_is_generic_ret)
|
|
self.inferPackBodyReturnType(fd.body)
|
|
else
|
|
self.resolveReturnType(fd);
|
|
self.target_type = ret_ty;
|
|
|
|
// Param list: ctx (if needed) + fixed prefix + N pack params.
|
|
// Comptime non-pack params are NOT in the runtime signature —
|
|
// their values are folded into the mangle and substituted via
|
|
// `comptime_param_nodes` / bound as runtime locals in scope.
|
|
// NOT deinit'd — `params.items` is stored by reference in
|
|
// `Function.init` and read back later via `func.params`.
|
|
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 return;
|
|
}
|
|
for (fd.params, 0..) |p, i| {
|
|
if (i == pack_param_idx) continue;
|
|
if (p.is_comptime) continue; // folded into mangle, not in IR
|
|
const pty = self.resolveParamType(&p);
|
|
params.append(self.alloc, .{
|
|
.name = self.module.types.internString(p.name),
|
|
.ty = pty,
|
|
}) catch return;
|
|
}
|
|
for (arg_types, 0..) |ty, i| {
|
|
params.append(self.alloc, .{
|
|
.name = self.module.types.internString(pack_synth_names.items[i]),
|
|
.ty = ty,
|
|
}) catch return;
|
|
}
|
|
self.setCurrentSourceFile(saved_sig_src);
|
|
|
|
const name_id = self.module.types.internString(owned_name);
|
|
_ = self.builder.beginFunction(name_id, params.items, ret_ty);
|
|
self.builder.currentFunc().has_implicit_ctx = wants_ctx;
|
|
|
|
const entry_name = self.module.types.internString("entry");
|
|
const entry = self.builder.appendBlock(entry_name, &.{});
|
|
self.builder.switchToBlock(entry);
|
|
if (wants_ctx) self.current_ctx_ref = Ref.fromIndex(0);
|
|
|
|
var scope = Scope.init(self.alloc, null);
|
|
defer scope.deinit();
|
|
self.scope = &scope;
|
|
|
|
// Bind non-pack params. Walk fd.params + call_node.args
|
|
// together; comptime non-pack params bind both as runtime
|
|
// locals (so bare-name body access works) AND as
|
|
// comptime_param_nodes entries (so `#insert` substitution
|
|
// works). Non-comptime non-pack params consume IR param
|
|
// slots in order.
|
|
var cpn = std.StringHashMap(*const Node).init(self.alloc);
|
|
defer cpn.deinit();
|
|
var param_idx: u32 = if (wants_ctx) 1 else 0;
|
|
var ct_arg_idx: usize = 0;
|
|
for (fd.params, 0..) |p, i| {
|
|
if (i == pack_param_idx) break;
|
|
if (p.is_comptime) {
|
|
if (ct_arg_idx < call_node.args.len) {
|
|
const call_arg = call_node.args[ct_arg_idx];
|
|
self.stampCallerSource(call_arg);
|
|
cpn.put(p.name, call_arg) catch return;
|
|
// Bind as a runtime local for bare-name access.
|
|
// Lower the call arg as a value, then alloca + store.
|
|
const val = self.lowerExpr(call_arg);
|
|
const val_ty = self.builder.getRefType(val);
|
|
const slot = self.builder.alloca(val_ty);
|
|
self.builder.store(slot, val);
|
|
scope.put(p.name, .{ .ref = slot, .ty = val_ty, .is_alloca = true });
|
|
}
|
|
ct_arg_idx += 1;
|
|
continue;
|
|
}
|
|
// Pin to the pack fn's OWN module (E4): a fixed-prefix param whose
|
|
// type is bare-visible only in the defining module must resolve
|
|
// there, not in the caller's restored context. Mirrors the
|
|
// signature build above and `resolveParamTypeInSource` at the
|
|
// cross-module call-arg typing sites.
|
|
const pty = self.resolveParamTypeInSource(fd.body.source_file, &p);
|
|
const slot = self.builder.alloca(pty);
|
|
self.builder.store(slot, Ref.fromIndex(param_idx));
|
|
scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true });
|
|
param_idx += 1;
|
|
ct_arg_idx += 1;
|
|
}
|
|
// Install comptime_param_nodes for the body lowering.
|
|
const saved_cpn = self.comptime_param_nodes;
|
|
self.comptime_param_nodes = cpn;
|
|
defer self.comptime_param_nodes = saved_cpn;
|
|
var pack_param_slots = std.ArrayList(Ref).empty;
|
|
defer pack_param_slots.deinit(self.alloc);
|
|
for (arg_types, 0..) |ty, i| {
|
|
const synth_name = pack_synth_names.items[i];
|
|
const slot = self.builder.alloca(ty);
|
|
self.builder.store(slot, Ref.fromIndex(param_idx));
|
|
scope.put(synth_name, .{ .ref = slot, .ty = ty, .is_alloca = true });
|
|
pack_param_slots.append(self.alloc, slot) catch return;
|
|
param_idx += 1;
|
|
}
|
|
|
|
// Pack bindings remain installed from the pre-resolution
|
|
// (generic-`$R`) inference step above. No need to reinstall.
|
|
|
|
// Materialise an `[]Any` slice value for the pack name so
|
|
// bare `args` (forwarding) and `args[<runtime_int>]` (loops)
|
|
// resolve at runtime. Per-position type info is lost via
|
|
// Any boxing — that's the inherent cost of treating a
|
|
// heterogeneous pack as a uniform value. Literal-indexed
|
|
// access still goes through `packArgNodeAt` and keeps the
|
|
// concrete per-position types.
|
|
self.materialisePackSlice(&scope, pack_name, pack_param_slots.items, arg_types);
|
|
|
|
// Pin to the metaprogram's OWN module for the BODY lowering only, so its
|
|
// bare names (and anything it `#insert`s — e.g. `build_format` / `out` /
|
|
// `emit` inside `std.print`) resolve in the defining module's visibility
|
|
// context, not the call site's (issue 0106). The comptime-param call-site
|
|
// args above were deliberately lowered FIRST, in the caller's context.
|
|
// Mirrors `lowerFunctionBodyInto`, which switches to `func.source_file`;
|
|
// the defining path is stamped on the body node by `resolveImports`. A
|
|
// synthesized/sourceless body keeps the caller's context.
|
|
const saved_source = self.current_source_file;
|
|
defer self.setCurrentSourceFile(saved_source);
|
|
if (fd.body.source_file) |src| self.setCurrentSourceFile(src);
|
|
|
|
if (ret_ty != .void) {
|
|
const body_val = self.lowerBlockValue(fd.body);
|
|
if (!self.currentBlockHasTerminator()) {
|
|
if (body_val) |val| {
|
|
const val_ty = self.builder.getRefType(val);
|
|
const coerced = if (val_ty != .void) self.coerceToType(val, val_ty, ret_ty) else val;
|
|
self.builder.ret(coerced, ret_ty);
|
|
} else {
|
|
self.ensureTerminator(ret_ty);
|
|
}
|
|
}
|
|
} else {
|
|
self.lowerBlock(fd.body);
|
|
self.ensureTerminator(ret_ty);
|
|
}
|
|
self.builder.finalize();
|
|
}
|
|
|
|
fn monomorphizeFunction(self: *Lowering, fd: *const ast.FnDecl, mangled_name: []const u8, bindings: *std.StringHashMap(TypeId)) void {
|
|
// Mark as lowered before lowering (prevents infinite recursion)
|
|
// Need to dupe the name since mangled_name may be stack-allocated
|
|
const owned_name = self.alloc.dupe(u8, mangled_name) catch return;
|
|
self.lowered_functions.put(owned_name, {}) catch {};
|
|
|
|
// Save builder state
|
|
const saved_func = self.builder.func;
|
|
const saved_block = self.builder.current_block;
|
|
const saved_counter = self.builder.inst_counter;
|
|
const saved_scope = self.scope;
|
|
const saved_bindings = self.type_bindings;
|
|
const saved_defer_base = self.func_defer_base;
|
|
const saved_block_terminated = self.block_terminated;
|
|
const saved_target = self.target_type;
|
|
// Pack-fn mono state is lexical to the pack-fn body. A generic
|
|
// function called from inside a pack-fn mono (e.g.
|
|
// `build(args: []Type, $ret: Type)` invoked from
|
|
// `probe(..$args) { build($args, void) }`) must not inherit the
|
|
// caller's pack maps — `lowerFieldAccess`'s `<pack_name>.len`
|
|
// intercept would otherwise constant-fold the callee's
|
|
// same-named param to whichever shape triggered the first mono
|
|
// and bake the wrong arity into the cached IR. Same shape of
|
|
// fix as `lazyLowerFunction` (issue-0048, commit 0ede097).
|
|
const saved_pan = self.pack_arg_nodes;
|
|
const saved_ppc = self.pack_param_count;
|
|
const saved_pat = self.pack_arg_types;
|
|
const saved_iri = self.inline_return_target;
|
|
self.pack_arg_nodes = null;
|
|
self.pack_param_count = null;
|
|
self.pack_arg_types = null;
|
|
self.inline_return_target = null;
|
|
defer {
|
|
self.pack_arg_nodes = saved_pan;
|
|
self.pack_param_count = saved_ppc;
|
|
self.pack_arg_types = saved_pat;
|
|
self.inline_return_target = saved_iri;
|
|
}
|
|
self.func_defer_base = self.defer_stack.items.len;
|
|
self.block_terminated = false;
|
|
|
|
// Install type bindings
|
|
self.type_bindings = bindings.*;
|
|
|
|
// Pin to the template's defining module for the whole monomorphization
|
|
// (return type, param types, body), so a library-internal bare TYPE ref
|
|
// — e.g. `List(T).append`'s `alloc: Allocator` default-param type, or a
|
|
// body reference to a type visible only in the template's module —
|
|
// resolves where it is visible, not at the (possibly cross-module) call
|
|
// site. This is the issue-0100-F1 plain-fn pin extended to generic
|
|
// instantiation; without it the non-transitive bare-TYPE gate (E4) would
|
|
// reject a 2-flat-hop library type the call site cannot see directly.
|
|
// A synthesized / sourceless body keeps the caller's context.
|
|
const saved_source_mono = self.current_source_file;
|
|
defer self.setCurrentSourceFile(saved_source_mono);
|
|
if (fd.body.source_file) |src| self.setCurrentSourceFile(src);
|
|
|
|
// Resolve return type with type bindings active. The body's tail
|
|
// expression inherits this as its target_type so bare `.{...}`
|
|
// literals resolve to the monomorphised return type instead of
|
|
// whatever leaked in from the caller (e.g. caller's xx target).
|
|
const ret_ty = self.resolveReturnType(fd);
|
|
self.target_type = ret_ty;
|
|
|
|
const wants_ctx = self.funcWantsImplicitCtx(fd);
|
|
const saved_ctx_ref_mono = self.current_ctx_ref;
|
|
defer self.current_ctx_ref = saved_ctx_ref_mono;
|
|
|
|
// Build param list (substituting type params, skipping type param declarations).
|
|
// Prepend `__sx_ctx: *void` at slot 0 if the function gets the implicit param.
|
|
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 (fd.params) |p| {
|
|
if (isTypeParamDecl(&p, fd.type_params)) continue;
|
|
const pty = self.resolveParamType(&p);
|
|
params.append(self.alloc, .{
|
|
.name = self.module.types.internString(p.name),
|
|
.ty = pty,
|
|
}) catch unreachable;
|
|
}
|
|
|
|
// Create the monomorphized function
|
|
const name_id = self.module.types.internString(owned_name);
|
|
const func_id = self.builder.beginFunction(name_id, params.items, ret_ty);
|
|
_ = func_id;
|
|
self.builder.currentFunc().has_implicit_ctx = wants_ctx;
|
|
|
|
// Create entry block
|
|
const entry_name = self.module.types.internString("entry");
|
|
const entry = self.builder.appendBlock(entry_name, &.{});
|
|
self.builder.switchToBlock(entry);
|
|
if (wants_ctx) self.current_ctx_ref = Ref.fromIndex(0);
|
|
|
|
// Create scope and bind params
|
|
var scope = Scope.init(self.alloc, null);
|
|
defer scope.deinit();
|
|
self.scope = &scope;
|
|
|
|
{
|
|
var param_idx: u32 = if (wants_ctx) 1 else 0;
|
|
for (fd.params) |p| {
|
|
if (isTypeParamDecl(&p, fd.type_params)) continue;
|
|
const pty = self.resolveParamType(&p);
|
|
const slot = self.builder.alloca(pty);
|
|
const param_ref = Ref.fromIndex(param_idx);
|
|
self.builder.store(slot, param_ref);
|
|
scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true });
|
|
param_idx += 1;
|
|
}
|
|
}
|
|
|
|
// Handle builtin function bodies (e.g. #builtin sqrt monomorphized to sqrt__f32)
|
|
if (fd.body.data == .builtin_expr) {
|
|
// Emit builtin call with param 0, then return
|
|
if (resolveBuiltin(fd.name)) |bid| {
|
|
const param0 = Ref.fromIndex(0);
|
|
const result = self.builder.callBuiltin(bid, &.{param0}, ret_ty);
|
|
self.builder.ret(result, ret_ty);
|
|
} else {
|
|
self.ensureTerminator(ret_ty);
|
|
}
|
|
self.builder.finalize();
|
|
} else {
|
|
// Lower the function body
|
|
if (ret_ty != .void) {
|
|
const body_val = self.lowerBlockValue(fd.body);
|
|
if (!self.currentBlockHasTerminator()) {
|
|
if (body_val) |val| {
|
|
const val_ty = self.builder.getRefType(val);
|
|
const coerced = if (val_ty != .void) self.coerceToType(val, val_ty, ret_ty) else val;
|
|
self.builder.ret(coerced, ret_ty);
|
|
} else {
|
|
self.ensureTerminator(ret_ty);
|
|
}
|
|
}
|
|
} else {
|
|
self.lowerBlock(fd.body);
|
|
self.ensureTerminator(ret_ty);
|
|
}
|
|
self.builder.finalize();
|
|
}
|
|
|
|
// Restore builder state
|
|
self.type_bindings = saved_bindings;
|
|
self.scope = saved_scope;
|
|
self.func_defer_base = saved_defer_base;
|
|
self.block_terminated = saved_block_terminated;
|
|
self.target_type = saved_target;
|
|
self.builder.func = saved_func;
|
|
self.builder.current_block = saved_block;
|
|
self.builder.inst_counter = saved_counter;
|
|
}
|
|
|
|
// ── Reflection builtins ────────────────────────────────────────
|
|
|
|
/// Try to lower a call as a reflection builtin (expanded inline during lowering).
|
|
/// Returns null if the call is not a recognized reflection builtin.
|
|
fn tryLowerReflectionCall(self: *Lowering, name: []const u8, c: *const ast.Call) ?Ref {
|
|
// Strict `$T: Type` guard for the type-introspection builtins. A
|
|
// value argument (`6`, `true`, `5.2`, a struct) is rejected with a
|
|
// diagnostic instead of being silently reinterpreted as a TypeId
|
|
// index / sized via its `typeof` (issue 0090). One shared
|
|
// classification covers all 7; it runs before dispatch.
|
|
if (self.reflectionTypeArgGuard(name, c)) |sentinel| return sentinel;
|
|
|
|
if (std.mem.eql(u8, name, "size_of")) {
|
|
// size_of(T) → const_int(sizeof(T))
|
|
const ty = self.resolveTypeArg(c.args[0]);
|
|
const size: i64 = @intCast(self.typeSizeBytes(ty));
|
|
return self.builder.constInt(size, .s64);
|
|
}
|
|
if (std.mem.eql(u8, name, "align_of")) {
|
|
const ty = self.resolveTypeArg(c.args[0]);
|
|
const a: i64 = @intCast(self.module.types.typeAlignBytes(ty));
|
|
return self.builder.constInt(a, .s64);
|
|
}
|
|
if (std.mem.eql(u8, name, "field_count")) {
|
|
// field_count(T) → const_int(N)
|
|
const ty = self.resolveTypeArg(c.args[0]);
|
|
const info = self.module.types.get(ty);
|
|
const count: i64 = switch (info) {
|
|
.@"struct" => |s| @intCast(s.fields.len),
|
|
.@"union" => |u| @intCast(u.fields.len),
|
|
.tagged_union => |u| @intCast(u.fields.len),
|
|
.@"enum" => |e| @intCast(e.variants.len),
|
|
.array => |a| @intCast(a.length),
|
|
.vector => |v| @intCast(v.length),
|
|
else => 0,
|
|
};
|
|
return self.builder.constInt(count, .s64);
|
|
}
|
|
if (std.mem.eql(u8, name, "type_name")) {
|
|
// type_name(T):
|
|
// - Statically resolvable arg (type expression, pack
|
|
// index, generic binding, etc.) → fold to const_string
|
|
// at lower time.
|
|
// - Dynamic arg (e.g. `list[i]` indexing into a
|
|
// `$args`-derived []Type slice) → emit a
|
|
// `callBuiltin(.type_name, [arg_ref])`. The interp's
|
|
// arm (commit 9600ba5) reads the runtime `.type_tag`
|
|
// and returns the per-position name. Without this
|
|
// split, the catch-all `else => .s64` in
|
|
// `resolveTypeArg` silently returns "s64" for every
|
|
// dynamic call — exactly the silent-arm pattern the
|
|
// project's REJECTED PATTERNS forbid.
|
|
if (self.isStaticTypeArg(c.args[0])) {
|
|
const ty = self.resolveTypeArg(c.args[0]);
|
|
const tn_str = self.formatTypeName(ty);
|
|
const sid = self.module.types.internString(tn_str);
|
|
return self.builder.constString(sid);
|
|
}
|
|
const arg_ref = self.lowerExpr(c.args[0]);
|
|
const args_owned = self.alloc.dupe(Ref, &.{arg_ref}) catch return self.builder.constString(self.module.types.internString(""));
|
|
return self.builder.callBuiltin(.type_name, args_owned, .string);
|
|
}
|
|
if (std.mem.eql(u8, name, "type_eq")) {
|
|
// type_eq(T1, T2) → const_bool — comptime TypeId equality.
|
|
// TypeIds are interned per structural shape so equality on
|
|
// them matches the user's intuition: `type_eq(s64, s64)` is
|
|
// true, `type_eq(*s64, *s64)` is true, distinct shapes are
|
|
// false. Pack-indexed types (`$args[0]`) resolve through
|
|
// `resolveTypeArg` → `resolveTypeWithBindings`.
|
|
if (c.args.len < 2) return self.builder.constBool(false);
|
|
const a = self.resolveTypeArg(c.args[0]);
|
|
const b = self.resolveTypeArg(c.args[1]);
|
|
return self.builder.constBool(a == b);
|
|
}
|
|
if (std.mem.eql(u8, name, "type_is_unsigned")) {
|
|
// type_is_unsigned(T) → bool. Static arg (a spelled type or
|
|
// generic binding) folds to const_bool at lower time. A
|
|
// dynamic arg — the runtime `type_of(x)` value queried by
|
|
// `any_to_string` — emits a `callBuiltin`: the interp reads
|
|
// the boxed TypeId, LLVM GEPs a per-type signedness table.
|
|
// Mirrors `type_name`'s static/dynamic split; the same split
|
|
// avoids `resolveTypeArg`'s silent `.s64` default lying about
|
|
// a runtime Type value.
|
|
if (c.args.len < 1) return self.builder.constBool(false);
|
|
if (self.isStaticTypeArg(c.args[0])) {
|
|
const ty = self.resolveTypeArg(c.args[0]);
|
|
return self.builder.constBool(self.module.types.isUnsignedInt(ty));
|
|
}
|
|
const arg_ref = self.lowerExpr(c.args[0]);
|
|
const args_owned = self.alloc.dupe(Ref, &.{arg_ref}) catch return self.builder.constBool(false);
|
|
return self.builder.callBuiltin(.type_is_unsigned, args_owned, .bool);
|
|
}
|
|
if (std.mem.eql(u8, name, "has_impl")) {
|
|
// has_impl(P, T) → const_bool. Returns true when type T has
|
|
// a reachable impl for protocol P. P is either:
|
|
// - plain protocol name (`Hash`, `Eq`) for unary protocols;
|
|
// - parameterised call like `Into(Block)` — for protocols
|
|
// with type args, the args must be fully spelled.
|
|
// Delegates to `computeHasImpl` (shared with the
|
|
// `tryConstBoolCondition` arm so `inline if has_impl(...)`
|
|
// folds at compile time).
|
|
if (c.args.len < 2) return self.builder.constBool(false);
|
|
const ty = self.resolveTypeArg(c.args[1]);
|
|
return self.builder.constBool(self.computeHasImpl(c.args[0], ty));
|
|
}
|
|
if (std.mem.eql(u8, name, "is_flags")) {
|
|
const ty = self.resolveTypeArg(c.args[0]);
|
|
if (!ty.isBuiltin()) {
|
|
const info = self.module.types.get(ty);
|
|
if (info == .@"enum") return self.builder.constBool(info.@"enum".is_flags);
|
|
}
|
|
return self.builder.constBool(false);
|
|
}
|
|
if (std.mem.eql(u8, name, "compile_error")) {
|
|
// compile_error(msg) — raise a build-time diagnostic at
|
|
// the call site. The argument must be a string literal so
|
|
// the message text is available at lower time. Returns a
|
|
// void-typed const (the call site is consumed for its
|
|
// side effect, not its value).
|
|
if (self.diagnostics) |diags| {
|
|
if (c.args.len < 1) {
|
|
diags.addFmt(.err, c.callee.span, "compile_error requires a string argument", .{});
|
|
} else if (c.args[0].data == .string_literal) {
|
|
const lit = c.args[0].data.string_literal;
|
|
const msg = if (lit.is_raw)
|
|
lit.raw
|
|
else
|
|
unescape.unescapeString(self.alloc, lit.raw) catch lit.raw;
|
|
diags.addFmt(.err, c.callee.span, "{s}", .{msg});
|
|
} else {
|
|
diags.addFmt(.err, c.callee.span, "compile_error argument must be a string literal", .{});
|
|
}
|
|
}
|
|
return self.builder.constInt(0, .void);
|
|
}
|
|
if (std.mem.eql(u8, name, "field_name")) {
|
|
// field_name(T, i) → field_name_get instruction
|
|
if (c.args.len < 2) return self.builder.constString(self.module.types.internString(""));
|
|
const ty = self.resolveTypeArg(c.args[0]);
|
|
const idx = self.lowerExpr(c.args[1]);
|
|
return self.builder.emit(.{ .field_name_get = .{
|
|
.base = .none,
|
|
.index = idx,
|
|
.struct_type = ty,
|
|
} }, .string);
|
|
}
|
|
if (std.mem.eql(u8, name, "is_comptime")) {
|
|
// True under the comptime interpreter, false in compiled code — the
|
|
// op decides per backend (it can't fold here, since the same IR
|
|
// serves both). Lets stdlib gate a comptime-only diagnostic branch.
|
|
return self.builder.emit(.{ .is_comptime = {} }, .bool);
|
|
}
|
|
if (std.mem.eql(u8, name, "__interp_print_frames")) {
|
|
// Backs `trace.print_interpreter_frames()`: dumps the interp call
|
|
// chain at comptime, no-op in compiled code (ERR E4.1).
|
|
return self.builder.emit(.{ .interp_print_frames = {} }, .void);
|
|
}
|
|
if (std.mem.eql(u8, name, "__trace_resolve_frame")) {
|
|
// Backs `trace.sx`'s formatter: a raw trace-buffer u64 → a `TraceFrame`.
|
|
// Compiled code reinterprets the operand as `*TraceFrame` and loads it;
|
|
// the interp unpacks (func_id, span.start) and resolves (ERR E3.0
|
|
// slice 3b). Result type is the `TraceFrame` struct from trace.sx.
|
|
const frame_ty = self.module.types.findByName(self.module.types.internString("TraceFrame")) orelse {
|
|
if (self.diagnostics) |d| d.addFmt(.err, null, "`__trace_resolve_frame` needs `TraceFrame` (from trace.sx) in scope", .{});
|
|
return self.builder.constInt(0, .void);
|
|
};
|
|
const arg = self.lowerExpr(c.args[0]);
|
|
return self.builder.emit(.{ .trace_resolve = .{ .operand = arg } }, frame_ty);
|
|
}
|
|
if (std.mem.eql(u8, name, "error_tag_name")) {
|
|
// error_tag_name(e) → look the error-set value's runtime tag id up
|
|
// in the always-linked tag-name table. The value IS its u32 tag id.
|
|
if (c.args.len < 1) return self.builder.constString(self.module.types.internString(""));
|
|
const e = self.lowerExpr(c.args[0]);
|
|
return self.builder.emit(.{ .error_tag_name_get = .{ .operand = e } }, .string);
|
|
}
|
|
if (std.mem.eql(u8, name, "field_value")) {
|
|
// field_value(s, i) → field_value_get instruction (structs/unions)
|
|
// → index_get + box_any (slices/arrays)
|
|
if (c.args.len < 2) return self.builder.constInt(0, .any);
|
|
const base = self.lowerExpr(c.args[0]);
|
|
const idx = self.lowerExpr(c.args[1]);
|
|
const struct_ty = self.inferExprType(c.args[0]);
|
|
|
|
// For slices, arrays, and vectors, use index_get to access elements
|
|
if (!struct_ty.isBuiltin()) {
|
|
const ti = self.module.types.get(struct_ty);
|
|
if (ti == .slice or ti == .array or ti == .vector) {
|
|
const elem_ty = self.getElementType(struct_ty);
|
|
const elem = self.builder.emit(.{ .index_get = .{ .lhs = base, .rhs = idx } }, elem_ty);
|
|
return self.builder.boxAny(elem, elem_ty);
|
|
}
|
|
}
|
|
|
|
return self.builder.emit(.{ .field_value_get = .{
|
|
.base = base,
|
|
.index = idx,
|
|
.struct_type = struct_ty,
|
|
} }, .any);
|
|
}
|
|
if (std.mem.eql(u8, name, "type_of")) {
|
|
// type_of(val) — produce a Type value (.any-typed aggregate).
|
|
if (c.args.len < 1) return self.builder.constType(.void);
|
|
const arg_ty = self.inferExprType(c.args[0]);
|
|
if (arg_ty == .any) {
|
|
// Runtime: extract tag, rebuild Any with `{.any, tag}` so
|
|
// the returned value carries Type semantics (tag field
|
|
// says ".any" → the value field holds the type id).
|
|
const val = self.lowerExpr(c.args[0]);
|
|
const tag_val = self.builder.structGet(val, 0, .s64);
|
|
return self.builder.boxAny(tag_val, .any);
|
|
} else {
|
|
return self.builder.constType(arg_ty);
|
|
}
|
|
}
|
|
if (std.mem.eql(u8, name, "field_index")) {
|
|
// field_index(T, val) → extract tag from tagged union
|
|
if (c.args.len < 2) return self.builder.constInt(0, .s64);
|
|
const val = self.lowerExpr(c.args[1]);
|
|
// For tagged unions: extract field 0 (the tag)
|
|
return self.builder.emit(.{ .enum_tag = .{ .operand = val } }, .s64);
|
|
}
|
|
if (std.mem.eql(u8, name, "field_value_int")) {
|
|
// field_value_int(T, i) → lookup enum variant value by index
|
|
if (c.args.len < 2) return self.builder.constInt(0, .s64);
|
|
const ty = self.resolveTypeArg(c.args[0]);
|
|
const idx = self.lowerExpr(c.args[1]);
|
|
// For enums with explicit values, build a global value array and index into it
|
|
if (!ty.isBuiltin()) {
|
|
const ti = self.module.types.get(ty);
|
|
if (ti == .@"enum") {
|
|
if (ti.@"enum".explicit_values) |vals| {
|
|
// Build inline switch: for each index, return the explicit value
|
|
// Simple approach: build an array of constants and use index_get
|
|
var elems = std.ArrayList(Ref).empty;
|
|
defer elems.deinit(self.alloc);
|
|
for (vals) |v| {
|
|
elems.append(self.alloc, self.builder.constInt(v, .s64)) catch unreachable;
|
|
}
|
|
const arr_ty = self.module.types.arrayOf(.s64, @intCast(vals.len));
|
|
const arr = self.builder.structInit(elems.items, arr_ty);
|
|
return self.builder.emit(.{ .index_get = .{ .lhs = arr, .rhs = idx } }, .s64);
|
|
}
|
|
}
|
|
}
|
|
// Default: return the index itself (regular enums)
|
|
return idx;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Strict `$T: Type` classification shared by the 7 type-introspection
|
|
/// builtins. An argument denotes a type iff it is a spelled /
|
|
/// compile-time type or generic type parameter (the `isStaticTypeArg`
|
|
/// shapes), or a runtime `Type` value — which is `.any`-typed at
|
|
/// runtime (`type_of(x)`, a `[]Type` element `list[i]`, a `Type`-typed
|
|
/// local / field / param). Any other expression — a value of type
|
|
/// s64 / f64 / bool / a struct — is NOT a type.
|
|
pub fn reflectionArgIsType(self: *Lowering, arg: *const Node) bool {
|
|
if (self.isStaticTypeArg(arg)) return true;
|
|
return self.inferExprType(arg) == .any;
|
|
}
|
|
|
|
/// Guard for the type-introspection builtins (`size_of`, `align_of`,
|
|
/// `field_count`, `type_name`, `type_eq`, `type_is_unsigned`,
|
|
/// `is_flags`): every argument must denote a type. A value argument is
|
|
/// rejected with a diagnostic rather than silently reinterpreted as a
|
|
/// TypeId index or sized via its `typeof` (issue 0090).
|
|
///
|
|
/// Returns null when `name` is not a guarded builtin OR every argument
|
|
/// is a type (→ fall through to normal dispatch). Returns a harmless
|
|
/// result-typed sentinel Ref when a violation was diagnosed; the
|
|
/// emitted `.err` gates the build so the value is never observed.
|
|
fn reflectionTypeArgGuard(self: *Lowering, name: []const u8, c: *const ast.Call) ?Ref {
|
|
const arity: usize = if (std.mem.eql(u8, name, "type_eq"))
|
|
2
|
|
else if (std.mem.eql(u8, name, "size_of") or
|
|
std.mem.eql(u8, name, "align_of") or
|
|
std.mem.eql(u8, name, "field_count") or
|
|
std.mem.eql(u8, name, "type_name") or
|
|
std.mem.eql(u8, name, "type_is_unsigned") or
|
|
std.mem.eql(u8, name, "is_flags"))
|
|
1
|
|
else
|
|
return null;
|
|
|
|
var ok = true;
|
|
if (c.args.len != arity) {
|
|
if (self.diagnostics) |d| {
|
|
d.addFmt(.err, c.callee.span, "{s} expects {d} type argument{s}, got {d}", .{
|
|
name, arity, if (arity == 1) @as([]const u8, "") else "s", c.args.len,
|
|
});
|
|
}
|
|
ok = false;
|
|
} else {
|
|
for (c.args) |a| {
|
|
if (self.reflectionArgIsType(a)) continue;
|
|
if (self.diagnostics) |d| {
|
|
d.addFmt(.err, a.span, "{s} expects a type, got '{s}'", .{
|
|
name, self.formatTypeName(self.inferExprType(a)),
|
|
});
|
|
}
|
|
ok = false;
|
|
}
|
|
}
|
|
if (ok) return null;
|
|
return self.reflectionErrorSentinel(name);
|
|
}
|
|
|
|
/// Result-typed placeholder returned after `reflectionTypeArgGuard`
|
|
/// diagnoses a non-type argument: a string for `type_name`, a bool for
|
|
/// the predicate builtins, an int for the size / count builtins. Never
|
|
/// observed at runtime — the diagnostic already fails the build — but
|
|
/// keeps the IR well-typed so lowering can finish and report every
|
|
/// error in one pass.
|
|
fn reflectionErrorSentinel(self: *Lowering, name: []const u8) Ref {
|
|
if (std.mem.eql(u8, name, "type_name"))
|
|
return self.builder.constString(self.module.types.internString(""));
|
|
if (std.mem.eql(u8, name, "type_eq") or
|
|
std.mem.eql(u8, name, "type_is_unsigned") or
|
|
std.mem.eql(u8, name, "is_flags"))
|
|
return self.builder.constBool(false);
|
|
return self.builder.constInt(0, .s64);
|
|
}
|
|
|
|
/// Resolve a type argument from a call expression. Handles:
|
|
/// - Type param bindings ($T → concrete type via type_bindings)
|
|
/// - Direct type names (Vec4 → lookup in TypeTable)
|
|
/// - type_expr AST nodes
|
|
/// True iff `node` matches an AST shape that `resolveTypeArg`
|
|
/// can resolve to a concrete TypeId without falling through to
|
|
/// the silent `.s64` default. Used by `tryLowerReflectionCall`
|
|
/// to split static-fold from dynamic-builtin-call paths.
|
|
///
|
|
/// Static-arg shapes mirror the explicit arms of `resolveTypeArg`:
|
|
/// - type_expr / identifier (type name or bound generic)
|
|
/// - pack_index_type_expr (`$pack[<lit>]`)
|
|
/// - compound type literals (pointer, array, slice, optional,
|
|
/// many_pointer, function_type_expr)
|
|
/// - parameterised type-constructor `call` (Vector, List, etc.)
|
|
/// - tuple_literal as a tuple TYPE
|
|
///
|
|
/// Dynamic shapes (index_expr, field_access, runtime locals,
|
|
/// etc.) fall to the alternative path that emits a builtin_call.
|
|
fn isStaticTypeArg(self: *Lowering, node: *const Node) bool {
|
|
switch (node.data) {
|
|
.type_expr => |te| {
|
|
// A type-keyword name (e.g. `s64`) is always static.
|
|
// A user-defined name that happens to be in scope as
|
|
// a runtime variable (`x: Type = s64; type_name(x)`)
|
|
// is NOT static — route through the dynamic builtin
|
|
// call so the runtime lookup table fires.
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(te.name) != null) return false;
|
|
}
|
|
return true;
|
|
},
|
|
.identifier => |id| {
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(id.name) != null) return false;
|
|
}
|
|
return true;
|
|
},
|
|
.pack_index_type_expr,
|
|
.pointer_type_expr,
|
|
.many_pointer_type_expr,
|
|
.array_type_expr,
|
|
.slice_type_expr,
|
|
.optional_type_expr,
|
|
.function_type_expr,
|
|
.tuple_literal,
|
|
.call,
|
|
=> return true,
|
|
else => return false,
|
|
}
|
|
}
|
|
|
|
/// True iff `node` is a Type-shaped expression that resolves to a
|
|
/// concrete TypeId at lower time WITHOUT being a runtime variable
|
|
/// reference. Differs from `isStaticTypeArg` in that we exclude
|
|
/// identifiers that are in scope as runtime locals/globals — those
|
|
/// are runtime Type values (e.g. `t: Type = f64`) and the
|
|
/// comparison fold can't statically resolve them.
|
|
fn isStaticTypeRef(self: *Lowering, node: *const Node) bool {
|
|
switch (node.data) {
|
|
.type_expr => |te| {
|
|
// Compound type names (`s64`, `Point`, `Vec4`) resolve
|
|
// statically. If the name is also a runtime var in
|
|
// scope, it's a value reference, not a type ref.
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(te.name) != null) return false;
|
|
}
|
|
return self.isKnownTypeName(te.name) or
|
|
self.module.types.findByName(self.module.types.internString(te.name)) != null or
|
|
self.program_index.type_alias_map.get(te.name) != null;
|
|
},
|
|
.identifier => |id| {
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(id.name) != null) return false;
|
|
}
|
|
return self.isKnownTypeName(id.name) or
|
|
self.module.types.findByName(self.module.types.internString(id.name)) != null or
|
|
self.program_index.type_alias_map.get(id.name) != null;
|
|
},
|
|
.pointer_type_expr,
|
|
.many_pointer_type_expr,
|
|
.array_type_expr,
|
|
.slice_type_expr,
|
|
.optional_type_expr,
|
|
.function_type_expr,
|
|
.pack_index_type_expr,
|
|
=> return true,
|
|
.call => |cl| {
|
|
// `type_of(x)` resolves statically when `x`'s type is
|
|
// known — which it always is for a typed expression.
|
|
if (cl.callee.data == .identifier and
|
|
std.mem.eql(u8, cl.callee.data.identifier.name, "type_of") and
|
|
cl.args.len == 1)
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
else => return false,
|
|
}
|
|
}
|
|
|
|
/// Resolve a tuple LITERAL used in a type position (`(s32, s32)` reinterpreted
|
|
/// as a tuple type at a type-demanding site such as `size_of`). Every element
|
|
/// must itself denote a type; a non-type element — e.g. the `1` in
|
|
/// `(s32, 1)` — is a user error. Emit a diagnostic pointing at the offending
|
|
/// element and return `.unresolved`; never fabricate a tuple with a bogus
|
|
/// field (issue 0067). type_bridge.resolveAstType builds the tuple only after
|
|
/// this validation passes.
|
|
fn resolveTupleLiteralTypeArg(self: *Lowering, node: *const Node) TypeId {
|
|
for (node.data.tuple_literal.elements) |el| {
|
|
if (!type_bridge.isTypeShapedAstNode(el.value, &self.module.types)) {
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, el.value.span, "tuple type element is not a type (found `{s}`); a tuple used as a type must list only types, e.g. `(s32, s32)`", .{@tagName(el.value.data)});
|
|
}
|
|
return .unresolved;
|
|
}
|
|
}
|
|
return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
|
}
|
|
|
|
pub fn resolveTypeArg(self: *Lowering, node: *const Node) TypeId {
|
|
// Pack-index access in a type-arg slot (e.g. `type_name($args[0])`
|
|
// or `type_eq($args[i], s64)`). Same shape as the
|
|
// `resolveTypeWithBindings` arm — looks up the bound pack types
|
|
// and returns the i-th. OOB and no-active-binding emit focused
|
|
// diagnostics rather than silently defaulting to .s64 (the
|
|
// catch-all `else` below) — that fall-through is exactly the
|
|
// "silent unimplemented arm" the project's REJECTED PATTERNS
|
|
// forbid.
|
|
if (node.data == .pack_index_type_expr) {
|
|
const pi = node.data.pack_index_type_expr;
|
|
if (self.pack_arg_types) |pat| {
|
|
if (pat.get(pi.pack_name)) |arg_tys| {
|
|
if (pi.index < arg_tys.len) return arg_tys[pi.index];
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, node.span, "pack-index ${s}[{}] out of bounds: '{s}' has {} element{s}", .{
|
|
pi.pack_name, pi.index, pi.pack_name, arg_tys.len,
|
|
if (arg_tys.len == 1) @as([]const u8, "") else @as([]const u8, "s"),
|
|
});
|
|
}
|
|
return .unresolved;
|
|
}
|
|
}
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, node.span, "pack-index ${s}[{}] used outside an active pack binding", .{
|
|
pi.pack_name, pi.index,
|
|
});
|
|
}
|
|
return .unresolved;
|
|
}
|
|
// Bare `$<name>` in a type-arg position. Single-type generic
|
|
// bindings (`$R: Type` in `Closure(..$args) -> $R`) live in
|
|
// `type_bindings`; if the name is bound there, return the
|
|
// bound TypeId directly. Pack bindings would otherwise resolve
|
|
// to a slice value, not a single Type — the caller (e.g.
|
|
// `type_name(...)`) expects a single arg.
|
|
if (node.data == .comptime_pack_ref) {
|
|
const cpr = node.data.comptime_pack_ref;
|
|
if (self.type_bindings) |tb| {
|
|
if (tb.get(cpr.pack_name)) |ty| return ty;
|
|
}
|
|
}
|
|
switch (node.data) {
|
|
.identifier => |id| {
|
|
// Check type bindings first (from generic monomorphization)
|
|
if (self.type_bindings) |tb| {
|
|
if (tb.get(id.name)) |ty| return ty;
|
|
}
|
|
if (self.program_index.type_alias_map.get(id.name)) |alias_ty| return alias_ty;
|
|
const name_id = self.module.types.internString(id.name);
|
|
if (self.module.types.findByName(name_id)) |t| return t;
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, node.span, "unresolved type: '{s}'", .{id.name});
|
|
}
|
|
return .unresolved;
|
|
},
|
|
.type_expr => |te| {
|
|
if (self.program_index.type_alias_map.get(te.name)) |alias_ty| return alias_ty;
|
|
return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
|
},
|
|
.call => |cl| {
|
|
// `type_of(x)` resolves to `inferExprType(x)` at lower
|
|
// time when `x`'s type is statically known (which it
|
|
// is for any expression — type inference always
|
|
// produces a concrete TypeId). Lets
|
|
// `type_of(a) == s64` fold the same as
|
|
// `inferExprType(a) == s64`.
|
|
if (cl.callee.data == .identifier and
|
|
std.mem.eql(u8, cl.callee.data.identifier.name, "type_of") and
|
|
cl.args.len == 1)
|
|
{
|
|
return self.inferExprType(cl.args[0]);
|
|
}
|
|
// Handle type constructor calls: size_of(Sx(f32)), size_of(Complex(u32))
|
|
return self.resolveTypeCallWithBindings(&cl);
|
|
},
|
|
.tuple_literal => return self.resolveTupleLiteralTypeArg(node),
|
|
.pointer_type_expr,
|
|
.many_pointer_type_expr,
|
|
.array_type_expr,
|
|
.slice_type_expr,
|
|
.optional_type_expr,
|
|
.function_type_expr,
|
|
=> return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map),
|
|
else => return .unresolved,
|
|
}
|
|
}
|
|
|
|
/// Format a type name for display (e.g. "*Point", "[]s32", "[3]f64").
|
|
pub fn formatTypeName(self: *Lowering, ty: TypeId) []const u8 {
|
|
// Builtin types: use their canonical name
|
|
if (ty == .s8) return "s8";
|
|
if (ty == .s16) return "s16";
|
|
if (ty == .s32) return "s32";
|
|
if (ty == .s64) return "s64";
|
|
if (ty == .u8) return "u8";
|
|
if (ty == .u16) return "u16";
|
|
if (ty == .u32) return "u32";
|
|
if (ty == .u64) return "u64";
|
|
if (ty == .f32) return "f32";
|
|
if (ty == .f64) return "f64";
|
|
if (ty == .bool) return "bool";
|
|
if (ty == .void) return "void";
|
|
if (ty == .string) return "string";
|
|
if (ty == .any) return "Any";
|
|
if (ty == .usize) return "usize";
|
|
if (ty == .isize) return "isize";
|
|
|
|
const info = self.module.types.get(ty);
|
|
return switch (info) {
|
|
.@"struct" => |s| self.module.types.getString(s.name),
|
|
.@"union" => |u| self.module.types.getString(u.name),
|
|
.tagged_union => |u| self.module.types.getString(u.name),
|
|
.@"enum" => |e| self.module.types.getString(e.name),
|
|
.pointer => |p| blk: {
|
|
const inner = self.formatTypeName(p.pointee);
|
|
break :blk std.fmt.allocPrint(self.alloc, "*{s}", .{inner}) catch "pointer";
|
|
},
|
|
.many_pointer => |p| blk: {
|
|
const inner = self.formatTypeName(p.element);
|
|
break :blk std.fmt.allocPrint(self.alloc, "[*]{s}", .{inner}) catch "many_pointer";
|
|
},
|
|
.slice => |s| blk: {
|
|
const inner = self.formatTypeName(s.element);
|
|
break :blk std.fmt.allocPrint(self.alloc, "[]{s}", .{inner}) catch "slice";
|
|
},
|
|
.array => |a| blk: {
|
|
const inner = self.formatTypeName(a.element);
|
|
break :blk std.fmt.allocPrint(self.alloc, "[{d}]{s}", .{ a.length, inner }) catch "array";
|
|
},
|
|
.signed => |w| std.fmt.allocPrint(self.alloc, "s{d}", .{w}) catch "signed",
|
|
.unsigned => |w| std.fmt.allocPrint(self.alloc, "u{d}", .{w}) catch "unsigned",
|
|
.optional => |o| blk: {
|
|
const inner = self.formatTypeName(o.child);
|
|
break :blk std.fmt.allocPrint(self.alloc, "?{s}", .{inner}) catch "optional";
|
|
},
|
|
.vector => |v| blk: {
|
|
const inner = self.formatTypeName(v.element);
|
|
break :blk std.fmt.allocPrint(self.alloc, "Vector({d},{s})", .{ v.length, inner }) catch "vector";
|
|
},
|
|
else => @tagName(info),
|
|
};
|
|
}
|
|
|
|
/// Format a function type string like "() -> s32" or "(s32, s32) -> s32".
|
|
fn formatFnTypeString(self: *Lowering, fd: *const ast.FnDecl) []const u8 {
|
|
var buf: [512]u8 = undefined;
|
|
var pos: usize = 0;
|
|
buf[pos] = '(';
|
|
pos += 1;
|
|
for (fd.params, 0..) |p, i| {
|
|
if (i > 0) {
|
|
@memcpy(buf[pos..][0..2], ", ");
|
|
pos += 2;
|
|
}
|
|
const pty = self.resolveParamType(&p);
|
|
const name = self.formatTypeName(pty);
|
|
@memcpy(buf[pos..][0..name.len], name);
|
|
pos += name.len;
|
|
}
|
|
buf[pos] = ')';
|
|
pos += 1;
|
|
const ret_ty = self.resolveReturnType(fd);
|
|
if (ret_ty != .void) {
|
|
@memcpy(buf[pos..][0..4], " -> ");
|
|
pos += 4;
|
|
const rname = self.formatTypeName(ret_ty);
|
|
@memcpy(buf[pos..][0..rname.len], rname);
|
|
pos += rname.len;
|
|
}
|
|
const result = self.alloc.alloc(u8, pos) catch unreachable;
|
|
@memcpy(result, buf[0..pos]);
|
|
return result;
|
|
}
|
|
|
|
/// Format a type name for function name mangling (identifier-safe).
|
|
/// E.g. *Point → "ptr_Point", []s32 → "slice_s32", [3]f64 → "array_3_f64".
|
|
/// Check if a param type expression references a type param name (possibly nested).
|
|
pub fn matchTypeParam(_: *Lowering, type_node: *const Node, tp_name: []const u8) bool {
|
|
return switch (type_node.data) {
|
|
.type_expr => |te| std.mem.eql(u8, te.name, tp_name),
|
|
.identifier => |id| std.mem.eql(u8, id.name, tp_name),
|
|
.slice_type_expr => |st| matchTypeParamStatic(st.element_type, tp_name),
|
|
.pointer_type_expr => |pt| matchTypeParamStatic(pt.pointee_type, tp_name),
|
|
.many_pointer_type_expr => |mp| matchTypeParamStatic(mp.element_type, tp_name),
|
|
.optional_type_expr => |ot| matchTypeParamStatic(ot.inner_type, tp_name),
|
|
.array_type_expr => |at| matchTypeParamStatic(at.element_type, tp_name),
|
|
.closure_type_expr => |ct| blk: {
|
|
for (ct.param_types) |pt| if (matchTypeParamStatic(pt, tp_name)) break :blk true;
|
|
if (ct.return_type) |rt| if (matchTypeParamStatic(rt, tp_name)) break :blk true;
|
|
break :blk false;
|
|
},
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
fn matchTypeParamStatic(type_node: *const Node, tp_name: []const u8) bool {
|
|
return switch (type_node.data) {
|
|
.type_expr => |te| std.mem.eql(u8, te.name, tp_name),
|
|
.identifier => |id| std.mem.eql(u8, id.name, tp_name),
|
|
.slice_type_expr => |st| matchTypeParamStatic(st.element_type, tp_name),
|
|
.pointer_type_expr => |pt| matchTypeParamStatic(pt.pointee_type, tp_name),
|
|
.many_pointer_type_expr => |mp| matchTypeParamStatic(mp.element_type, tp_name),
|
|
.optional_type_expr => |ot| matchTypeParamStatic(ot.inner_type, tp_name),
|
|
.array_type_expr => |at| matchTypeParamStatic(at.element_type, tp_name),
|
|
.closure_type_expr => |ct| blk: {
|
|
for (ct.param_types) |pt| if (matchTypeParamStatic(pt, tp_name)) break :blk true;
|
|
if (ct.return_type) |rt| if (matchTypeParamStatic(rt, tp_name)) break :blk true;
|
|
break :blk false;
|
|
},
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
/// Extract the concrete type that corresponds to a type param from an arg type.
|
|
/// E.g., param type []$T with arg type []s64 → T = s64.
|
|
pub fn extractTypeParam(self: *Lowering, type_node: *const Node, arg_ty: TypeId, tp_name: []const u8) ?TypeId {
|
|
return switch (type_node.data) {
|
|
.type_expr => |te| if (std.mem.eql(u8, te.name, tp_name)) arg_ty else null,
|
|
.identifier => |id| if (std.mem.eql(u8, id.name, tp_name)) arg_ty else null,
|
|
.slice_type_expr => |st| blk: {
|
|
// arg_ty should be a slice → extract element type
|
|
if (arg_ty.isBuiltin()) break :blk null;
|
|
const info = self.module.types.get(arg_ty);
|
|
break :blk switch (info) {
|
|
.slice => |s| self.extractTypeParam(st.element_type, s.element, tp_name),
|
|
else => null,
|
|
};
|
|
},
|
|
.pointer_type_expr => |pt| blk: {
|
|
if (arg_ty.isBuiltin()) break :blk null;
|
|
const info = self.module.types.get(arg_ty);
|
|
break :blk switch (info) {
|
|
.pointer => |p| self.extractTypeParam(pt.pointee_type, p.pointee, tp_name),
|
|
else => null,
|
|
};
|
|
},
|
|
.many_pointer_type_expr => |mp| blk: {
|
|
if (arg_ty.isBuiltin()) break :blk null;
|
|
const info = self.module.types.get(arg_ty);
|
|
break :blk switch (info) {
|
|
.many_pointer => |p| self.extractTypeParam(mp.element_type, p.element, tp_name),
|
|
else => null,
|
|
};
|
|
},
|
|
.optional_type_expr => |ot| blk: {
|
|
if (arg_ty.isBuiltin()) break :blk null;
|
|
const info = self.module.types.get(arg_ty);
|
|
break :blk switch (info) {
|
|
.optional => |o| self.extractTypeParam(ot.inner_type, o.child, tp_name),
|
|
else => null,
|
|
};
|
|
},
|
|
.array_type_expr => |at| blk: {
|
|
if (arg_ty.isBuiltin()) break :blk null;
|
|
const info = self.module.types.get(arg_ty);
|
|
break :blk switch (info) {
|
|
.array => |a| self.extractTypeParam(at.element_type, a.element, tp_name),
|
|
else => null,
|
|
};
|
|
},
|
|
.closure_type_expr => |ct| blk: {
|
|
if (arg_ty.isBuiltin()) break :blk null;
|
|
const info = self.module.types.get(arg_ty);
|
|
const c_params: []const TypeId, const c_ret: TypeId = switch (info) {
|
|
.closure => |c| .{ c.params, c.ret },
|
|
.function => |f| .{ f.params, f.ret },
|
|
else => break :blk null,
|
|
};
|
|
// Prefer the return position (`Closure(...) -> $R`), then params.
|
|
if (ct.return_type) |rt| {
|
|
if (self.extractTypeParam(rt, c_ret, tp_name)) |ety| break :blk ety;
|
|
}
|
|
for (ct.param_types, 0..) |pt, i| {
|
|
if (i >= c_params.len) break;
|
|
if (self.extractTypeParam(pt, c_params[i], tp_name)) |ety| break :blk ety;
|
|
}
|
|
break :blk null;
|
|
},
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
/// Mangle a TypeId into its mono-key fragment. Thin delegation to the
|
|
/// canonical owner (`GenericResolver`, `generics.zig`); kept on `Lowering`
|
|
/// because ~30 cross-cutting callers (impl-map keys, conversion keys, shape
|
|
/// keys) reach it here, well beyond generic monomorphization.
|
|
pub fn mangleTypeName(self: *Lowering, ty: TypeId) []const u8 {
|
|
return self.genericResolver().mangleTypeName(ty);
|
|
}
|
|
|
|
/// Resolve type category names (like "int", "struct", "float") to matching TypeId tag values.
|
|
/// Returns a list of TypeId index values that match the category.
|
|
fn resolveTypeCategoryTags(self: *Lowering, name: []const u8) []const u64 {
|
|
var tags = std.ArrayList(u64).empty;
|
|
|
|
// Fixed builtin categories
|
|
if (std.mem.eql(u8, name, "int")) {
|
|
tags.append(self.alloc, TypeId.s8.index()) catch {};
|
|
tags.append(self.alloc, TypeId.s16.index()) catch {};
|
|
tags.append(self.alloc, TypeId.s32.index()) catch {};
|
|
tags.append(self.alloc, TypeId.s64.index()) catch {};
|
|
tags.append(self.alloc, TypeId.u8.index()) catch {};
|
|
tags.append(self.alloc, TypeId.u16.index()) catch {};
|
|
tags.append(self.alloc, TypeId.u32.index()) catch {};
|
|
tags.append(self.alloc, TypeId.u64.index()) catch {};
|
|
tags.append(self.alloc, TypeId.usize.index()) catch {};
|
|
tags.append(self.alloc, TypeId.isize.index()) catch {};
|
|
return tags.items;
|
|
}
|
|
if (std.mem.eql(u8, name, "float")) {
|
|
tags.append(self.alloc, TypeId.f32.index()) catch {};
|
|
tags.append(self.alloc, TypeId.f64.index()) catch {};
|
|
return tags.items;
|
|
}
|
|
if (std.mem.eql(u8, name, "bool")) {
|
|
tags.append(self.alloc, TypeId.bool.index()) catch {};
|
|
return tags.items;
|
|
}
|
|
if (std.mem.eql(u8, name, "string")) {
|
|
tags.append(self.alloc, TypeId.string.index()) catch {};
|
|
return tags.items;
|
|
}
|
|
if (std.mem.eql(u8, name, "void")) {
|
|
tags.append(self.alloc, TypeId.void.index()) catch {};
|
|
return tags.items;
|
|
}
|
|
if (std.mem.eql(u8, name, "type") or std.mem.eql(u8, name, "Type")) {
|
|
tags.append(self.alloc, TypeId.any.index()) catch {};
|
|
return tags.items;
|
|
}
|
|
|
|
// Dynamic categories: scan TypeTable for matching types
|
|
const Category = enum { @"struct", @"enum", @"union", slice, array, pointer, vector, optional, error_set };
|
|
const cat: ?Category = if (std.mem.eql(u8, name, "struct"))
|
|
.@"struct"
|
|
else if (std.mem.eql(u8, name, "enum") or std.mem.eql(u8, name, "union"))
|
|
.@"enum"
|
|
else if (std.mem.eql(u8, name, "slice"))
|
|
.slice
|
|
else if (std.mem.eql(u8, name, "array"))
|
|
.array
|
|
else if (std.mem.eql(u8, name, "pointer"))
|
|
.pointer
|
|
else if (std.mem.eql(u8, name, "vector"))
|
|
.vector
|
|
else if (std.mem.eql(u8, name, "optional"))
|
|
.optional
|
|
else if (std.mem.eql(u8, name, "error_set"))
|
|
.error_set
|
|
else
|
|
null;
|
|
|
|
if (cat) |c| {
|
|
for (self.module.types.infos.items, 0..) |info, idx| {
|
|
const matches = switch (c) {
|
|
.@"struct" => info == .@"struct",
|
|
.@"enum" => info == .@"enum" or info == .tagged_union,
|
|
.@"union" => info == .@"union" or info == .tagged_union,
|
|
.slice => info == .slice,
|
|
.array => info == .array,
|
|
.pointer => info == .pointer or info == .many_pointer,
|
|
.vector => info == .vector,
|
|
.optional => info == .optional,
|
|
.error_set => info == .error_set,
|
|
};
|
|
if (matches) {
|
|
tags.append(self.alloc, @intCast(idx)) catch {};
|
|
}
|
|
}
|
|
}
|
|
|
|
// Specific type name (e.g., Point, Color) — look up in type registry
|
|
if (tags.items.len == 0) {
|
|
const name_id = self.module.types.internString(name);
|
|
if (self.module.types.findByName(name_id)) |tid| {
|
|
tags.append(self.alloc, tid.index()) catch {};
|
|
}
|
|
}
|
|
|
|
return tags.items;
|
|
}
|
|
|
|
/// Check if a match expression is a type-category match (patterns are type/category names).
|
|
fn inferMatchResultType(self: *Lowering, me: *const ast.MatchExpr) TypeId {
|
|
// Infer result type from the first non-null arm body.
|
|
// If we skip null_literal arms and find a concrete type T, and there
|
|
// were null arms, the result is ?T (optional).
|
|
var has_null = false;
|
|
var saw_unresolved = false;
|
|
var saw_noreturn = false;
|
|
for (me.arms) |arm| {
|
|
const last_node = if (arm.body.data == .block) blk: {
|
|
if (arm.body.data.block.stmts.len > 0) {
|
|
break :blk arm.body.data.block.stmts[arm.body.data.block.stmts.len - 1];
|
|
}
|
|
break :blk arm.body;
|
|
} else arm.body;
|
|
|
|
if (last_node.data == .null_literal) {
|
|
has_null = true;
|
|
continue;
|
|
}
|
|
|
|
// First arm with a statically-inferable type determines the result.
|
|
// An arm whose type isn't inferable from the AST alone (e.g. a bare
|
|
// enum literal) doesn't decide — keep looking; the caller falls back
|
|
// to the contextual target type if none of the arms resolve.
|
|
const arm_ty = self.inferExprType(last_node);
|
|
// A diverging arm (`noreturn` — `return` / `raise` / `break` /
|
|
// `continue`) doesn't produce a value, so it doesn't decide the
|
|
// result type; keep looking. The match is `noreturn` only if EVERY
|
|
// arm diverges (handled after the loop).
|
|
if (arm_ty == .noreturn) {
|
|
saw_noreturn = true;
|
|
continue;
|
|
}
|
|
if (arm_ty == .unresolved) {
|
|
saw_unresolved = true;
|
|
continue;
|
|
}
|
|
if (has_null and arm_ty != .void) {
|
|
return self.module.types.optionalOf(arm_ty);
|
|
}
|
|
return arm_ty;
|
|
}
|
|
if (saw_unresolved) return .unresolved;
|
|
if (saw_noreturn) return .noreturn; // all arms diverge
|
|
return .void;
|
|
}
|
|
|
|
fn isTypeCategoryMatch(me: *const ast.MatchExpr) bool {
|
|
for (me.arms) |arm| {
|
|
if (arm.pattern) |pat| {
|
|
const name = switch (pat.data) {
|
|
.identifier => |id| id.name,
|
|
.type_expr => |te| te.name,
|
|
else => continue,
|
|
};
|
|
const categories = [_][]const u8{
|
|
"int", "float", "bool", "string", "void", "type", "Type",
|
|
"struct", "enum", "union", "slice", "array", "pointer", "vector",
|
|
};
|
|
for (categories) |cat| {
|
|
if (std.mem.eql(u8, name, cat)) return true;
|
|
}
|
|
// Also match specific struct/enum type names (e.g., case Point:)
|
|
if (name.len > 0 and name[0] >= 'A' and name[0] <= 'Z') return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// After args have been lowered, append the lowered values of any
|
|
/// `param: T = default_expr` defaults for positions past `args.items.len`.
|
|
/// Stops at the first param without a default. Used at method-dispatch
|
|
/// sites whose callee is a field_access (so `expandCallDefaults` can't
|
|
/// handle them up front). The default expression is lowered in the
|
|
/// caller's current scope, so identifiers like `context.allocator`
|
|
/// resolve to the caller's runtime context.
|
|
fn appendDefaultArgs(self: *Lowering, fd: *const ast.FnDecl, args: *std.ArrayList(Ref)) void {
|
|
if (args.items.len >= fd.params.len) return;
|
|
var i: usize = args.items.len;
|
|
while (i < fd.params.len) : (i += 1) {
|
|
const dflt = fd.params[i].default_expr orelse break;
|
|
const v = self.lowerExpr(dflt);
|
|
args.append(self.alloc, v) catch unreachable;
|
|
}
|
|
}
|
|
|
|
/// When a bare-identifier call omits trailing positional args and the
|
|
/// callee's signature provides defaults for them, return a fresh Call
|
|
/// node with the defaults filled in. Returns null when no expansion is
|
|
/// needed (callee unknown, all args provided, or no defaults available).
|
|
fn expandCallDefaults(self: *Lowering, c: *const ast.Call, sel_author: ?*const SelectedFunc, author_ambiguous: bool) ?*ast.Call {
|
|
const fd = blk: {
|
|
switch (c.callee.data) {
|
|
.identifier => |id| {
|
|
const eff_name = blk2: {
|
|
const scoped = if (self.scope) |scope| scope.lookupFn(id.name) orelse id.name else id.name;
|
|
if (self.program_index.ufcs_alias_map.get(id.name)) |target| {
|
|
break :blk2 if (self.scope) |scope| scope.lookupFn(target) orelse target else target;
|
|
}
|
|
break :blk2 scoped;
|
|
};
|
|
// fix-0102d site 1 / R5 §C: for a genuine flat same-name
|
|
// collision the omitted trailing args are filled from the
|
|
// author the call resolver selected — its `*FnDecl` defaults —
|
|
// not the first-wins winner's. lowering consumes the ONE author
|
|
// verdict (`selectedFreeAuthor`, computed once in `lowerCall`)
|
|
// rather than re-resolving the name, so default expansion and
|
|
// dispatch agree on the author. `.ambiguous` declines to expand
|
|
// (the call path emits the single diagnostic); a non-collision
|
|
// call keeps the existing first-wins winner, byte-for-byte.
|
|
// Reading `.decl` only keeps `materialized` null — inspecting
|
|
// defaults must not lower the author (0102d).
|
|
if (author_ambiguous) return null;
|
|
if (sel_author) |sf| break :blk sf.decl;
|
|
break :blk self.program_index.fn_ast_map.get(eff_name) orelse return null;
|
|
},
|
|
// Namespace call `mod.fn(args)` — args map directly to params
|
|
// (no `self` prepend), so default expansion is the same shape as
|
|
// a bare call. A METHOD call `value.method(args)` prepends `self`
|
|
// (arg/param counts are offset), so it's excluded: only treat the
|
|
// receiver as a namespace when it isn't a value in scope.
|
|
.field_access => |fa| {
|
|
const obj_name: ?[]const u8 = switch (fa.object.data) {
|
|
.identifier => |id| id.name,
|
|
.type_expr => |te| te.name,
|
|
else => null,
|
|
};
|
|
const name = obj_name orelse return null;
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(name) != null) return null; // method call on a value
|
|
}
|
|
if (self.program_index.global_names.contains(name)) return null;
|
|
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ name, fa.field }) catch fa.field;
|
|
break :blk self.program_index.fn_ast_map.get(qualified) orelse self.program_index.fn_ast_map.get(fa.field) orelse return null;
|
|
},
|
|
else => return null,
|
|
}
|
|
};
|
|
if (c.args.len >= fd.params.len) return null;
|
|
var end: usize = c.args.len;
|
|
while (end < fd.params.len) : (end += 1) {
|
|
if (fd.params[end].default_expr == null) break;
|
|
}
|
|
if (end == c.args.len) return null;
|
|
|
|
var new_args = self.alloc.alloc(*ast.Node, end) catch return null;
|
|
for (c.args, 0..) |arg, i| new_args[i] = arg;
|
|
var i: usize = c.args.len;
|
|
while (i < end) : (i += 1) {
|
|
const def = fd.params[i].default_expr.?;
|
|
// `#caller_location` resolves at the CALL site, not the callee's
|
|
// signature: emit a fresh marker carrying the call's span + file so
|
|
// lowering synthesizes the caller's `Source_Location` (ERR E4.1b).
|
|
if (def.data == .caller_location) {
|
|
const n = self.alloc.create(ast.Node) catch return null;
|
|
n.* = .{ .span = c.callee.span, .data = .{ .caller_location = {} }, .source_file = c.callee.source_file };
|
|
new_args[i] = n;
|
|
} else {
|
|
new_args[i] = def;
|
|
}
|
|
}
|
|
const new_call = self.alloc.create(ast.Call) catch return null;
|
|
new_call.* = .{ .callee = c.callee, .args = new_args };
|
|
return new_call;
|
|
}
|
|
|
|
/// Resolve parameter types for a call expression (for target_type context).
|
|
/// Returns empty slice if the function can't be resolved.
|
|
/// Return the param types of a Function from the caller's POV — i.e.
|
|
/// skipping the synthetic `__sx_ctx` slot when present. lowerCall's
|
|
/// arg-lowering uses these to set `target_type` per arg, and user
|
|
/// args don't include `__sx_ctx`, so the slot must be elided.
|
|
fn userParamTypes(self: *Lowering, func: *const Function) []TypeId {
|
|
const start: usize = if (func.has_implicit_ctx) 1 else 0;
|
|
var types_list = std.ArrayList(TypeId).empty;
|
|
if (func.params.len > start) {
|
|
for (func.params[start..]) |p| {
|
|
types_list.append(self.alloc, p.ty) catch unreachable;
|
|
}
|
|
}
|
|
return types_list.items;
|
|
}
|
|
|
|
fn resolveCallParamTypes(self: *Lowering, c: *const ast.Call, sel_author: ?*SelectedFunc) []const TypeId {
|
|
// Method calls: obj.method(args) — resolve param types from the method signature,
|
|
// skipping the first param (self) since it's prepended later.
|
|
if (c.callee.data == .field_access) {
|
|
const fa = c.callee.data.field_access;
|
|
|
|
// Namespace/static call: `Type.method(args)` where `Type` is a type
|
|
// identifier (not a value in scope). Args correspond to ALL params
|
|
// — no self prepend — so target_type for arg lowering must include
|
|
// the leading param. Skipping it would lose the protocol context
|
|
// for `xx ptr` inline-cast args.
|
|
if (fa.object.data == .identifier) {
|
|
const obj_name = fa.object.data.identifier.name;
|
|
const is_value = blk: {
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(obj_name) != null) break :blk true;
|
|
}
|
|
if (self.program_index.global_names.contains(obj_name)) break :blk true;
|
|
break :blk false;
|
|
};
|
|
if (!is_value) {
|
|
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ obj_name, fa.field }) catch return &.{};
|
|
if (self.resolveFuncByName(qualified)) |fid| {
|
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
|
return self.userParamTypes(func);
|
|
}
|
|
if (self.program_index.fn_ast_map.get(qualified)) |fd| {
|
|
var types_list = std.ArrayList(TypeId).empty;
|
|
for (fd.params) |p| {
|
|
types_list.append(self.alloc, self.resolveParamType(&p)) catch unreachable;
|
|
}
|
|
return types_list.items;
|
|
}
|
|
}
|
|
}
|
|
|
|
const obj_ty = self.inferExprType(fa.object);
|
|
// Protocol-typed receiver: look up the method on the protocol decl. The
|
|
// protocol's ProtocolMethodInfo.param_types already excludes self.
|
|
if (self.getProtocolInfo(obj_ty)) |proto_info| {
|
|
for (proto_info.methods) |m| {
|
|
if (std.mem.eql(u8, m.name, fa.field)) return m.param_types;
|
|
}
|
|
}
|
|
// Optional-protocol receiver (`?GPU`): same as above but the
|
|
// protocol type sits inside the optional's payload.
|
|
if (!obj_ty.isBuiltin()) {
|
|
const opt_info = self.module.types.get(obj_ty);
|
|
if (opt_info == .optional) {
|
|
if (self.getProtocolInfo(opt_info.optional.child)) |proto_info| {
|
|
for (proto_info.methods) |m| {
|
|
if (std.mem.eql(u8, m.name, fa.field)) return m.param_types;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Closure-typed struct field: `c.on(args)` lowers to call_closure on
|
|
// the field value. Pick up the callee's param types from the closure
|
|
// type so each arg gets the right target_type during lowering.
|
|
if (!obj_ty.isBuiltin()) {
|
|
const field_name_id = self.module.types.internString(fa.field);
|
|
const struct_fields = self.getStructFields(obj_ty);
|
|
for (struct_fields) |f| {
|
|
if (f.name == field_name_id and !f.ty.isBuiltin()) {
|
|
const fti = self.module.types.get(f.ty);
|
|
if (fti == .closure) return fti.closure.params;
|
|
if (fti == .function) return fti.function.params;
|
|
}
|
|
}
|
|
}
|
|
if (self.getStructTypeName(obj_ty)) |sname| {
|
|
// Foreign-class receiver (`#objc_class` / `#jni_class` / etc.):
|
|
// resolve the method from `foreign_class_map` walking `#extends`.
|
|
// Without this path, `target_type` for each arg falls back to
|
|
// whatever `self.target_type` was on entry — typically the
|
|
// enclosing fn's return type — which silently truncates `xx ptr`
|
|
// casts inside e.g. a `BOOL`-returning method body.
|
|
if (self.program_index.foreign_class_map.get(sname)) |fcd| {
|
|
if (self.findForeignMethodInChain(fcd, fa.field)) |found| {
|
|
const md = found.method;
|
|
const saved_fc = self.current_foreign_class;
|
|
defer self.current_foreign_class = saved_fc;
|
|
self.current_foreign_class = found.fcd;
|
|
const user_param_start: usize = if (md.is_static) 0 else 1;
|
|
if (md.params.len > user_param_start) {
|
|
var types_list = std.ArrayList(TypeId).empty;
|
|
for (md.params[user_param_start..]) |p_node| {
|
|
types_list.append(self.alloc, self.resolveType(p_node)) catch unreachable;
|
|
}
|
|
return types_list.items;
|
|
}
|
|
return &.{};
|
|
}
|
|
}
|
|
|
|
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sname, fa.field }) catch return &.{};
|
|
// Try already-lowered functions first
|
|
if (self.resolveFuncByName(qualified)) |fid| {
|
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
|
// Skip both `__sx_ctx` (if present) AND `self` param;
|
|
// caller args include neither.
|
|
const skip: usize = (if (func.has_implicit_ctx) @as(usize, 1) else 0) + 1;
|
|
if (func.params.len > skip) {
|
|
var types_list = std.ArrayList(TypeId).empty;
|
|
for (func.params[skip..]) |p| {
|
|
types_list.append(self.alloc, p.ty) catch unreachable;
|
|
}
|
|
return types_list.items;
|
|
}
|
|
}
|
|
// Try AST map (not yet lowered)
|
|
if (self.program_index.fn_ast_map.get(qualified)) |fd| {
|
|
if (fd.params.len > 0) {
|
|
var types_list = std.ArrayList(TypeId).empty;
|
|
for (fd.params[1..]) |p| {
|
|
types_list.append(self.alloc, self.resolveParamTypeInSource(fd.body.source_file, &p)) catch unreachable;
|
|
}
|
|
return types_list.items;
|
|
}
|
|
}
|
|
// Try generic struct template method: List__Container.append → List.append
|
|
// with type bindings from the struct instantiation
|
|
if (self.struct_instance_template.get(sname)) |tmpl_name| {
|
|
const tmpl_qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tmpl_name, fa.field }) catch return &.{};
|
|
if (self.program_index.fn_ast_map.get(tmpl_qualified)) |fd| {
|
|
if (fd.params.len > 0) {
|
|
// Temporarily set type_bindings so resolveParamType can substitute T → concrete type
|
|
const saved_bindings = self.type_bindings;
|
|
if (self.struct_instance_bindings.getPtr(sname)) |bindings| {
|
|
self.type_bindings = bindings.*;
|
|
}
|
|
var types_list = std.ArrayList(TypeId).empty;
|
|
for (fd.params[1..]) |p| {
|
|
types_list.append(self.alloc, self.resolveParamTypeInSource(fd.body.source_file, &p)) catch unreachable;
|
|
}
|
|
self.type_bindings = saved_bindings;
|
|
return types_list.items;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return &.{};
|
|
}
|
|
if (c.callee.data != .identifier) return &.{};
|
|
const bare_name = c.callee.data.identifier.name;
|
|
const name = blk: {
|
|
const scoped = if (self.scope) |scope| scope.lookupFn(bare_name) orelse bare_name else bare_name;
|
|
if (self.program_index.ufcs_alias_map.get(bare_name)) |target| {
|
|
break :blk if (self.scope) |scope| scope.lookupFn(target) orelse target else target;
|
|
}
|
|
break :blk scoped;
|
|
};
|
|
|
|
// fix-0102c F2 / R5 §C: a genuine flat same-name collision must type this
|
|
// call's args against the author the call resolver selected, not the
|
|
// first-wins winner's params. lowering consumes the ONE author verdict
|
|
// (`selectedFreeAuthor`, computed once in `lowerCall`) rather than
|
|
// re-resolving the name, so arg lowering (implicit address-of, coercion)
|
|
// matches the author actually dispatched — otherwise a `*T`-param shadow
|
|
// gets a `T` value arg that is later bit-cast to a pointer (segfault). The
|
|
// FuncId materializes into the SHARED verdict (once), so dispatch reuses
|
|
// it. A non-collision call falls to the existing first-wins path below,
|
|
// byte-for-byte.
|
|
if (sel_author) |sf| {
|
|
const fid = self.selectedFuncId(sf, bare_name);
|
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
|
return self.userParamTypes(func);
|
|
}
|
|
|
|
// Check declared functions
|
|
if (self.resolveFuncByName(name)) |fid| {
|
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
|
return self.userParamTypes(func);
|
|
}
|
|
|
|
// Check AST map for function signatures
|
|
if (self.program_index.fn_ast_map.get(name)) |fd| {
|
|
var types_list = std.ArrayList(TypeId).empty;
|
|
for (fd.params) |p| {
|
|
types_list.append(self.alloc, self.resolveParamType(&p)) catch unreachable;
|
|
}
|
|
return types_list.items;
|
|
}
|
|
|
|
// Check global function pointer variables
|
|
if (self.program_index.global_names.get(bare_name)) |gi| {
|
|
if (!gi.ty.isBuiltin()) {
|
|
const ti = self.module.types.get(gi.ty);
|
|
if (ti == .function) {
|
|
return ti.function.params;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check local scope for function pointer variables
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(bare_name)) |binding| {
|
|
if (!binding.ty.isBuiltin()) {
|
|
const ti = self.module.types.get(binding.ty);
|
|
if (ti == .function) {
|
|
return ti.function.params;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return &.{};
|
|
}
|
|
|
|
/// Check if a param is a type param declaration ($T: Type).
|
|
/// A type param declaration has param.name == one of the type_params names.
|
|
pub fn isTypeParamDecl(param: *const ast.Param, type_params: []const ast.StructTypeParam) bool {
|
|
for (type_params) |tp| {
|
|
if (std.mem.eql(u8, param.name, tp.name)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Check if a function has comptime (non-Type) value parameters.
|
|
fn hasComptimeParams(fd: *const ast.FnDecl) bool {
|
|
for (fd.params) |p| {
|
|
if (p.is_comptime) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// A plain free function: no type params (not generic) and an ordinary sx
|
|
/// body (not `#foreign` / `#builtin` / `#compiler`). Only these get an
|
|
/// out-of-line identity-addressable slot — the bare-call disambiguation
|
|
/// (fix-0102c) and the shadow-author lowering pass leave every other shape
|
|
/// to the existing name-keyed dispatch.
|
|
fn isPlainFreeFn(fd: *const ast.FnDecl) bool {
|
|
if (fd.type_params.len > 0) return false;
|
|
return switch (fd.body.data) {
|
|
.foreign_expr, .builtin_expr, .compiler_expr => false,
|
|
else => true,
|
|
};
|
|
}
|
|
|
|
/// Pack-fn: has a trailing heterogeneous pack param (`is_variadic
|
|
/// AND is_comptime`). Mixed shapes — non-pack comptime params
|
|
/// before the pack — are also accepted; the mono folds those
|
|
/// comptime VALUES into the mangled name and binds them as both
|
|
/// comptime substitutions (for #insert) and runtime locals (for
|
|
/// bare-name body references).
|
|
fn isPackFn(fd: *const ast.FnDecl) bool {
|
|
for (fd.params) |p| {
|
|
if (isPackParam(p)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// A trailing pack parameter: the comptime type-pack `..$args`
|
|
/// (`is_comptime`) or the protocol-constrained pack `..xs: P` (`is_pack`).
|
|
/// Both monomorphize per call shape via `lowerPackFnCall`; the slice
|
|
/// variadic (`..xs: []T`) is neither and stays a runtime slice.
|
|
fn isPackParam(p: ast.Param) bool {
|
|
return p.is_variadic and (p.is_comptime or p.is_pack);
|
|
}
|
|
|
|
/// Creates a temporary function marked `is_comptime = true` that wraps
|
|
/// the given expression as its return value. Returns the FuncId.
|
|
pub fn createComptimeFunction(self: *Lowering, prefix: []const u8, expr: *const Node, ret_ty: TypeId) FuncId {
|
|
var buf: [64]u8 = undefined;
|
|
const name = std.fmt.bufPrint(&buf, "{s}_{d}", .{ prefix, self.comptime_counter }) catch prefix;
|
|
self.comptime_counter += 1;
|
|
|
|
// Save current builder + lowering state. The wrapper fn we're
|
|
// about to build runs the comptime expression in isolation —
|
|
// it must NOT inherit the enclosing call's `inline_return_target`
|
|
// (which would re-route a `return` inside the wrapper into a
|
|
// slot belonging to a different basic block), pack bindings
|
|
// (which would substitute caller's `args` inside the wrapper),
|
|
// or comptime-param bindings (which would substitute caller's
|
|
// `$fmt` inside the wrapper's #insert children). Without these
|
|
// saves, nested comptime calls leak outer state into the
|
|
// interp-executed wrapper, producing garbage stores (issue-0046
|
|
// face 1 — storeAtRawPtr null).
|
|
const saved_func = self.builder.func;
|
|
const saved_block = self.builder.current_block;
|
|
const saved_counter = self.builder.inst_counter;
|
|
const saved_scope = self.scope;
|
|
const saved_ctx_ref = self.current_ctx_ref;
|
|
const saved_iri = self.inline_return_target;
|
|
const saved_pan = self.pack_arg_nodes;
|
|
const saved_ppc = self.pack_param_count;
|
|
const saved_pat = self.pack_arg_types;
|
|
const saved_cpn = self.comptime_param_nodes;
|
|
const saved_block_terminated = self.block_terminated;
|
|
const saved_target_type = self.target_type;
|
|
const saved_func_defer_base = self.func_defer_base;
|
|
self.inline_return_target = null;
|
|
self.pack_arg_nodes = null;
|
|
self.pack_param_count = null;
|
|
self.pack_arg_types = null;
|
|
self.comptime_param_nodes = null;
|
|
self.block_terminated = false;
|
|
self.target_type = null;
|
|
self.func_defer_base = self.defer_stack.items.len;
|
|
defer {
|
|
self.current_ctx_ref = saved_ctx_ref;
|
|
self.inline_return_target = saved_iri;
|
|
self.pack_arg_nodes = saved_pan;
|
|
self.pack_param_count = saved_ppc;
|
|
self.pack_arg_types = saved_pat;
|
|
self.comptime_param_nodes = saved_cpn;
|
|
self.block_terminated = saved_block_terminated;
|
|
self.target_type = saved_target_type;
|
|
self.func_defer_base = saved_func_defer_base;
|
|
}
|
|
|
|
// Build params: implicit `__sx_ctx` at slot 0 when the program
|
|
// uses Context (so the body's `context.X` reads + transitive calls
|
|
// resolve cleanly). The comptime function's top-level invocation
|
|
// supplies `&__sx_default_context` (interp via callWithDefaultContext;
|
|
// codegen via the comptime-eval glue in emit_llvm).
|
|
const wants_ctx = self.implicit_ctx_enabled;
|
|
const params_slice = blk: {
|
|
if (!wants_ctx) break :blk &[_]Function.Param{};
|
|
const owned = self.alloc.alloc(Function.Param, 1) catch break :blk &[_]Function.Param{};
|
|
owned[0] = .{
|
|
.name = self.module.types.internString("__sx_ctx"),
|
|
.ty = self.module.types.ptrTo(.void),
|
|
};
|
|
break :blk owned;
|
|
};
|
|
|
|
// Create the comptime function
|
|
const name_id = self.module.types.internString(name);
|
|
const func_id = self.builder.beginFunction(name_id, params_slice, ret_ty);
|
|
|
|
// Mark as comptime + has_implicit_ctx
|
|
const fn_mut = self.module.getFunctionMut(func_id);
|
|
fn_mut.is_comptime = true;
|
|
fn_mut.has_implicit_ctx = wants_ctx;
|
|
|
|
// Create entry block
|
|
const entry_name = self.module.types.internString("entry");
|
|
const entry = self.builder.appendBlock(entry_name, &.{});
|
|
self.builder.switchToBlock(entry);
|
|
if (wants_ctx) self.current_ctx_ref = Ref.fromIndex(0);
|
|
|
|
// Create a scope that chains to the enclosing scope (so the
|
|
// expression can reference names visible at the #run site).
|
|
var ct_scope = Scope.init(self.alloc, saved_scope);
|
|
self.scope = &ct_scope;
|
|
|
|
// Lower the expression and return it
|
|
const result = self.lowerExpr(expr);
|
|
if (ret_ty == .void) {
|
|
self.builder.retVoid();
|
|
} else {
|
|
self.builder.ret(result, ret_ty);
|
|
}
|
|
|
|
self.builder.finalize();
|
|
|
|
// Restore builder state
|
|
self.scope = saved_scope;
|
|
ct_scope.deinit();
|
|
self.builder.func = saved_func;
|
|
self.builder.current_block = saved_block;
|
|
self.builder.inst_counter = saved_counter;
|
|
|
|
return func_id;
|
|
}
|
|
|
|
// ── Block helpers ───────────────────────────────────────────────
|
|
|
|
fn freshBlock(self: *Lowering, prefix: []const u8) BlockId {
|
|
return self.freshBlockWithParams(prefix, &.{});
|
|
}
|
|
|
|
fn freshBlockWithParams(self: *Lowering, prefix: []const u8, params: []const TypeId) BlockId {
|
|
var buf: [64]u8 = undefined;
|
|
const name = std.fmt.bufPrint(&buf, "{s}.{d}", .{ prefix, self.block_counter }) catch prefix;
|
|
self.block_counter += 1;
|
|
const name_id = self.module.types.internString(name);
|
|
return self.builder.appendBlock(name_id, params);
|
|
}
|
|
|
|
fn currentBlockHasTerminator(self: *Lowering) bool {
|
|
const func = self.builder.module.getFunctionMut(self.builder.func.?);
|
|
const block_idx = self.builder.current_block orelse return true;
|
|
const block = &func.blocks.items[block_idx.index()];
|
|
if (block.insts.items.len > 0) {
|
|
const last_op = block.insts.items[block.insts.items.len - 1].op;
|
|
return switch (last_op) {
|
|
.ret, .ret_void, .br, .cond_br, .switch_br, .@"unreachable" => true,
|
|
else => false,
|
|
};
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// ── Type resolution ─────────────────────────────────────────────
|
|
// Delegates to type_bridge for full AST type node resolution.
|
|
|
|
fn resolveReturnType(self: *Lowering, fd: *const ast.FnDecl) TypeId {
|
|
if (fd.return_type) |rt| {
|
|
return self.resolveTypeWithBindings(rt);
|
|
}
|
|
// No explicit annotation — the type is inferred from the body, which
|
|
// references the function's own parameters (`(x: s32) => x * 2`). Those
|
|
// params aren't pushed into `self.scope` until body lowering, so bind
|
|
// them into a temporary scope here; otherwise `inferExprType` can't
|
|
// resolve `x`, the inference yields `.unresolved`, and that reaches LLVM
|
|
// emission as `func.ret` (issue 0059). Whether it slipped through used to
|
|
// depend on a same-named binding lingering from earlier lowering.
|
|
var tmp_scope = Scope.init(self.alloc, self.scope);
|
|
defer tmp_scope.deinit();
|
|
const saved_scope = self.scope;
|
|
self.scope = &tmp_scope;
|
|
defer self.scope = saved_scope;
|
|
for (fd.params, 0..) |p, i| {
|
|
// Bind only plain annotated value params — that's all the body's
|
|
// return type can depend on by name. Skip variadic / pack / comptime
|
|
// params (their concrete types come from per-call substitution) and
|
|
// unannotated ones (no context here). Resolve the type directly via
|
|
// resolveTypeWithBindings rather than resolveParamType: the latter
|
|
// does variadic/pack bookkeeping that must run exactly once, at body
|
|
// lowering — calling it here too corrupts that state.
|
|
if (p.is_variadic or p.is_pack or p.is_comptime) continue;
|
|
if (p.type_expr.data == .inferred_type) continue;
|
|
const pty = self.resolveTypeWithBindings(p.type_expr);
|
|
tmp_scope.put(p.name, .{ .ref = Ref.fromIndex(@intCast(i)), .ty = pty, .is_alloca = false });
|
|
}
|
|
// Arrow functions without explicit return type: infer from body expression.
|
|
if (fd.is_arrow) {
|
|
return self.inferExprType(fd.body);
|
|
}
|
|
// Not arrow: an explicit `return <value>` statement wins. Otherwise
|
|
// default to void — the body's tail expression is a side-effect
|
|
// statement, not an implicit return.
|
|
if (self.findReturnValueType(fd.body)) |ty| return ty;
|
|
return .void;
|
|
}
|
|
|
|
/// Walk a function body and return the type of the first `return <value>;`
|
|
/// statement encountered. Does not descend into nested function or lambda
|
|
/// declarations (those have their own return types).
|
|
fn findReturnValueType(self: *Lowering, node: *const Node) ?TypeId {
|
|
return switch (node.data) {
|
|
.return_stmt => |rs| if (rs.value) |v| self.inferExprType(v) else null,
|
|
.block => |blk| blk: {
|
|
for (blk.stmts) |s| {
|
|
if (self.findReturnValueType(s)) |t| break :blk t;
|
|
}
|
|
break :blk null;
|
|
},
|
|
.if_expr => |ie| blk: {
|
|
if (self.findReturnValueType(ie.then_branch)) |t| break :blk t;
|
|
if (ie.else_branch) |eb| {
|
|
if (self.findReturnValueType(eb)) |t| break :blk t;
|
|
}
|
|
break :blk null;
|
|
},
|
|
.while_expr => |we| self.findReturnValueType(we.body),
|
|
.for_expr => |fe| self.findReturnValueType(fe.body),
|
|
.match_expr => |me| blk: {
|
|
for (me.arms) |arm| {
|
|
if (self.findReturnValueType(arm.body)) |t| break :blk t;
|
|
}
|
|
break :blk null;
|
|
},
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
fn resolveParamType(self: *Lowering, p: *const ast.Param) TypeId {
|
|
// A plain value param with no annotation can only be typed from
|
|
// context (a lambda's target closure signature). When `resolveParamType`
|
|
// is reached for one, there is no such context — so it's a genuine
|
|
// "missing annotation" error, not an 8-byte-int guess. (Comptime/
|
|
// variadic pack params also carry `inferred_type` but get their types
|
|
// from per-call substitution, so they're exempt here.)
|
|
if (p.type_expr.data == .inferred_type and !p.is_comptime and !p.is_variadic and !p.is_pack) {
|
|
if (self.diagnostics) |d| {
|
|
d.addFmt(.err, p.type_expr.span, "parameter '{s}' has no type annotation", .{p.name});
|
|
}
|
|
return .unresolved;
|
|
}
|
|
const declared_ty = self.resolveTypeWithBindings(p.type_expr);
|
|
if (p.is_variadic) {
|
|
// Two surface forms:
|
|
// - legacy `name: ..T` — declared_ty is the element type;
|
|
// wrap to receive a `[]T` slice.
|
|
// - new `..name: []T` — declared_ty is already the slice
|
|
// type; use it as-is. Wrapping here would double up to
|
|
// `[][]T` and downstream LLVM emission crashes when the
|
|
// caller's argument-marshal pack produces a `[]T` that
|
|
// doesn't match the callee's stored param shape.
|
|
if (!declared_ty.isBuiltin()) {
|
|
const info = self.module.types.get(declared_ty);
|
|
if (info == .slice) return declared_ty;
|
|
}
|
|
return self.module.types.sliceOf(declared_ty);
|
|
}
|
|
return declared_ty;
|
|
}
|
|
|
|
pub fn resolveType(self: *Lowering, type_ann: *const Node) TypeId {
|
|
return self.resolveTypeWithBindings(type_ann);
|
|
}
|
|
|
|
/// Resolve a type node with the visibility context pinned to `src`, the
|
|
/// DEFINING module of a namespaced callee, restoring the caller's context
|
|
/// after. A namespaced callee's declared return type may name a type that is
|
|
/// bare-visible only inside the callee's own module — namespaced-only from the
|
|
/// call site's view. Post-E1 the bare leaf is source-aware, so resolving that
|
|
/// return type in the CALL SITE's context would wrongly reject it (the type
|
|
/// analog of the issue-0100-F1 source pin that lowers a namespaced fn body in
|
|
/// its own module's context). `src == null` falls back to the call site's
|
|
/// context unchanged.
|
|
pub fn resolveTypeInSource(self: *Lowering, src: ?[]const u8, type_ann: *const Node) TypeId {
|
|
const pinned = src orelse return self.resolveType(type_ann);
|
|
const saved = self.current_source_file;
|
|
defer self.setCurrentSourceFile(saved);
|
|
self.setCurrentSourceFile(pinned);
|
|
return self.resolveType(type_ann);
|
|
}
|
|
|
|
/// `resolveParamType` with the visibility context pinned to `src`, the
|
|
/// DEFINING module of the param's function. An imported method's
|
|
/// default-param type (`alloc: Allocator`) is bare-visible only inside its
|
|
/// own module, so typing a cross-module call's args against it must resolve
|
|
/// in that module's context, not the call site's (E4 — the param analog of
|
|
/// `resolveTypeInSource`). `src == null` falls back unchanged.
|
|
fn resolveParamTypeInSource(self: *Lowering, src: ?[]const u8, p: *const ast.Param) TypeId {
|
|
const pinned = src orelse return self.resolveParamType(p);
|
|
const saved = self.current_source_file;
|
|
defer self.setCurrentSourceFile(saved);
|
|
self.setCurrentSourceFile(pinned);
|
|
return self.resolveParamType(p);
|
|
}
|
|
|
|
/// Construct a `TypeResolver` view over the current lowering state (borrows
|
|
/// only; cheap by-value, reflects current `diagnostics` / `program_index`).
|
|
fn typeResolver(self: *Lowering) TypeResolver {
|
|
return .{
|
|
.alloc = self.alloc,
|
|
.types = &self.module.types,
|
|
.diagnostics = self.diagnostics,
|
|
.index = &self.program_index,
|
|
};
|
|
}
|
|
|
|
/// Snapshot the active resolution context (Principle 2) for `TypeResolver`.
|
|
/// A2.2 wires the type bindings + literal target; the pack/comptime fields
|
|
/// are populated as A2.3 moves the cases that consume them.
|
|
fn resolveEnv(self: *Lowering) ResolveEnv {
|
|
return .{
|
|
.type_bindings = if (self.type_bindings) |*tb| tb else null,
|
|
.target_type = self.target_type,
|
|
};
|
|
}
|
|
|
|
/// Inner-type recursion hook for `TypeResolver.resolveCompound`: resolves a
|
|
/// child type node through the full stateful resolver, so generic structs /
|
|
/// bindings / aliases in element position keep their resolution.
|
|
pub fn resolveInner(self: *Lowering, node: *const Node) TypeId {
|
|
return self.resolveTypeWithBindings(node);
|
|
}
|
|
|
|
/// Fixed-array dimension hook for `TypeResolver.resolveCompound`. A literal
|
|
/// `[16]T` and a named-const `N :: 16; [N]T` must resolve to the SAME length:
|
|
/// the dimension folds to a compile-time integer (looked up in the comptime /
|
|
/// value / module-const tables the stateful lowering owns) and is narrowed to
|
|
/// `u32` through the single range-checked `program_index.foldDimU32` — never a
|
|
/// bare `@intCast`, so an oversized-but-valid `i64` dim (`[5_000_000_000]`)
|
|
/// diagnoses instead of panicking the compiler (issue 0087). A dimension that
|
|
/// isn't a compile-time integer (or doesn't fit a `u32`) is a hard error:
|
|
/// emit a diagnostic so the driver aborts (`hasErrors()`), then return a
|
|
/// harmless `0` so body lowering finishes without touching the `.unresolved`
|
|
/// sentinel (which would `@panic` in `sizeOf` mid-lowering, before the
|
|
/// diagnostic surfaces). The diagnostic — not the returned length — is what
|
|
/// guarantees no garbage ships (issue 0083).
|
|
pub fn resolveArrayLen(self: *Lowering, len_node: *const Node) ?u32 {
|
|
const result = program_index_mod.foldDimU32(len_node, self, 0);
|
|
if (result == .ok) return result.ok;
|
|
// A non-const / oversized / negative dim is a hard error. Emit the
|
|
// shared diagnostic (single wording source — `program_index.reportDimError`,
|
|
// also used by the stateless alias path so the two cannot diverge) and
|
|
// return null so `resolveCompound` yields the `.unresolved` sentinel — NO
|
|
// fabricated length (issue 0083: a `0` here gives a 0-byte alloca and OOB
|
|
// element access). Lowering the binding never computes the failed type's
|
|
// size: `alloca` records the type but defers `sizeOf` to LLVM emission,
|
|
// which the emitted diagnostic pre-empts via `hasErrors()`, and a
|
|
// downstream use of the `.unresolved`-typed value is poison-suppressed (a
|
|
// field access stays silent — `emitFieldError`). So the failure surfaces
|
|
// as ONE clean diagnostic and never reaches the `sizeOf` panic.
|
|
if (self.diagnostics) |d| program_index_mod.reportDimError(d, len_node.span, result);
|
|
return null;
|
|
}
|
|
|
|
/// Leaf-name lookup for the shared dimension evaluator: a name bound to a
|
|
/// compile-time integer across the three const tables.
|
|
pub fn lookupDimName(self: *Lowering, name: []const u8) ?i64 {
|
|
return self.comptimeIntNamed(name);
|
|
}
|
|
|
|
/// Pack-length leaf for the shared integer-expression evaluator: a pack
|
|
/// name's monomorphised arity (e.g. an `inline for 0..xs.len` bound).
|
|
/// Resolves through `pack_param_count`, which is populated when a comptime
|
|
/// call binds a pack name. A name with no active pack binding is not a
|
|
/// compile-time integer leaf here → null.
|
|
pub fn lookupPackLen(self: *Lowering, name: []const u8) ?i64 {
|
|
if (self.pack_param_count) |ppc| {
|
|
if (ppc.get(name)) |n| return @intCast(n);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Float-valued leaf for the shared float-expression evaluator: a name bound
|
|
/// to a NUMERIC module const whose compile-time value is a (non-integral)
|
|
/// float — the FLOAT counterpart of `lookupDimName`, routed through the SAME
|
|
/// `module_const_map` so the unified narrowing rule resolves a float-const
|
|
/// leaf (`F : f64 : 2.5`) exactly as it resolves an int-const leaf. Integer /
|
|
/// integral-float leaves and comptime int bindings are already resolved by the
|
|
/// `evalConstIntExpr` delegation inside `evalConstFloatExpr`; this surfaces the
|
|
/// non-integral float const so the rule can reject it.
|
|
pub fn lookupFloatName(self: *Lowering, name: []const u8) ?f64 {
|
|
if (self.moduleConstBareInvisible(name)) return null;
|
|
return program_index_mod.moduleConstFloat(&self.program_index.module_const_map, &self.module.types, name);
|
|
}
|
|
|
|
/// True iff `name` is a FLOAT-valued module const (`F : f64 : 2.5`,
|
|
/// `K : f64 : 4.0`, untyped `M :: 4.0`, untyped-EXPR `ME :: 4.0 + 1.0`). The
|
|
/// int folder's division arm consults this so a `/` with a float-const operand
|
|
/// is recognised as float division (issue 0095 / F0.11-6). Comptime / generic
|
|
/// value bindings are always integer-valued, so only the module-const table
|
|
/// can name a float.
|
|
pub fn nameIsFloatTyped(self: *Lowering, name: []const u8) bool {
|
|
if (self.moduleConstBareInvisible(name)) return false;
|
|
return program_index_mod.moduleConstIsFloatTyped(&self.program_index.module_const_map, &self.module.types, name);
|
|
}
|
|
|
|
/// Resolve a name to a compile-time integer across the three const tables.
|
|
fn comptimeIntNamed(self: *Lowering, name: []const u8) ?i64 {
|
|
if (self.comptime_constants.get(name)) |cv| switch (cv) {
|
|
.int_val => |iv| return iv,
|
|
else => {},
|
|
};
|
|
if (self.comptime_value_bindings) |cvb| {
|
|
if (cvb.get(name)) |v| return v;
|
|
}
|
|
// Folded req #1: gate the bare module const on source-aware visibility
|
|
// before reading the global map (see `moduleConstBareInvisible`).
|
|
if (self.moduleConstBareInvisible(name)) return null;
|
|
// The module-const branch is shared verbatim with the stateless
|
|
// registration-time resolver (`type_bridge`) so a `[N]T` dimension
|
|
// resolves to the same length on both paths (issue 0083).
|
|
return program_index_mod.moduleConstInt(&self.program_index.module_const_map, &self.module.types, name);
|
|
}
|
|
|
|
/// Folded req #1: TRUE iff `name` is a module const that is NOT reachable
|
|
/// bare from the querying module — the source-aware gate every Lowering-side
|
|
/// comptime `module_const_map` reader (`comptimeIntNamed` / `lookupFloatName`
|
|
/// / `nameIsFloatTyped`) consults before the global first-match. A
|
|
/// namespaced-only import's const must be qualified (`ns.X`); without this
|
|
/// gate a bare reference leaks into a comptime-scalar / array-dim position
|
|
/// through the global table (the int folder even falls back to the float
|
|
/// reader, so all three must gate). The value itself is still folded over the
|
|
/// global map, so a cross-module const CHAIN (`N :: M + 1`, M flat-imported)
|
|
/// resolves exactly as before; the stateless `type_bridge` registration path
|
|
/// keeps the global reader this step. A main-file body carries a null
|
|
/// `current_source_file` (it IS the root), so the querying module is
|
|
/// `main_file` there; a fully unwired index (no source at all) falls open.
|
|
fn moduleConstBareInvisible(self: *Lowering, name: []const u8) bool {
|
|
const from = self.current_source_file orelse self.main_file orelse return false;
|
|
var res = self.resolver();
|
|
const set = res.collectVisibleAuthors(name, from, .user_bare_flat);
|
|
defer if (set.flat.len > 0) self.alloc.free(set.flat);
|
|
if (set.own) |o| if (self.sourceHasModuleConst(o.source, name)) return false;
|
|
for (set.flat) |fa| if (self.sourceHasModuleConst(fa.source, name)) return false;
|
|
return true;
|
|
}
|
|
|
|
/// True iff `source`'s per-source const cache declares `name` (E0's
|
|
/// `module_consts_by_source` write side).
|
|
fn sourceHasModuleConst(self: *Lowering, source: []const u8, name: []const u8) bool {
|
|
const inner = self.program_index.module_consts_by_source.get(source) orelse return false;
|
|
return inner.contains(name);
|
|
}
|
|
|
|
/// Resolve a type node, checking type_bindings first for generic type params.
|
|
pub fn resolveTypeWithBindings(self: *Lowering, node: *const Node) TypeId {
|
|
// Pack-index in a type position: `$<pack>[<lit>]` resolves to the
|
|
// i-th element type of the active pack binding (step 3 of the
|
|
// variadic heterogeneous type packs feature). Unblocks parametric
|
|
// trampoline bodies (`(*void, $args[0]) -> $args[1]`) in stdlib's
|
|
// generic Into(Block) impl. OOB indices / a missing binding emit a
|
|
// diagnostic and return the `.unresolved` sentinel — never a plausible
|
|
// `.s64`, which would silently fabricate an 8-byte int.
|
|
if (node.data == .pack_index_type_expr) {
|
|
const pi = node.data.pack_index_type_expr;
|
|
if (self.pack_arg_types) |pat| {
|
|
if (pat.get(pi.pack_name)) |arg_tys| {
|
|
if (pi.index < arg_tys.len) return arg_tys[pi.index];
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, node.span, "pack-index type ${s}[{}] out of bounds: '{s}' has {} element{s}", .{
|
|
pi.pack_name, pi.index, pi.pack_name, arg_tys.len,
|
|
if (arg_tys.len == 1) @as([]const u8, "") else @as([]const u8, "s"),
|
|
});
|
|
}
|
|
return .unresolved;
|
|
}
|
|
}
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, node.span, "pack-index type ${s}[{}] used outside an active pack binding", .{
|
|
pi.pack_name, pi.index,
|
|
});
|
|
}
|
|
return .unresolved;
|
|
}
|
|
// `*Self` substitution inside foreign-class member declarations
|
|
// — both foreign and sx-defined — resolves to the class's own
|
|
// 0-field stub struct (i.e. the opaque Obj-C pointer type).
|
|
// This matches the Obj-C idiom where `self` IS the object.
|
|
// `self.field` access on sx-defined classes is rewritten by
|
|
// lowerFieldAccess to go through the `__sx_state` ivar
|
|
// (object_getIvar + struct_gep) when needed — see M1.2 A.3.
|
|
if (node.data == .type_expr and std.mem.eql(u8, node.data.type_expr.name, "Self")) {
|
|
if (self.current_foreign_class) |fcd| {
|
|
if (fcd.runtime == .objc_class or fcd.runtime == .objc_protocol) {
|
|
return self.foreignClassStructType(fcd);
|
|
}
|
|
}
|
|
}
|
|
// Structural type shapes — `*T`, `[*]T`, `[]T`, `?T`, `[N]T`, functions,
|
|
// PLAIN closures, and PLAIN tuples — are owned by
|
|
// `TypeResolver.resolveCompound` (A2.3b). Element types recurse through
|
|
// the full stateful resolver (`resolveInner` → here) so generic structs
|
|
// / bindings keep their resolution. resolveCompound returns null only
|
|
// for the pack-shaped forms (`Closure(..p)`, spread tuples) below.
|
|
if (TypeResolver.resolveCompound(&self.module.types, node, self)) |t| return t;
|
|
// Generic type-param binding (`$T`, or a bare return-type `T` without
|
|
// the `$` prefix) — owned by TypeResolver via the explicit ResolveEnv.
|
|
// The parameterized / call / closure / function arms that used to live
|
|
// here were redundant with the unconditional handling just below (both
|
|
// read the active bindings through the same resolvers), so they're gone.
|
|
if (TypeResolver.resolveBinding(node, self.resolveEnv())) |t| return t;
|
|
// Even without active type_bindings, handle parameterized types with struct templates
|
|
if (node.data == .parameterized_type_expr) {
|
|
return self.resolveParameterizedWithBindings(&node.data.parameterized_type_expr, node.span);
|
|
}
|
|
if (node.data == .call) {
|
|
return self.resolveTypeCallWithBindings(&node.data.call);
|
|
}
|
|
// Plain structural shapes were handled by resolveCompound above. What
|
|
// reaches here is the PACK-shaped subset, owned by `PackResolver`
|
|
// (packs.zig): pack-shaped `Closure(..p)` and spread tuples. (Functions
|
|
// are never pack-shaped at the type level — resolveCompound owns them
|
|
// all, so there is no function arm here.)
|
|
switch (node.data) {
|
|
.closure_type_expr => |ct| {
|
|
return self.packResolver().resolveClosureTypeWithBindings(&ct);
|
|
},
|
|
.tuple_type_expr => |tt| {
|
|
return self.packResolver().resolveTupleTypeWithBindings(&tt);
|
|
},
|
|
// `(..$Ts)` in a type position (e.g. a struct field) parses as a
|
|
// tuple LITERAL whose elements include a pack spread; PackResolver
|
|
// expands it (returns null when no spread, so we fall through).
|
|
.tuple_literal => |tl| {
|
|
if (self.packResolver().resolveTupleLiteralType(&tl)) |t| return t;
|
|
},
|
|
else => {},
|
|
}
|
|
// An unbound generic type param (`$R` with no active binding) must not
|
|
// fabricate an empty-struct stub — that surfaces as `R{}` downstream.
|
|
// Return `.unresolved` so callers (e.g. lambda return-type inference,
|
|
// call-site `$R` inference) treat it as not-yet-known.
|
|
if (node.data == .type_expr and node.data.type_expr.is_generic) {
|
|
// A VALUE param (`$N: u32`) named in a TYPE position (`x: N`) is bound
|
|
// to a compile-time integer, not a type, so `resolveBinding` above
|
|
// found no TYPE binding and it lands here. In the MAIN file the
|
|
// `UnknownTypeChecker` owns this diagnostic (and halts before codegen);
|
|
// an imported template's fields are resolved in the template's source
|
|
// context (see `instantiateGenericStruct`) and are checker-trusted, so
|
|
// this leaf is the sole guard — emit the tailored hint, mirroring the
|
|
// imported `.undeclared` leaf. A genuinely-unbound type param (`$R`,
|
|
// no value binding) stays a silent `.unresolved`.
|
|
const nm = node.data.type_expr.name;
|
|
const bound_value = if (self.comptime_value_bindings) |cvb| cvb.contains(nm) else false;
|
|
if (bound_value) {
|
|
const is_main = if (self.main_file) |mf|
|
|
(if (self.current_source_file) |csf| std.mem.eql(u8, csf, mf) else true)
|
|
else
|
|
true;
|
|
if (!is_main) {
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, node.span, "'{s}' is a value parameter, not a type; introduce a generic type parameter with `${s}: Type`", .{ nm, nm });
|
|
}
|
|
}
|
|
return .unresolved;
|
|
}
|
|
// Bare type names resolve through the source-aware `selectNominalLeaf`
|
|
// (E1): the nominal author is selected over the ONE graph-walk collector
|
|
// and resolved against the source-keyed caches, not the global
|
|
// `findByName` first-match / global alias map. Other node kinds (inline
|
|
// type decls, error types) still route through type_bridge, which reads
|
|
// the global compat maps (cut over in a later phase).
|
|
switch (node.data) {
|
|
.type_expr => |te| return self.resolveNominalLeaf(te.name, te.is_raw, node.span),
|
|
.identifier => |id| return self.resolveNominalLeaf(id.name, id.is_raw, node.span),
|
|
// A non-spread tuple literal in a type position is a tuple-type
|
|
// literal (`(s32, s32)`); validate its elements are types and reject
|
|
// non-type elements loudly (issue 0067).
|
|
.tuple_literal => return self.resolveTupleLiteralTypeArg(node),
|
|
else => return type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map),
|
|
}
|
|
}
|
|
|
|
/// Bind a `PackResolver` to this Lowering for pack-aware TYPE-position
|
|
/// resolution (`Closure(..p)` / `(Params...) -> R` / `(..xs)` tuples and
|
|
/// their `..xs.T` projections). A2.3 moved that logic into `packs.zig`.
|
|
fn packResolver(self: *Lowering) PackResolver {
|
|
return .{ .l = self };
|
|
}
|
|
|
|
/// Resolve a `Vector(N, T)` lane count to a positive compile-time integer
|
|
/// through the shared `program_index.foldDimU32` folder (min 1) — so a literal
|
|
/// (`Vector(4, f32)`), a module/generic const (`Vector(N, f32)`), and a const
|
|
/// expression (`Vector(M + 1, f32)`) all resolve identically, and the i64→u32
|
|
/// narrowing is range-checked (an oversized lane diagnoses instead of
|
|
/// panicking — issue 0087). A non-const lane (`Vector(get(), f32)`) or a
|
|
/// non-positive one emits a clean diagnostic and returns null; the caller
|
|
/// yields `.unresolved` rather than fabricating a `<0 x float>` lane count
|
|
/// that crashes LLVM verification.
|
|
fn resolveVectorLane(self: *Lowering, lane_node: *const Node) ?u32 {
|
|
switch (program_index_mod.foldDimU32(lane_node, self, 1)) {
|
|
.ok => |n| return n,
|
|
.too_large => |v| {
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, lane_node.span, "Vector lane count {} does not fit in u32", .{v});
|
|
return null;
|
|
},
|
|
.non_integral_float => |v| {
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, lane_node.span, "Vector lane count must be an integer, but '{d}' is a non-integral float", .{v});
|
|
return null;
|
|
},
|
|
.not_const, .below_min => {
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, lane_node.span, "Vector lane count must be a positive compile-time integer constant", .{});
|
|
return null;
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Resolve a generic value-param argument (`$K: u32`) to its compile-time
|
|
/// integer AND verify it fits the param's declared integer type. The folded
|
|
/// value is bound and mangled into the instantiation name, so a module/generic
|
|
/// const arg (`Vec(N, f32)`), a const expression (`Make(M + 1, s64)`), an
|
|
/// integral float (`Box(4.0)` → 4), and a literal (`Vec(3, f32)`) all bind the
|
|
/// same value a literal would. An out-of-range arg (`Box(5_000_000_000)` for a
|
|
/// `u32` param) or a non-const arg emits a clean diagnostic and returns null;
|
|
/// the caller bails rather than binding a truncated / fabricated value under a
|
|
/// wrong mangled name.
|
|
///
|
|
/// `type_name` is the param's declared constraint type (`"u32"`, null if
|
|
/// unknown). A `u32` count routes through the shared
|
|
/// `program_index.foldDimU32` — the SAME fold-and-narrow gate an array dim /
|
|
/// Vector lane uses — so the documented "single u32 gate for value-param
|
|
/// counts" holds; any other integer type range-checks against
|
|
/// `program_index.intTypeRange`; an unrecognised type folds without bounding.
|
|
fn resolveValueParamArg(self: *Lowering, arg_node: *const Node, param_name: []const u8, type_name: ?[]const u8) ?i64 {
|
|
// Resolve an ALIASED integer constraint (`$K: Count` where `Count :: u32`,
|
|
// `$K: Small` where `Small :: s8`) to its underlying builtin so the range
|
|
// gate below treats it exactly like `$K: u32` / `$K: s8` (issue 0083 — an
|
|
// alias previously slipped past `intTypeRange`, so `Box(5_000_000_000)`
|
|
// with `$K: Count` bound a truncated value). A non-integer / unrecognised
|
|
// constraint yields null → no range bound (fold only), as before.
|
|
const tn_canon: ?[]const u8 = if (type_name) |tn| self.canonicalIntConstraintName(tn) else null;
|
|
if (tn_canon) |tn| {
|
|
if (std.mem.eql(u8, tn, "u32")) {
|
|
switch (program_index_mod.foldDimU32(arg_node, self, 0)) {
|
|
.ok => |n| return n,
|
|
.not_const, .non_integral_float => {
|
|
self.diagValueParamNotConst(arg_node, param_name);
|
|
return null;
|
|
},
|
|
.below_min => |v| {
|
|
self.diagValueParamRange(arg_node, param_name, tn, v);
|
|
return null;
|
|
},
|
|
.too_large => |v| {
|
|
self.diagValueParamRange(arg_node, param_name, tn, v);
|
|
return null;
|
|
},
|
|
}
|
|
}
|
|
}
|
|
// Non-`u32` integer constraint: fold through the SAME unified count fold
|
|
// so an integral float arg (`Box(4.0)`, `Make(F + 1.5, ...)`) binds the
|
|
// integer it equals, exactly as the `u32` gate above does; a non-integral
|
|
// float / non-const arg is not a valid count.
|
|
const v = switch (program_index_mod.foldCountI64(arg_node, self)) {
|
|
.int => |iv| iv,
|
|
.non_integral, .not_const => {
|
|
self.diagValueParamNotConst(arg_node, param_name);
|
|
return null;
|
|
},
|
|
};
|
|
if (tn_canon) |tn| {
|
|
if (program_index_mod.intTypeRange(tn)) |r| {
|
|
if (v < r.min or v > r.max) {
|
|
self.diagValueParamRange(arg_node, param_name, tn, v);
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
return v;
|
|
}
|
|
|
|
/// Resolve a generic value-param constraint type NAME to its canonical builtin
|
|
/// integer type name, chasing a type alias (`Count :: u32` → "u32",
|
|
/// `Small :: s8` → "s8") so an ALIASED integer constraint range-checks exactly
|
|
/// like the builtin it names. Returns the name unchanged when it is already a
|
|
/// builtin integer; null when it isn't an integer type (directly or via alias)
|
|
/// — the caller then folds without a range bound rather than guessing. The
|
|
/// alias map + type table are the same single sources every other resolver
|
|
/// reads, so this can't diverge from how the alias is laid out elsewhere.
|
|
fn canonicalIntConstraintName(self: *Lowering, name: []const u8) ?[]const u8 {
|
|
if (program_index_mod.intTypeRange(name) != null) return name;
|
|
if (self.program_index.type_alias_map.get(name)) |tid| {
|
|
const canon = self.module.types.typeName(tid);
|
|
if (program_index_mod.intTypeRange(canon) != null) return canon;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
fn diagValueParamNotConst(self: *Lowering, arg_node: *const Node, param_name: []const u8) void {
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, arg_node.span, "generic value parameter '{s}' must be a compile-time integer constant", .{param_name});
|
|
}
|
|
|
|
fn diagValueParamRange(self: *Lowering, arg_node: *const Node, param_name: []const u8, type_name: []const u8, value: i64) void {
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, arg_node.span, "value {} does not fit in {s} parameter {s}", .{ value, type_name, param_name });
|
|
}
|
|
|
|
/// Resolve a .call node that represents a type constructor (e.g., List(T), Vector(N, T)).
|
|
fn resolveTypeCallWithBindings(self: *Lowering, cl: *const ast.Call) TypeId {
|
|
const callee_name: []const u8 = switch (cl.callee.data) {
|
|
.identifier => |id| id.name,
|
|
.field_access => |fa| fa.field,
|
|
else => return .unresolved,
|
|
};
|
|
// Built-in: Vector(N, T)
|
|
if (std.mem.eql(u8, callee_name, "Vector") and cl.args.len == 2) {
|
|
const length = self.resolveVectorLane(cl.args[0]) orelse return .unresolved;
|
|
const elem = self.resolveTypeWithBindings(cl.args[1]);
|
|
return self.module.types.vectorOf(elem, length);
|
|
}
|
|
// User-defined generic struct
|
|
if (self.program_index.struct_template_map.getPtr(callee_name)) |tmpl| {
|
|
return self.instantiateGenericStruct(tmpl, cl.args);
|
|
}
|
|
// User-defined type-returning function: Complex(u32), Sx(f32)
|
|
// Also resolve via scope fn_names (local functions get mangled names)
|
|
const resolved_name = if (self.scope) |scope| (scope.lookupFn(callee_name) orelse callee_name) else callee_name;
|
|
if (self.program_index.fn_ast_map.get(resolved_name)) |fd| {
|
|
if (fd.type_params.len > 0) {
|
|
if (self.instantiateTypeFunction(callee_name, callee_name, fd, cl.args)) |ty| {
|
|
return ty;
|
|
}
|
|
}
|
|
}
|
|
// Try as a named type
|
|
const name_id = self.module.types.internString(callee_name);
|
|
return self.module.types.findByName(name_id) orelse .unresolved;
|
|
}
|
|
|
|
/// Resolve a parameterized type expr, substituting bindings for type/value params.
|
|
/// Handles both built-in types (Vector) and user-defined generic structs.
|
|
/// `span` locates the reference for the unresolved-base diagnostic.
|
|
fn resolveParameterizedWithBindings(self: *Lowering, pt: *const ast.ParameterizedTypeExpr, span: ?ast.Span) TypeId {
|
|
const base_name = if (std.mem.lastIndexOfScalar(u8, pt.name, '.')) |dot| pt.name[dot + 1 ..] else pt.name;
|
|
const table = &self.module.types;
|
|
|
|
// Vector(N, T) — built-in parameterized type. A backtick raw base
|
|
// (`` `Vector(…) ``) is the LITERAL user type named `Vector`, so it
|
|
// skips this intrinsic and resolves through the template map (0089).
|
|
if (!pt.is_raw and std.mem.eql(u8, base_name, "Vector")) {
|
|
if (pt.args.len == 2) {
|
|
const length = self.resolveVectorLane(pt.args[0]) orelse return .unresolved;
|
|
const elem = self.resolveTypeWithBindings(pt.args[1]);
|
|
return table.vectorOf(elem, length);
|
|
}
|
|
}
|
|
|
|
// User-defined generic struct: look up template and instantiate
|
|
if (self.program_index.struct_template_map.getPtr(base_name)) |tmpl| {
|
|
return self.instantiateGenericStruct(tmpl, pt.args);
|
|
}
|
|
|
|
// Parameterized protocol used as a value type (`VL(s64)`): materialize a
|
|
// 16-byte protocol value with the type-arg bound (not a 0-field stub).
|
|
if (self.program_index.protocol_ast_map.get(base_name)) |pd| {
|
|
if (pd.type_params.len > 0) {
|
|
return self.instantiateParamProtocol(pd, pt.args);
|
|
}
|
|
}
|
|
|
|
// User-defined type-returning function used as a TYPE annotation
|
|
// (`b : Make(N, s64)` where `Make :: ($K: u32, $T: Type) -> Type`). The
|
|
// `.call`-node path (`resolveTypeCallWithBindings`) already routes here;
|
|
// a `parameterized_type_expr` must too, or the function name falls through
|
|
// to the empty-struct stub below and `b.field` / `b.len` fails.
|
|
const resolved_name = if (self.scope) |scope| (scope.lookupFn(base_name) orelse base_name) else base_name;
|
|
if (self.program_index.fn_ast_map.get(resolved_name)) |fd| {
|
|
if (fd.type_params.len > 0) {
|
|
if (self.instantiateTypeFunction(base_name, base_name, fd, pt.args)) |ty| {
|
|
return ty;
|
|
}
|
|
}
|
|
}
|
|
|
|
// The base names no known type constructor — not Vector, not a generic
|
|
// struct template, not a parameterized protocol, not a type-returning
|
|
// function. A silent 0-field stub here would mis-size every downstream
|
|
// `b.field` / `b.len`; emit the diagnostic and poison with `.unresolved`
|
|
// (the `.call`-node sibling `resolveTypeCallWithBindings` already poisons).
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, span, "unknown type '{s}'", .{base_name});
|
|
return .unresolved;
|
|
}
|
|
|
|
/// Instantiate a generic struct template with concrete args.
|
|
/// E.g., Vec(3, f32) → struct Vec__3_f32 { data: Vector(3, f32) }
|
|
fn instantiateGenericStruct(self: *Lowering, tmpl: *const StructTemplate, args: []const *const Node) TypeId {
|
|
const table = &self.module.types;
|
|
|
|
// Build mangled name dynamically: StructName__arg1_arg2
|
|
var name_parts = std.ArrayList(u8).empty;
|
|
name_parts.appendSlice(self.alloc, tmpl.name) catch {};
|
|
|
|
// Bind type params to args and build name suffix
|
|
const saved_type_bindings = self.type_bindings;
|
|
const saved_value_bindings = self.comptime_value_bindings;
|
|
const saved_pack_bindings = self.pack_bindings;
|
|
const saved_pack_arg_types = self.pack_arg_types;
|
|
var tb = std.StringHashMap(TypeId).init(self.alloc);
|
|
var cvb = std.StringHashMap(i64).init(self.alloc);
|
|
var pb = std.StringHashMap([]const TypeId).init(self.alloc);
|
|
|
|
for (tmpl.type_params, 0..) |tp, i| {
|
|
if (i >= args.len) break;
|
|
|
|
// `..$Ts: []Type` — bind the REMAINING args as a type pack.
|
|
if (tp.is_variadic) {
|
|
var pack_tys = std.ArrayList(TypeId).empty;
|
|
for (args[i..]) |a| {
|
|
// A spread arg `..sources.T` expands to the source pack's
|
|
// per-element (projected) types; a plain arg is one type.
|
|
if (a.data == .spread_expr) {
|
|
if (self.packResolver().packTypeElems(a.data.spread_expr.operand)) |elems| {
|
|
defer self.alloc.free(elems);
|
|
for (elems) |ty| {
|
|
pack_tys.append(self.alloc, ty) catch {};
|
|
name_parts.appendSlice(self.alloc, "__") catch {};
|
|
name_parts.appendSlice(self.alloc, self.formatTypeName(ty)) catch {};
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
const ty = self.resolveTypeWithBindings(a);
|
|
pack_tys.append(self.alloc, ty) catch {};
|
|
name_parts.appendSlice(self.alloc, "__") catch {};
|
|
name_parts.appendSlice(self.alloc, self.formatTypeName(ty)) catch {};
|
|
}
|
|
pb.put(tp.name, pack_tys.toOwnedSlice(self.alloc) catch &.{}) catch {};
|
|
break; // a pack param is always last
|
|
}
|
|
|
|
name_parts.appendSlice(self.alloc, "__") catch {};
|
|
|
|
if (tp.is_type_param) {
|
|
const ty = self.resolveTypeWithBindings(args[i]);
|
|
tb.put(tp.name, ty) catch {};
|
|
const tname = self.formatTypeName(ty);
|
|
name_parts.appendSlice(self.alloc, tname) catch {};
|
|
} else {
|
|
// Value param (e.g., $N: u32) — fold to a compile-time integer
|
|
// and range-check against its declared type.
|
|
const val = self.resolveValueParamArg(args[i], tp.name, tp.value_type) orelse return .unresolved;
|
|
cvb.put(tp.name, val) catch {};
|
|
var val_buf: [32]u8 = undefined;
|
|
const val_str = std.fmt.bufPrint(&val_buf, "{d}", .{val}) catch "0";
|
|
name_parts.appendSlice(self.alloc, val_str) catch {};
|
|
}
|
|
}
|
|
|
|
const mangled_name = name_parts.items;
|
|
|
|
// Check if already instantiated
|
|
const name_id = table.internString(mangled_name);
|
|
if (table.findByName(name_id)) |existing| {
|
|
// Already registered — check if it has fields
|
|
const info = table.get(existing);
|
|
if (info == .@"struct" and info.@"struct".fields.len > 0) {
|
|
return existing;
|
|
}
|
|
}
|
|
|
|
// Set up bindings and resolve fields. `pack_bindings` makes a
|
|
// pack-shaped field type like `(..$Ts)` resolve to the bound type list.
|
|
self.type_bindings = tb;
|
|
self.comptime_value_bindings = cvb;
|
|
self.pack_bindings = pb;
|
|
self.pack_arg_types = pb;
|
|
|
|
// Resolve the field type nodes in the TEMPLATE's source context, not the
|
|
// (possibly cross-module) instantiation site. A field naming a type
|
|
// visible only in the template's module then resolves correctly, and the
|
|
// source-aware nominal leaf classifies main vs imported by the TEMPLATE's
|
|
// file — so an undeclared field type (`y: Missing`) or a value param used
|
|
// as a type (`x: N` for `$N: u32`) is diagnosed at the right authority
|
|
// (the leaf for an imported template, the `UnknownTypeChecker` for a
|
|
// main-file one) instead of silently fabricating a stub / poisoning with
|
|
// `.unresolved` that panics at LLVM emission.
|
|
const saved_src = self.current_source_file;
|
|
const saved_diag_src = if (self.diagnostics) |d| d.current_source_file else null;
|
|
if (tmpl.source_file) |sf| {
|
|
self.current_source_file = sf;
|
|
if (self.diagnostics) |d| d.current_source_file = sf;
|
|
}
|
|
|
|
var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty;
|
|
for (tmpl.field_names, tmpl.field_type_nodes) |fname, ftype_node| {
|
|
const field_ty = self.resolveTypeWithBindings(ftype_node);
|
|
fields.append(self.alloc, .{
|
|
.name = table.internString(fname),
|
|
.ty = field_ty,
|
|
}) catch unreachable;
|
|
}
|
|
|
|
self.current_source_file = saved_src;
|
|
if (self.diagnostics) |d| d.current_source_file = saved_diag_src;
|
|
|
|
// Restore bindings
|
|
self.type_bindings = saved_type_bindings;
|
|
self.comptime_value_bindings = saved_value_bindings;
|
|
self.pack_bindings = saved_pack_bindings;
|
|
self.pack_arg_types = saved_pack_arg_types;
|
|
|
|
// Register the monomorphized struct
|
|
const info: types.TypeInfo = .{ .@"struct" = .{ .name = name_id, .fields = fields.items } };
|
|
const id = if (table.findByName(name_id)) |existing| existing else table.intern(info);
|
|
table.updatePreservingKey(id, info);
|
|
|
|
// Bind the template name to this concrete instance so a method's
|
|
// `self: *Combined` (the template name) resolves to `*Combined__s64_s64`
|
|
// — otherwise `self.field` hits the 0-field generic stub.
|
|
tb.put(tmpl.name, id) catch {};
|
|
|
|
// Store the type bindings and template name for method resolution
|
|
const owned_mangled = self.alloc.dupe(u8, mangled_name) catch return id;
|
|
self.struct_instance_bindings.put(owned_mangled, tb) catch {};
|
|
self.struct_instance_template.put(owned_mangled, tmpl.name) catch {};
|
|
|
|
return id;
|
|
}
|
|
|
|
/// Instantiate a type-returning function: `Foo :: Complex(u32)` where
|
|
/// `Complex :: ($T:Type) -> Type { return struct { value: T; count: u32; }; }`
|
|
/// Walks the function body to find the returned struct/enum, resolves field types
|
|
/// with the provided type bindings, and registers the result.
|
|
fn instantiateTypeFunction(self: *Lowering, alias_name: []const u8, template_name: []const u8, fd: *const ast.FnDecl, args: []const *const Node) ?TypeId {
|
|
const table = &self.module.types;
|
|
|
|
// Build type bindings from params + args
|
|
const saved_type_bindings = self.type_bindings;
|
|
const saved_value_bindings = self.comptime_value_bindings;
|
|
var tb = std.StringHashMap(TypeId).init(self.alloc);
|
|
var cvb = std.StringHashMap(i64).init(self.alloc);
|
|
|
|
// Build mangled name
|
|
var name_parts = std.ArrayList(u8).empty;
|
|
name_parts.appendSlice(self.alloc, template_name) catch {};
|
|
|
|
for (fd.type_params, 0..) |tp, i| {
|
|
if (i >= args.len) break;
|
|
name_parts.appendSlice(self.alloc, "__") catch {};
|
|
|
|
// Check if this is a Type param ($T: Type) or a value param ($N: u32)
|
|
const is_type_param = if (tp.constraint.data == .type_expr)
|
|
std.mem.eql(u8, tp.constraint.data.type_expr.name, "Type")
|
|
else
|
|
true; // default to type param
|
|
|
|
if (is_type_param) {
|
|
const ty = self.resolveTypeWithBindings(args[i]);
|
|
tb.put(tp.name, ty) catch {};
|
|
const tname = self.formatTypeName(ty);
|
|
name_parts.appendSlice(self.alloc, tname) catch {};
|
|
} else {
|
|
// Value param (e.g., $N: u32) — fold to a compile-time integer
|
|
// and range-check against its declared type. A failed bind has
|
|
// already diagnosed itself, so poison to `.unresolved` rather
|
|
// than `null`: `null` makes the caller fall through to the
|
|
// empty-struct placeholder named after the fn, which then
|
|
// cascades a bogus `field not found` on any later access. The
|
|
// struct binder (`instantiateGenericStruct`) poisons the same way.
|
|
const vp_type: ?[]const u8 = if (tp.constraint.data == .type_expr) tp.constraint.data.type_expr.name else null;
|
|
const val = self.resolveValueParamArg(args[i], tp.name, vp_type) orelse return .unresolved;
|
|
cvb.put(tp.name, val) catch {};
|
|
var val_buf: [32]u8 = undefined;
|
|
const val_str = std.fmt.bufPrint(&val_buf, "{d}", .{val}) catch "0";
|
|
name_parts.appendSlice(self.alloc, val_str) catch {};
|
|
}
|
|
}
|
|
|
|
const mangled_name = name_parts.items;
|
|
|
|
// Check if already instantiated
|
|
const mangled_name_id = table.internString(mangled_name);
|
|
if (table.findByName(mangled_name_id)) |existing| {
|
|
const info = table.get(existing);
|
|
if ((info == .@"struct" and info.@"struct".fields.len > 0) or info == .@"union" or info == .tagged_union) {
|
|
return existing;
|
|
}
|
|
}
|
|
|
|
// Activate bindings
|
|
self.type_bindings = tb;
|
|
self.comptime_value_bindings = cvb;
|
|
defer {
|
|
self.type_bindings = saved_type_bindings;
|
|
self.comptime_value_bindings = saved_value_bindings;
|
|
}
|
|
|
|
// Resolve the type fn's body (inline struct/union fields, or the returned
|
|
// type expression) in its OWN module (E4), so a 2-flat-hop library type
|
|
// named there is bare-visible — not the cross-module call site. The arg
|
|
// exprs above were already resolved in the caller's context.
|
|
const saved_tf_src = self.current_source_file;
|
|
defer self.setCurrentSourceFile(saved_tf_src);
|
|
if (fd.body.source_file) |src| self.setCurrentSourceFile(src);
|
|
|
|
// Determine if alias_name is a real alias (e.g., "Foo" for "Complex(u32)")
|
|
// or just the template name itself (inline use like "Sx(f32)")
|
|
const has_alias = !std.mem.eql(u8, alias_name, template_name);
|
|
|
|
// Try struct first
|
|
if (findStructInBody(fd.body)) |struct_decl| {
|
|
// Resolve struct fields with type bindings active
|
|
var struct_fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty;
|
|
for (struct_decl.field_names, struct_decl.field_types) |fname, ftype_node| {
|
|
const field_ty = self.resolveTypeWithBindings(ftype_node);
|
|
struct_fields.append(self.alloc, .{
|
|
.name = table.internString(fname),
|
|
.ty = field_ty,
|
|
}) catch {};
|
|
}
|
|
|
|
// Always register under mangled name
|
|
const mangled_info: types.TypeInfo = .{ .@"struct" = .{
|
|
.name = mangled_name_id,
|
|
.fields = struct_fields.items,
|
|
} };
|
|
const mangled_id = if (table.findByName(mangled_name_id)) |existing| existing else table.intern(mangled_info);
|
|
table.updatePreservingKey(mangled_id, mangled_info);
|
|
|
|
// If there's a real alias, also register under alias name and in alias map
|
|
if (has_alias) {
|
|
const alias_name_id = table.internString(alias_name);
|
|
const alias_info: types.TypeInfo = .{ .@"struct" = .{
|
|
.name = alias_name_id,
|
|
.fields = struct_fields.items,
|
|
} };
|
|
const alias_id = if (table.findByName(alias_name_id)) |existing| existing else table.intern(alias_info);
|
|
table.updatePreservingKey(alias_id, alias_info);
|
|
|
|
// Store defaults if any
|
|
if (struct_decl.field_defaults.len > 0) {
|
|
self.struct_defaults_map.put(alias_name, struct_decl.field_defaults) catch {};
|
|
}
|
|
|
|
return alias_id;
|
|
}
|
|
|
|
return mangled_id;
|
|
}
|
|
|
|
// Try tagged enum/union
|
|
if (findUnionInBody(fd.body)) |enum_decl| {
|
|
return self.instantiateTypeUnion(if (has_alias) alias_name else mangled_name, mangled_name, &enum_decl);
|
|
}
|
|
|
|
// General case: the body returns a TYPE EXPRESSION that is not an inline
|
|
// struct/union/enum — `return [K]T`, `Vector(K, T)`, `*T`, an alias, etc.
|
|
// Resolve it with the value/type bindings active (so `[K]T` folds K to a
|
|
// compile-time integer). The result is interned structurally, so
|
|
// `Make(N, s64)`, `Make(3, s64)`, and `Make(M + 1, s64)` all yield the
|
|
// same TypeId. `.unresolved` means the return wasn't a type expression
|
|
// (e.g. a value-returning function in a type position) → fall through to
|
|
// the caller's fallback rather than fabricating a type.
|
|
if (findReturnTypeExpr(fd.body)) |ret_node| {
|
|
const ty = self.resolveTypeWithBindings(ret_node);
|
|
if (ty != .unresolved) return ty;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// The type expression a type-returning function yields: the value of its
|
|
/// `return` (block body) or the bare expression (arrow body / `=> [K]T`).
|
|
/// Used for a non-struct/union return shape, which the struct/union body
|
|
/// walkers above don't match.
|
|
fn findReturnTypeExpr(body: *const Node) ?*const Node {
|
|
if (body.data == .block) {
|
|
for (body.data.block.stmts) |stmt| {
|
|
if (stmt.data == .return_stmt) return stmt.data.return_stmt.value;
|
|
}
|
|
return null;
|
|
}
|
|
return body;
|
|
}
|
|
|
|
/// Instantiate a tagged enum from a type function body.
|
|
fn instantiateTypeUnion(self: *Lowering, alias_name: []const u8, mangled_name: []const u8, ed: *const ast.EnumDecl) ?TypeId {
|
|
const table = &self.module.types;
|
|
|
|
// Build variant fields (tagged enum variants stored as StructInfo.Field)
|
|
var variant_fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty;
|
|
for (ed.variant_names, 0..) |vname, i| {
|
|
const payload_ty: TypeId = if (i < ed.variant_types.len and ed.variant_types[i] != null)
|
|
self.resolveTypeWithBindings(ed.variant_types[i].?)
|
|
else
|
|
.void;
|
|
variant_fields.append(self.alloc, .{
|
|
.name = table.internString(vname),
|
|
.ty = payload_ty,
|
|
}) catch {};
|
|
}
|
|
|
|
const alias_name_id = table.internString(alias_name);
|
|
const info: types.TypeInfo = .{ .tagged_union = .{
|
|
.name = alias_name_id,
|
|
.fields = variant_fields.items,
|
|
.tag_type = .s64,
|
|
} };
|
|
const id = if (table.findByName(alias_name_id)) |existing| existing else table.intern(info);
|
|
table.updatePreservingKey(id, info);
|
|
|
|
// Also register under mangled name
|
|
if (!std.mem.eql(u8, alias_name, mangled_name)) {
|
|
const mangled_name_id = table.internString(mangled_name);
|
|
const mangled_info: types.TypeInfo = .{ .tagged_union = .{
|
|
.name = mangled_name_id,
|
|
.fields = variant_fields.items,
|
|
.tag_type = .s64,
|
|
} };
|
|
const mid = if (table.findByName(mangled_name_id)) |existing| existing else table.intern(mangled_info);
|
|
table.updatePreservingKey(mid, mangled_info);
|
|
}
|
|
|
|
return id;
|
|
}
|
|
|
|
/// Walk an AST body to find a struct declaration (from `return struct { ... }` or bare struct expr).
|
|
fn findStructInBody(body: *const Node) ?ast.StructDecl {
|
|
if (body.data == .struct_decl) return body.data.struct_decl;
|
|
if (body.data == .block) {
|
|
for (body.data.block.stmts) |stmt| {
|
|
if (stmt.data == .return_stmt) {
|
|
if (stmt.data.return_stmt.value) |val| {
|
|
if (val.data == .struct_decl) return val.data.struct_decl;
|
|
}
|
|
}
|
|
if (stmt.data == .struct_decl) return stmt.data.struct_decl;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Walk an AST body to find a tagged enum declaration.
|
|
fn findUnionInBody(body: *const Node) ?ast.EnumDecl {
|
|
const isTaggedEnum = struct {
|
|
fn check(node: *const Node) ?ast.EnumDecl {
|
|
if (node.data == .enum_decl and node.data.enum_decl.variant_types.len > 0) {
|
|
return node.data.enum_decl;
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
if (isTaggedEnum.check(body)) |ed| return ed;
|
|
const stmts = if (body.data == .block) body.data.block.stmts else return null;
|
|
for (stmts) |stmt| {
|
|
if (stmt.data == .return_stmt) {
|
|
if (stmt.data.return_stmt.value) |val| {
|
|
if (isTaggedEnum.check(val)) |ed| return ed;
|
|
}
|
|
}
|
|
if (isTaggedEnum.check(stmt)) |ed| return ed;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// ── Type registration ───────────────────────────────────────────
|
|
|
|
/// Register a struct declaration's fields and methods in the IR type table.
|
|
/// Register a `Foo :: error { A, B }` declaration as an error-set type.
|
|
/// Rejects an empty set here (sema gate) since type_bridge has no
|
|
/// diagnostics; non-empty sets are interned via type_bridge.
|
|
fn registerErrorSetDecl(self: *Lowering, node: *const Node) void {
|
|
const esd = node.data.error_set_decl;
|
|
if (esd.tag_names.len == 0) {
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, node.span, "error set '{s}' must declare at least one tag", .{esd.name});
|
|
}
|
|
return;
|
|
}
|
|
_ = type_bridge.resolveAstType(node, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
|
}
|
|
|
|
/// The `nominal_id` stamped on a nominal `TypeInfo` (0 for non-nominal /
|
|
/// structural). Reading it back lets a re-registration preserve the slot's
|
|
/// existing key when refreshing a forward-stubbed body.
|
|
fn nominalIdOf(info: types.TypeInfo) u32 {
|
|
return switch (info) {
|
|
.@"struct" => |s| s.nominal_id,
|
|
.@"enum" => |e| e.nominal_id,
|
|
.@"union" => |u| u.nominal_id,
|
|
.tagged_union => |u| u.nominal_id,
|
|
.error_set => |e| e.nominal_id,
|
|
else => 0,
|
|
};
|
|
}
|
|
|
|
/// Return `info` with its nominal arm's `nominal_id` set to `nid` (a no-op for
|
|
/// non-nominal infos). Used to build the key-matching body for
|
|
/// `updatePreservingKey` after a shadow author interned at a nonzero id.
|
|
fn stampNominalId(info: types.TypeInfo, nid: u32) types.TypeInfo {
|
|
var out = info;
|
|
switch (out) {
|
|
.@"struct" => |*s| s.nominal_id = nid,
|
|
.@"enum" => |*e| e.nominal_id = nid,
|
|
.@"union" => |*u| u.nominal_id = nid,
|
|
.tagged_union => |*u| u.nominal_id = nid,
|
|
.error_set => |*e| e.nominal_id = nid,
|
|
else => {},
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/// The top-level STRUCT decl a top-level node authors (a bare `struct_decl`, or
|
|
/// a `Name :: struct {...}` const wrapper), or null. Used by the genuine-shadow
|
|
/// scan in `scanDecls` to enumerate same-name struct authors uniformly.
|
|
fn topLevelStructDecl(decl: *const Node) ?*const ast.StructDecl {
|
|
return switch (decl.data) {
|
|
.struct_decl => &decl.data.struct_decl,
|
|
.const_decl => |cd| if (cd.value.data == .struct_decl) &cd.value.data.struct_decl else null,
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
/// Reserve a GENUINE same-name STRUCT shadow author's DISTINCT nominal slot
|
|
/// BEFORE any field resolves, so a self / forward / mutual reference to a shadow
|
|
/// name (`next: *Box`; `peer: *Node` where Node is a shadow declared later)
|
|
/// binds to ITS nominal TypeId via `type_decl_tids` instead of the global
|
|
/// findByName first-author fallback (issue 0105 / F1). Called only from the
|
|
/// `scanDecls` genuine-shadow pass, which has already established that ≥2
|
|
/// distinct struct decls author this name; ALL of them reserve — the FIRST at
|
|
/// id 0, the rest at fresh nonzero ids — so none falls through to the name-only
|
|
/// `findByName` (which, once a shadow is interned, no longer uniquely identifies
|
|
/// the first author). Idempotent per decl key: an already-reserved decl returns
|
|
/// before re-invoking `shadowNominalId`, so the shadow id is computed once.
|
|
/// Generic templates resolve lazily on instantiation and are skipped.
|
|
fn reserveShadowStructSlot(self: *Lowering, sd: *const ast.StructDecl) void {
|
|
if (sd.type_params.len > 0) return;
|
|
const table = &self.module.types;
|
|
const decl_key: *const anyopaque = @ptrCast(sd);
|
|
if (table.type_decl_tids.contains(decl_key)) return;
|
|
const name_id = table.internString(sd.name);
|
|
const nominal_id = self.shadowNominalId(name_id); // 0 for the first author, nonzero for the rest
|
|
const reserved = table.internNominal(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } }, nominal_id);
|
|
table.type_decl_tids.put(decl_key, reserved) catch {};
|
|
}
|
|
|
|
/// Register (or re-register) a top-level NAMED type decl under a per-source
|
|
/// nominal identity (E2), returning its TypeId. `decl_key` is the decl's
|
|
/// stable pointer (the import raw-facts identity); `info` carries the full
|
|
/// body; `nominal_id` is the slot's identity (0 for a single / first author,
|
|
/// nonzero for a later same-name shadow) — computed once by the caller
|
|
/// (`registerStructDecl`), which reuses the id reserved up-front in `scanDecls`
|
|
/// for a genuine shadow (so its fields' self / forward / mutual refs already
|
|
/// resolved against it). This stamps the id and records the `decl_key → TypeId`
|
|
/// map (`type_decl_tids`, the `fn_decl_fids` analogue).
|
|
///
|
|
/// A `nominal_id == 0` author adopts any forward-reference stub (`findByName`
|
|
/// orelse intern) — BYTE-IDENTICAL to pre-E2 registration. For a genuinely
|
|
/// multi-authored name, the FIRST source keeps id 0 and later sources get
|
|
/// fresh ids → DISTINCT TypeIds, so the authors no longer collapse last-wins
|
|
/// (issue 0105). Idempotent per `decl_key`: a re-registration — OR an up-front
|
|
/// shadow reservation — reuses the recorded slot, refreshing its body via
|
|
/// `updatePreservingKey` (key-stable because a struct's intern key is its
|
|
/// name + nominal id, not its fields).
|
|
fn internNamedTypeDecl(self: *Lowering, decl_key: *const anyopaque, name_id: types.StringId, info: types.TypeInfo, nominal_id: u32) TypeId {
|
|
const table = &self.module.types;
|
|
// Slot already recorded (re-registration, or a reserve-before-fields shadow
|
|
// reservation) → reuse its slot + nominal id, refresh the body.
|
|
if (table.type_decl_tids.get(decl_key)) |existing_id| {
|
|
table.updatePreservingKey(existing_id, stampNominalId(info, nominalIdOf(table.get(existing_id))));
|
|
return existing_id;
|
|
}
|
|
const id = if (nominal_id == 0)
|
|
(table.findByName(name_id) orelse table.internNominal(info, 0))
|
|
else
|
|
table.internNominal(info, nominal_id);
|
|
table.updatePreservingKey(id, stampNominalId(info, nominal_id));
|
|
table.type_decl_tids.put(decl_key, id) catch {};
|
|
return id;
|
|
}
|
|
|
|
/// The `nominal_id` to register a NAMED type author of `name_id` under. 0
|
|
/// unless `name_id` is authored as a named type by ≥2 distinct modules (a real
|
|
/// same-name shadow per the import facts): the FIRST source to register keeps
|
|
/// 0, each later source gets a fresh monotonic id. Gating on the import facts
|
|
/// keeps the single-author path at id 0 (byte-identical) even when one logical
|
|
/// type is re-registered from several `current_source_file` contexts.
|
|
fn shadowNominalId(self: *Lowering, name_id: types.StringId) u32 {
|
|
if (!self.nameHasMultipleTypeAuthors(self.module.types.getString(name_id))) return 0;
|
|
const src = self.current_source_file orelse self.main_file orelse "";
|
|
const gop = self.nominal_name_authors.getOrPut(name_id) catch return 0;
|
|
if (!gop.found_existing) {
|
|
gop.value_ptr.* = src;
|
|
return 0;
|
|
}
|
|
if (std.mem.eql(u8, gop.value_ptr.*, src)) return 0;
|
|
self.next_nominal_id += 1;
|
|
return self.next_nominal_id;
|
|
}
|
|
|
|
/// TRUE iff `name` is authored AS A NAMED TYPE (struct / enum / union /
|
|
/// error-set / protocol / foreign class) by ≥2 DISTINCT modules in the import
|
|
/// raw facts — the authoritative same-name-shadow signal (the only case where
|
|
/// distinct `nominal_id`s are needed). Module distinctness is by LEXICALLY
|
|
/// NORMALIZED path: one logical file reached through several spellings
|
|
/// (`testpkg/../allocators.sx` vs `allocators.sx`) is cached — and so parsed —
|
|
/// twice, landing two `module_decls` entries with two decl pointers for the
|
|
/// SAME source; normalizing collapses them to one author, NOT a false shadow.
|
|
/// False when the facts are unwired (comptime / registration host with no
|
|
/// `module_decls`): the single-author path applies, correct there.
|
|
fn nameHasMultipleTypeAuthors(self: *Lowering, name: []const u8) bool {
|
|
const decls = self.program_index.module_decls orelse return false;
|
|
var first_norm: ?[]const u8 = null;
|
|
defer if (first_norm) |f| self.alloc.free(f);
|
|
var it = decls.iterator();
|
|
while (it.next()) |entry| {
|
|
const m = entry.value_ptr;
|
|
const ref = m.names.get(name) orelse continue;
|
|
if (rawNamedTypePtr(ref) == null) continue;
|
|
const norm = std.fs.path.resolvePosix(self.alloc, &.{entry.key_ptr.*}) catch continue;
|
|
if (first_norm) |f| {
|
|
defer self.alloc.free(norm);
|
|
if (!std.mem.eql(u8, f, norm)) return true;
|
|
} else {
|
|
first_norm = norm;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// The opaque decl-pointer identity of a NAMED-type `RawDeclRef`, or null when
|
|
/// the ref is not a named type (fn / value-const / namespace alias). Used to
|
|
/// de-dup same-name authors by decl identity.
|
|
fn rawNamedTypePtr(ref: resolver_mod.RawDeclRef) ?*const anyopaque {
|
|
return switch (ref) {
|
|
.struct_decl => |d| @ptrCast(d),
|
|
.enum_decl => |d| @ptrCast(d),
|
|
.union_decl => |d| @ptrCast(d),
|
|
.error_set_decl => |d| @ptrCast(d),
|
|
.protocol_decl => |d| @ptrCast(d),
|
|
.foreign_class_decl => |d| @ptrCast(d),
|
|
.fn_decl, .const_decl, .namespace_decl => null,
|
|
};
|
|
}
|
|
|
|
fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl, source_file: ?[]const u8) void {
|
|
const table = &self.module.types;
|
|
const name_id = table.internString(sd.name);
|
|
|
|
// Generic structs: store as owned template, don't resolve fields yet
|
|
if (sd.type_params.len > 0) {
|
|
const owned_name = self.alloc.dupe(u8, sd.name) catch return;
|
|
|
|
// Build owned type_params
|
|
const tps = self.alloc.alloc(TemplateParam, sd.type_params.len) catch return;
|
|
for (sd.type_params, 0..) |tp, i| {
|
|
const is_type_param = tp.is_variadic or (if (tp.constraint.data == .type_expr) blk: {
|
|
const cname = tp.constraint.data.type_expr.name;
|
|
// "Type" or a protocol name → type param
|
|
break :blk std.mem.eql(u8, cname, "Type") or
|
|
self.program_index.protocol_decl_map.contains(cname) or
|
|
self.program_index.protocol_ast_map.contains(cname);
|
|
} else false);
|
|
tps[i] = .{
|
|
.name = self.alloc.dupe(u8, tp.name) catch return,
|
|
// $T: Type, $T: Lerpable, $T: Type/Eq — all are type params.
|
|
// `..$Ts: []Type` (variadic) is a type-pack param. Only value
|
|
// params like $N: u32 are non-type.
|
|
.is_type_param = is_type_param,
|
|
.is_variadic = tp.is_variadic,
|
|
// Capture a value param's declared type name (`$K: u32` →
|
|
// "u32") so instantiation can range-check the folded arg.
|
|
.value_type = if (!is_type_param and tp.constraint.data == .type_expr)
|
|
(self.alloc.dupe(u8, tp.constraint.data.type_expr.name) catch null)
|
|
else
|
|
null,
|
|
};
|
|
}
|
|
|
|
// Copy field names
|
|
const fnames = self.alloc.alloc([]const u8, sd.field_names.len) catch return;
|
|
for (sd.field_names, 0..) |fn_str, i| {
|
|
fnames[i] = self.alloc.dupe(u8, fn_str) catch return;
|
|
}
|
|
|
|
// Field type nodes: these are *Node pointers into the AST.
|
|
// Copy the slice of pointers (the nodes themselves are heap-allocated).
|
|
const ftype_nodes = self.alloc.dupe(*const Node, sd.field_types) catch return;
|
|
|
|
self.program_index.struct_template_map.put(owned_name, .{
|
|
.name = owned_name,
|
|
.type_params = tps,
|
|
.field_names = fnames,
|
|
.field_type_nodes = ftype_nodes,
|
|
.source_file = source_file,
|
|
}) catch {};
|
|
|
|
// Register methods under "TemplateName.method" in fn_ast_map
|
|
for (sd.methods) |method_node| {
|
|
if (method_node.data == .fn_decl) {
|
|
const method_fd = &method_node.data.fn_decl;
|
|
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, method_fd.name }) catch continue;
|
|
self.program_index.fn_ast_map.put(qualified, method_fd) catch {};
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Per-decl nominal identity (E2). EACH author of a GENUINE same-name STRUCT
|
|
// shadow already reserved its distinct slot up-front in `scanDecls` (the
|
|
// first at id 0, the rest at nonzero ids), so a self / forward / mutual
|
|
// reference to the shadow name bound to ITS nominal TypeId via
|
|
// `type_decl_tids`, not the global findByName first-author fallback (issue
|
|
// 0105 / F1): reuse that reserved id. A single-author name (or a phantom
|
|
// over-counted by the raw import facts) was NOT reserved — it keeps id 0 and
|
|
// the legacy post-field registration, byte-identical to pre-F1.
|
|
// `shadowNominalId` here only fires for the non-scanDecls registration paths
|
|
// (comptime `lowerDecls`, block-local), where module facts are unwired so it
|
|
// returns 0.
|
|
const decl_key: *const anyopaque = @ptrCast(sd);
|
|
const nominal_id: u32 = if (table.type_decl_tids.get(decl_key)) |id| nominalIdOf(table.get(id)) else self.shadowNominalId(name_id);
|
|
|
|
// Build field list, expanding #using entries
|
|
var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty;
|
|
var field_idx: usize = 0;
|
|
var using_idx: usize = 0;
|
|
const total_explicit = sd.field_names.len;
|
|
while (field_idx < total_explicit or using_idx < sd.using_entries.len) {
|
|
// Insert #using fields at their declared positions
|
|
while (using_idx < sd.using_entries.len and sd.using_entries[using_idx].insert_index == fields.items.len) {
|
|
const ue = sd.using_entries[using_idx];
|
|
const used_name_id = table.internString(ue.type_name);
|
|
if (table.findByName(used_name_id)) |used_ty| {
|
|
const used_info = table.get(used_ty);
|
|
if (used_info == .@"struct") {
|
|
for (used_info.@"struct".fields) |f| {
|
|
fields.append(self.alloc, f) catch unreachable;
|
|
}
|
|
}
|
|
}
|
|
using_idx += 1;
|
|
}
|
|
if (field_idx < total_explicit) {
|
|
const field_ty = self.resolveType(sd.field_types[field_idx]);
|
|
fields.append(self.alloc, .{
|
|
.name = table.internString(sd.field_names[field_idx]),
|
|
.ty = field_ty,
|
|
}) catch unreachable;
|
|
field_idx += 1;
|
|
} else break;
|
|
}
|
|
// Append remaining #using entries after all explicit fields
|
|
while (using_idx < sd.using_entries.len) {
|
|
const ue = sd.using_entries[using_idx];
|
|
const used_name_id = table.internString(ue.type_name);
|
|
if (table.findByName(used_name_id)) |used_ty| {
|
|
const used_info = table.get(used_ty);
|
|
if (used_info == .@"struct") {
|
|
for (used_info.@"struct".fields) |f| {
|
|
fields.append(self.alloc, f) catch unreachable;
|
|
}
|
|
}
|
|
}
|
|
using_idx += 1;
|
|
}
|
|
|
|
// Qualify inline __anon type names: __anon → StructName.field_name
|
|
for (sd.field_names, 0..) |fname, fi| {
|
|
if (fi < fields.items.len) {
|
|
const field_ty = fields.items[fi].ty;
|
|
if (!field_ty.isBuiltin()) {
|
|
self.qualifyAnonType(table, field_ty, sd.name, fname);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Register under the per-decl nominal identity computed above. A non-first
|
|
// shadow author's slot was already reserved before fields resolved, so this
|
|
// fills it (key-stable updatePreservingKey); a first / single author adopts
|
|
// any forward-reference stub. Same-name structs in DIFFERENT sources get
|
|
// distinct TypeIds instead of last-wins clobbering the first (issue 0105).
|
|
const info: types.TypeInfo = .{ .@"struct" = .{ .name = name_id, .fields = fields.items } };
|
|
_ = self.internNamedTypeDecl(decl_key, name_id, info, nominal_id);
|
|
|
|
// Store field defaults for struct literal lowering
|
|
if (sd.field_defaults.len > 0) {
|
|
var has_any_default = false;
|
|
for (sd.field_defaults) |d| {
|
|
if (d != null) { has_any_default = true; break; }
|
|
}
|
|
if (has_any_default) {
|
|
self.struct_defaults_map.put(sd.name, sd.field_defaults) catch {};
|
|
}
|
|
}
|
|
|
|
// Register struct methods as StructName.method in fn_ast_map
|
|
for (sd.methods) |method_node| {
|
|
if (method_node.data == .fn_decl) {
|
|
const method_fd = &method_node.data.fn_decl;
|
|
// Build qualified name: StructName.method
|
|
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, method_fd.name }) catch continue;
|
|
self.program_index.fn_ast_map.put(qualified, method_fd) catch {};
|
|
// Declare extern stub (body is lowered lazily on demand)
|
|
self.declareFunction(method_fd, qualified);
|
|
}
|
|
}
|
|
|
|
// Register struct-level constants (e.g., GRAVITY :f32: 9.81)
|
|
for (sd.constants) |const_node| {
|
|
if (const_node.data == .const_decl) {
|
|
const cd = const_node.data.const_decl;
|
|
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ sd.name, cd.name }) catch continue;
|
|
const ty: ?TypeId = if (cd.type_annotation) |ta| type_bridge.resolveAstType(ta, table, &self.program_index.type_alias_map, &self.program_index.module_const_map) else null;
|
|
self.struct_const_map.put(qualified, .{ .value = cd.value, .ty = ty }) catch {};
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Rename an __anon type to a qualified name: ParentStruct.field_name
|
|
/// Also renames variant payload struct types from __anon.X to ParentStruct.field_name.X
|
|
fn qualifyAnonType(self: *Lowering, table: *types.TypeTable, ty: TypeId, parent_name: []const u8, field_name: []const u8) void {
|
|
const ti = table.get(ty);
|
|
switch (ti) {
|
|
.@"union" => |u| {
|
|
const old_name = table.getString(u.name);
|
|
if (!std.mem.eql(u8, old_name, "__anon")) return;
|
|
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ parent_name, field_name }) catch return;
|
|
const qname_id = table.internString(qualified);
|
|
table.replaceKeyedInfo(ty, .{ .@"union" = .{ .name = qname_id, .fields = u.fields } });
|
|
},
|
|
.tagged_union => |u| {
|
|
const old_name = table.getString(u.name);
|
|
if (!std.mem.eql(u8, old_name, "__anon")) return;
|
|
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ parent_name, field_name }) catch return;
|
|
const qname_id = table.internString(qualified);
|
|
// Rename variant payload structs: __anon.X → ParentStruct.field.X
|
|
for (u.fields) |f| {
|
|
if (!f.ty.isBuiltin()) {
|
|
const finfo = table.get(f.ty);
|
|
if (finfo == .@"struct") {
|
|
const sname = table.getString(finfo.@"struct".name);
|
|
if (std.mem.startsWith(u8, sname, "__anon.")) {
|
|
const suffix = sname["__anon".len..]; // .VariantName
|
|
const sq = std.fmt.allocPrint(self.alloc, "{s}{s}", .{ qualified, suffix }) catch continue;
|
|
const sq_id = table.internString(sq);
|
|
table.replaceKeyedInfo(f.ty, .{ .@"struct" = .{ .name = sq_id, .fields = finfo.@"struct".fields } });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
table.replaceKeyedInfo(ty, .{ .tagged_union = .{ .name = qname_id, .fields = u.fields, .tag_type = u.tag_type, .backing_type = u.backing_type, .explicit_tag_values = u.explicit_tag_values } });
|
|
},
|
|
.@"enum" => |e| {
|
|
const old_name = table.getString(e.name);
|
|
if (!std.mem.eql(u8, old_name, "__anon")) return;
|
|
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ parent_name, field_name }) catch return;
|
|
const qname_id = table.internString(qualified);
|
|
table.replaceKeyedInfo(ty, .{ .@"enum" = .{ .name = qname_id, .variants = e.variants, .explicit_values = e.explicit_values } });
|
|
},
|
|
.@"struct" => |s| {
|
|
const old_name = table.getString(s.name);
|
|
if (!std.mem.eql(u8, old_name, "__anon")) return;
|
|
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ parent_name, field_name }) catch return;
|
|
const qname_id = table.internString(qualified);
|
|
table.replaceKeyedInfo(ty, .{ .@"struct" = .{ .name = qname_id, .fields = s.fields } });
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
/// Register a protocol declaration as a struct type in the IR type table.
|
|
/// Inline protocols: { ctx: *void, method1: *void, method2: *void, ... }
|
|
/// Non-inline protocols: { ctx: *void, __vtable: *void }
|
|
/// Also stores protocol info for dispatch and vtable struct type for vtable protocols.
|
|
/// Register a protocol declaration. Thin delegation to the canonical owner
|
|
/// (`ProtocolResolver`, `protocols.zig`); kept on `Lowering` as a `pub`
|
|
/// entry point because the scan pass + several unit tests reach it here.
|
|
pub fn registerProtocolDecl(self: *Lowering, pd: *const ast.ProtocolDecl) void {
|
|
return self.protocolResolver().registerProtocolDecl(pd);
|
|
}
|
|
|
|
/// Instantiate a parameterized protocol as a runtime VALUE type:
|
|
/// `VL(s64)` → a 16-byte `{ctx, __vtable}` protocol value (`is_protocol`),
|
|
/// with method infos resolved under the type-arg binding (so `get -> T`
|
|
/// becomes `get -> s64`) and the binding recorded for projection. Cached by
|
|
/// the mangled name `VL__s64`. Mirrors the non-parameterized path in
|
|
/// `registerProtocolDecl`.
|
|
fn instantiateParamProtocol(self: *Lowering, pd: *const ast.ProtocolDecl, args: []const *const Node) TypeId {
|
|
const table = &self.module.types;
|
|
const void_ptr_ty = table.ptrTo(.void);
|
|
|
|
var np = std.ArrayList(u8).empty;
|
|
np.appendSlice(self.alloc, pd.name) catch {};
|
|
var tb = std.StringHashMap(TypeId).init(self.alloc);
|
|
for (pd.type_params, 0..) |tp, i| {
|
|
if (i >= args.len) break;
|
|
const ty = self.resolveTypeWithBindings(args[i]);
|
|
tb.put(tp.name, ty) catch {};
|
|
np.appendSlice(self.alloc, "__") catch {};
|
|
np.appendSlice(self.alloc, self.formatTypeName(ty)) catch {};
|
|
}
|
|
const mangled = np.items;
|
|
const name_id = table.internString(mangled);
|
|
if (table.findByName(name_id)) |existing| {
|
|
const info = table.get(existing);
|
|
if (info == .@"struct" and info.@"struct".is_protocol) return existing;
|
|
}
|
|
|
|
// Value struct: {ctx, __vtable} (or ctx + fn-ptrs for an inline protocol).
|
|
var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty;
|
|
fields.append(self.alloc, .{ .name = table.internString("ctx"), .ty = void_ptr_ty }) catch unreachable;
|
|
if (pd.is_inline) {
|
|
for (pd.methods) |m| fields.append(self.alloc, .{ .name = table.internString(m.name), .ty = void_ptr_ty }) catch unreachable;
|
|
} else {
|
|
fields.append(self.alloc, .{ .name = table.internString("__vtable"), .ty = void_ptr_ty }) catch unreachable;
|
|
}
|
|
const struct_info: types.TypeInfo = .{ .@"struct" = .{ .name = name_id, .fields = fields.items, .is_protocol = true } };
|
|
const id = if (table.findByName(name_id)) |existing| existing else table.intern(struct_info);
|
|
table.updatePreservingKey(id, struct_info);
|
|
|
|
// Method infos resolved with the type-arg binding (T → s64), pinned to
|
|
// the protocol's OWN module (E4) so a method-signature type visible only
|
|
// there resolves correctly when instantiated cross-module. `Self` and the
|
|
// bound type-args short-circuit before the leaf; a concrete library type
|
|
// in a signature is the case this pin protects.
|
|
const saved_tb = self.type_bindings;
|
|
self.type_bindings = tb;
|
|
const saved_pp_src = self.current_source_file;
|
|
defer self.setCurrentSourceFile(saved_pp_src);
|
|
if (pd.source_file) |src| self.setCurrentSourceFile(src);
|
|
var method_infos = std.ArrayList(ProtocolMethodInfo).empty;
|
|
for (pd.methods) |method| {
|
|
var ptypes = std.ArrayList(TypeId).empty;
|
|
for (method.params) |p| {
|
|
const pty = blk: {
|
|
if (p.data == .type_expr and std.mem.eql(u8, p.data.type_expr.name, "Self")) break :blk void_ptr_ty;
|
|
break :blk self.resolveTypeWithBindings(p);
|
|
};
|
|
ptypes.append(self.alloc, pty) catch unreachable;
|
|
}
|
|
var ret_is_self = false;
|
|
const ret = if (method.return_type) |rt| blk: {
|
|
if (rt.data == .type_expr and std.mem.eql(u8, rt.data.type_expr.name, "Self")) {
|
|
ret_is_self = true;
|
|
break :blk void_ptr_ty;
|
|
}
|
|
break :blk self.resolveTypeWithBindings(rt);
|
|
} else .void;
|
|
method_infos.append(self.alloc, .{
|
|
.name = method.name,
|
|
.param_types = self.alloc.dupe(TypeId, ptypes.items) catch unreachable,
|
|
.ret_type = ret,
|
|
.ret_is_self = ret_is_self,
|
|
}) catch unreachable;
|
|
}
|
|
self.type_bindings = saved_tb;
|
|
|
|
const owned = self.alloc.dupe(u8, mangled) catch return id;
|
|
self.program_index.protocol_decl_map.put(owned, .{
|
|
.name = owned,
|
|
.is_inline = pd.is_inline,
|
|
.methods = self.alloc.dupe(ProtocolMethodInfo, method_infos.items) catch unreachable,
|
|
}) catch {};
|
|
// Record the type-arg binding so projection (`xs.T`, `.value`) and
|
|
// method-arg resolution on this instance can recover it.
|
|
self.struct_instance_bindings.put(owned, tb) catch {};
|
|
|
|
if (!pd.is_inline) {
|
|
var vtable_fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty;
|
|
for (pd.methods) |m| vtable_fields.append(self.alloc, .{ .name = table.internString(m.name), .ty = void_ptr_ty }) catch unreachable;
|
|
var vtable_name_buf: [192]u8 = undefined;
|
|
const vtable_name = std.fmt.bufPrint(&vtable_name_buf, "__{s}__Vtable", .{mangled}) catch "__Vtable";
|
|
const vtable_ty = table.intern(.{ .@"struct" = .{ .name = table.internString(vtable_name), .fields = vtable_fields.items } });
|
|
self.protocol_vtable_type_map.put(owned, vtable_ty) catch {};
|
|
}
|
|
return id;
|
|
}
|
|
|
|
// ── Pack projection name resolution (Feature 1, Decision 4) ──────────
|
|
//
|
|
// A `..pack.<name>` projection can target two protocol namespaces:
|
|
// - type-arg namespace: the `protocol($T, ...)` params.
|
|
// - runtime-accessor namespace: the protocol's methods (protocols have
|
|
// no fields; a zero-arg method like `value` is the accessor).
|
|
// Resolution is POSITION-driven, not precedence-driven: type position
|
|
// consults type-args, value position consults methods, with NO
|
|
// cross-namespace fallback.
|
|
|
|
pub const ProjectionPosition = enum { type_position, value_position };
|
|
|
|
pub const PackProjection = union(enum) {
|
|
type_arg: u32, // index into the protocol's `type_params`
|
|
method: u32, // index into the protocol's `methods`
|
|
not_found, // `name` absent from the position-selected namespace
|
|
};
|
|
|
|
/// Find `name` in `protocol_name`'s type-arg namespace (`protocol($T,...)`).
|
|
/// Returns the `type_params` index, or null (also for unknown protocols).
|
|
pub fn lookupProtocolArg(self: *Lowering, protocol_name: []const u8, name: []const u8) ?u32 {
|
|
const pd = self.program_index.protocol_ast_map.get(protocol_name) orelse return null;
|
|
for (pd.type_params, 0..) |tp, i| {
|
|
if (std.mem.eql(u8, tp.name, name)) return @intCast(i);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Find `name` in `protocol_name`'s runtime-accessor namespace (its methods
|
|
/// — protocols have no fields). Returns the `methods` index, or null.
|
|
pub fn lookupProtocolField(self: *Lowering, protocol_name: []const u8, name: []const u8) ?u32 {
|
|
const pd = self.program_index.protocol_ast_map.get(protocol_name) orelse return null;
|
|
for (pd.methods, 0..) |m, i| {
|
|
if (std.mem.eql(u8, m.name, name)) return @intCast(i);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Resolve `..pack.<name>` against `protocol_name` by position (Decision 4).
|
|
/// No cross-namespace fallback: a value-position name that exists only as a
|
|
/// type-arg (or vice versa) is `.not_found`, letting the caller emit a
|
|
/// position-specific diagnostic (G3, Step 2.7).
|
|
pub fn resolvePackProjection(
|
|
self: *Lowering,
|
|
protocol_name: []const u8,
|
|
name: []const u8,
|
|
pos: ProjectionPosition,
|
|
) PackProjection {
|
|
return switch (pos) {
|
|
.type_position => if (self.lookupProtocolArg(protocol_name, name)) |i|
|
|
.{ .type_arg = i }
|
|
else
|
|
.not_found,
|
|
.value_position => if (self.lookupProtocolField(protocol_name, name)) |i|
|
|
.{ .method = i }
|
|
else
|
|
.not_found,
|
|
};
|
|
}
|
|
|
|
/// Register a foreign-class declaration. The alias goes into
|
|
/// `foreign_class_map` for method-dispatch lookup. The underlying
|
|
/// type (e.g. `*Activity`) is resolved via the existing struct
|
|
/// fallback in `type_bridge.resolveTypeName` (which interns unknown
|
|
/// named types as 0-field structs).
|
|
///
|
|
/// sx-defined Obj-C classes (no `#foreign`, runtime == .objc_class)
|
|
/// also land in `module.objc_defined_class_cache` in declaration
|
|
/// order AND have their bodied methods registered into `fn_ast_map`
|
|
/// under qualified names `<ClassName>.<methodName>`. Lazy lowering
|
|
/// then handles the body via the standard path; `*Self` is
|
|
/// substituted to `*<ClassName>State` during body lowering (M1.2 A.2b).
|
|
fn registerForeignClassDecl(self: *Lowering, fcd: *const ast.ForeignClassDecl) void {
|
|
self.program_index.foreign_class_map.put(fcd.name, fcd) catch {};
|
|
if (!fcd.is_foreign and fcd.runtime == .objc_class) {
|
|
if (self.module.lookupObjcDefinedClass(fcd.name) == null) {
|
|
self.module.appendObjcDefinedClass(fcd.name, fcd);
|
|
// M2.3 — resolve the `#extends` alias to the actual
|
|
// Obj-C runtime class name. `#extends NSObjectBase`
|
|
// where NSObjectBase is aliased to "NSObject" must
|
|
// pass "NSObject" to objc_allocateClassPair, otherwise
|
|
// the runtime's class-hierarchy link is broken and
|
|
// inherited-method dispatch fails.
|
|
self.module.setObjcDefinedClassParent(fcd.name, self.resolveObjcParentName(fcd));
|
|
// M1.2 A.4b.i: per-class ivar handle global. The class-pair
|
|
// init constructor (emit_llvm) populates it via
|
|
// class_getInstanceVariable after the class is registered;
|
|
// IMP trampolines read it to find the __sx_state ivar.
|
|
self.declareObjcDefinedStateIvarGlobal(fcd.name);
|
|
// M1.2 A.6: per-class class-object global. -dealloc reads
|
|
// it to build an `objc_super` struct for `[super dealloc]`
|
|
// dispatch via `objc_msgSendSuper2`.
|
|
self.declareObjcDefinedClassGlobal(fcd.name);
|
|
}
|
|
self.registerObjcDefinedClassMethods(fcd);
|
|
}
|
|
}
|
|
|
|
/// Resolve the `#extends ParentAlias` declaration on a sx-defined
|
|
/// `#objc_class` to the actual Obj-C runtime class name. Falls
|
|
/// back to "NSObject" when no `#extends` is declared.
|
|
/// Aliases that resolve to foreign Obj-C classes use the
|
|
/// foreign_path; aliases for OTHER sx-defined classes use the
|
|
/// alias name directly (which equals the Obj-C class name for
|
|
/// sx-defined classes).
|
|
fn resolveObjcParentName(self: *Lowering, fcd: *const ast.ForeignClassDecl) []const u8 {
|
|
for (fcd.members) |m| switch (m) {
|
|
.extends => |alias| {
|
|
if (self.program_index.foreign_class_map.get(alias)) |parent_fcd| {
|
|
if (parent_fcd.is_foreign) return parent_fcd.foreign_path;
|
|
// Sx-defined parent — its alias IS its Obj-C name.
|
|
return parent_fcd.name;
|
|
}
|
|
// Unknown alias — pass through as-is and let the
|
|
// runtime diagnose if it's genuinely wrong.
|
|
return alias;
|
|
},
|
|
else => {},
|
|
};
|
|
return "NSObject";
|
|
}
|
|
|
|
/// Declare a per-class global `__<ClassName>_state_ivar : *void = null`.
|
|
/// emit_llvm's `emitObjcDefinedClassInit` constructor fills it in via
|
|
/// `class_getInstanceVariable(cls, "__sx_state")` once per module load.
|
|
fn declareObjcDefinedStateIvarGlobal(self: *Lowering, class_name: []const u8) void {
|
|
const gname = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{class_name}) catch return;
|
|
const name_id = self.module.types.internString(gname);
|
|
_ = self.module.addGlobal(.{
|
|
.name = name_id,
|
|
.ty = self.module.types.ptrTo(.void),
|
|
.init_val = .null_val,
|
|
.is_extern = false,
|
|
.is_const = false,
|
|
});
|
|
}
|
|
|
|
/// Declare a per-class global `__<ClassName>_class : *void = null`.
|
|
/// emit_llvm's `emitObjcDefinedClassInit` constructor stores the
|
|
/// freshly-allocated Class pointer into it after objc_registerClassPair.
|
|
/// The synthesized `-dealloc` IMP reads it to construct an `objc_super`
|
|
/// for `[super dealloc]` dispatch.
|
|
fn declareObjcDefinedClassGlobal(self: *Lowering, class_name: []const u8) void {
|
|
const gname = std.fmt.allocPrint(self.alloc, "__{s}_class", .{class_name}) catch return;
|
|
const name_id = self.module.types.internString(gname);
|
|
_ = self.module.addGlobal(.{
|
|
.name = name_id,
|
|
.ty = self.module.types.ptrTo(.void),
|
|
.init_val = .null_val,
|
|
.is_extern = false,
|
|
.is_const = false,
|
|
});
|
|
}
|
|
|
|
/// For each bodied instance method on an sx-defined `#objc_class`,
|
|
/// synthesize an `FnDecl` from the `ForeignMethodDecl`, register it
|
|
/// in `fn_ast_map` under `<ClassName>.<methodName>`, declare the IR
|
|
/// function, AND collect per-method registration data (selector
|
|
/// mangling + type encoding + IMP symbol name) into the class's
|
|
/// cache entry so emit_llvm can wire up `class_addMethod` calls
|
|
/// (M1.2 A.4b.iii). Bodyless declarations are skipped — they
|
|
/// reference inherited / external methods, not sx-side bodies.
|
|
fn registerObjcDefinedClassMethods(self: *Lowering, fcd: *const ast.ForeignClassDecl) void {
|
|
// Set current_foreign_class so `*Self` substitutions in
|
|
// declareFunction's type resolution find the state struct.
|
|
const saved = self.current_foreign_class;
|
|
self.current_foreign_class = fcd;
|
|
defer self.current_foreign_class = saved;
|
|
|
|
var method_infos = std.ArrayList(Module.ObjcDefinedMethodEntry).empty;
|
|
|
|
for (fcd.members) |m| {
|
|
const method = switch (m) {
|
|
.method => |md| md,
|
|
else => continue,
|
|
};
|
|
const body = method.body orelse continue;
|
|
const fd = self.synthesizeFnDeclFromObjcMethod(method, body) orelse continue;
|
|
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fcd.name, method.name }) catch continue;
|
|
self.program_index.fn_ast_map.put(qualified, fd) catch {};
|
|
self.declareFunction(fd, qualified);
|
|
|
|
// Selector mangling — A.1's deriveObjcSelector handles
|
|
// `#selector("...")` override + the default rule. Static
|
|
// methods use the same mangling rule (their first param
|
|
// ISN'T *Self, so no offset).
|
|
//
|
|
// ABI for the IMP signature (both instance + class methods):
|
|
// `(recv: id|Class, _cmd: SEL, ...user_args) -> ret`
|
|
// For instance methods the user-declared self is at param[0]
|
|
// (skipped); class methods have no self in the AST.
|
|
const user_param_start: usize = if (method.is_static) 0 else 1;
|
|
const user_arg_count = if (method.params.len > user_param_start) method.params.len - user_param_start else 0;
|
|
const sel_info = self.objc().deriveObjcSelector(method, user_arg_count);
|
|
|
|
const ret_ty: TypeId = if (method.return_type) |rt| self.resolveType(rt) else .void;
|
|
var arg_tys = std.ArrayList(TypeId).empty;
|
|
defer arg_tys.deinit(self.alloc);
|
|
if (method.params.len > user_param_start) {
|
|
for (method.params[user_param_start..]) |p_node| {
|
|
arg_tys.append(self.alloc, self.resolveType(p_node)) catch unreachable;
|
|
}
|
|
}
|
|
const encoding = self.objc().objcTypeEncodingFromSignature(ret_ty, arg_tys.items, null) catch continue;
|
|
|
|
const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, method.name }) catch continue;
|
|
|
|
method_infos.append(self.alloc, .{
|
|
.sel = sel_info.sel,
|
|
.encoding = encoding,
|
|
.imp_name = imp_name,
|
|
.is_class = method.is_static,
|
|
}) catch unreachable;
|
|
}
|
|
|
|
if (method_infos.items.len > 0) {
|
|
const methods_slice = method_infos.toOwnedSlice(self.alloc) catch return;
|
|
self.module.setObjcDefinedClassMethods(fcd.name, methods_slice);
|
|
}
|
|
}
|
|
|
|
/// Build an `FnDecl` whose params are zipped from the
|
|
/// `ForeignMethodDecl.params` (type nodes) and `param_names`. Used
|
|
/// to feed sx-defined class methods through the standard
|
|
/// fn-lowering pipeline. Allocator-owned; lives for the duration
|
|
/// of the Lowering pass.
|
|
fn synthesizeFnDeclFromObjcMethod(self: *Lowering, method: ast.ForeignMethodDecl, body: *ast.Node) ?*ast.FnDecl {
|
|
if (method.params.len != method.param_names.len) return null;
|
|
var params = std.ArrayList(ast.Param).empty;
|
|
for (method.params, method.param_names) |type_node, p_name| {
|
|
params.append(self.alloc, .{
|
|
.name = p_name,
|
|
.name_span = .{ .start = 0, .end = 0 },
|
|
.type_expr = type_node,
|
|
}) catch unreachable;
|
|
}
|
|
const fd = self.alloc.create(ast.FnDecl) catch return null;
|
|
fd.* = .{
|
|
.name = method.name,
|
|
.params = params.toOwnedSlice(self.alloc) catch unreachable,
|
|
.return_type = method.return_type,
|
|
.body = body,
|
|
};
|
|
return fd;
|
|
}
|
|
|
|
/// If `name` matches an sx-defined `#objc_class`'s qualified-method
|
|
/// pattern (`<ClassName>.<methodName>`), return the class's
|
|
/// ForeignClassDecl. Used by `lowerFunction` to set
|
|
/// `current_foreign_class` so `*Self` resolves to the state struct
|
|
/// during body lowering.
|
|
fn lookupObjcDefinedClassForMethod(self: *Lowering, name: []const u8) ?*const ast.ForeignClassDecl {
|
|
const dot = std.mem.indexOf(u8, name, ".") orelse return null;
|
|
return self.module.lookupObjcDefinedClass(name[0..dot]);
|
|
}
|
|
|
|
/// Lazily declare the `sx_jni_env_tl_get` / `sx_jni_env_tl_set`
|
|
/// runtime externs (step 2.16c). The storage lives in
|
|
/// `library/vendors/sx_jni_runtime/sx_jni_env_tl.c` as a
|
|
/// `_Thread_local` slot — keeping it OUT of the user's IR module
|
|
/// is what lets the LLVM ORC JIT load the module cleanly without
|
|
/// orc_rt platform support. AOT targets get the same .c file
|
|
/// linked in via `needs_jni_env_tl_runtime`, which Compilation
|
|
/// reads to append a synthetic c_import alongside the user's.
|
|
fn getJniEnvTlFids(self: *Lowering) struct { get: FuncId, set: FuncId } {
|
|
self.needs_jni_env_tl_runtime = true;
|
|
const ptr_ty = self.module.types.ptrTo(.void);
|
|
if (self.jni_env_tl_get_fid == null) {
|
|
const name = self.module.types.internString("sx_jni_env_tl_get");
|
|
const fid = self.builder.declareExtern(name, &.{}, ptr_ty);
|
|
const func = self.module.getFunctionMut(fid);
|
|
func.call_conv = .c;
|
|
self.jni_env_tl_get_fid = fid;
|
|
}
|
|
if (self.jni_env_tl_set_fid == null) {
|
|
const name = self.module.types.internString("sx_jni_env_tl_set");
|
|
const env_param = self.module.types.internString("env");
|
|
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
|
params.append(self.alloc, .{ .name = env_param, .ty = ptr_ty }) catch unreachable;
|
|
const fid = self.builder.declareExtern(name, params.toOwnedSlice(self.alloc) catch unreachable, .void);
|
|
const func = self.module.getFunctionMut(fid);
|
|
func.call_conv = .c;
|
|
self.jni_env_tl_set_fid = fid;
|
|
}
|
|
return .{ .get = self.jni_env_tl_get_fid.?, .set = self.jni_env_tl_set_fid.? };
|
|
}
|
|
|
|
/// Lazily declare the `sx_trace_push(u64)` / `sx_trace_clear()` runtime
|
|
/// externs (ERR E3.1). Storage is a `_Thread_local` ring buffer in
|
|
/// `library/vendors/sx_trace_runtime/sx_trace.c` — kept OUT of the user's IR
|
|
/// module (same JIT-TLS reason as the JNI env slot). Setting
|
|
/// `needs_trace_runtime` signals Compilation to auto-link the .c for AOT.
|
|
/// Wired into the `raise` / `try` push sites and the absorbing clear sites
|
|
/// at ERR E3.2.
|
|
fn getTraceFids(self: *Lowering) struct { push: FuncId, clear: FuncId } {
|
|
self.needs_trace_runtime = true;
|
|
if (self.trace_push_fid == null) {
|
|
const name = self.module.types.internString("sx_trace_push");
|
|
const frame_param = self.module.types.internString("frame");
|
|
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
|
params.append(self.alloc, .{ .name = frame_param, .ty = .u64 }) catch unreachable;
|
|
const fid = self.builder.declareExtern(name, params.toOwnedSlice(self.alloc) catch unreachable, .void);
|
|
self.module.getFunctionMut(fid).call_conv = .c;
|
|
self.trace_push_fid = fid;
|
|
}
|
|
if (self.trace_clear_fid == null) {
|
|
const name = self.module.types.internString("sx_trace_clear");
|
|
const fid = self.builder.declareExtern(name, &.{}, .void);
|
|
self.module.getFunctionMut(fid).call_conv = .c;
|
|
self.trace_clear_fid = fid;
|
|
}
|
|
return .{ .push = self.trace_push_fid.?, .clear = self.trace_clear_fid.? };
|
|
}
|
|
|
|
/// Error return-traces are emitted in debug-ish builds and skipped in
|
|
/// release (ERR E3.2 build-mode gating). `sx run` defaults to `-O0`
|
|
/// (`.none`), the common dev path; `.default`/`.aggressive` are release.
|
|
/// The spec's `--release-traces` opt-in + a `BuildOptions.error_traces`
|
|
/// accessor are a later refinement; for now the opt level is the gate.
|
|
fn tracesEnabled(self: *Lowering) bool {
|
|
const tc = self.target_config orelse return true; // no target → treat as debug
|
|
return tc.opt_level == .none or tc.opt_level == .less;
|
|
}
|
|
|
|
/// Emit a trace-buffer push of `frame` (an opaque u64) at a failure site.
|
|
/// No-op when traces are disabled (release). `frame` is a placeholder until
|
|
/// DWARF (E3.0) supplies real return-address PCs and E3.3 resolves them.
|
|
fn emitTracePush(self: *Lowering, frame: Ref) void {
|
|
if (!self.tracesEnabled()) return;
|
|
const fids = self.getTraceFids();
|
|
const coerced = self.coerceToType(frame, self.builder.getRefType(frame), .u64);
|
|
const args = self.alloc.dupe(Ref, &.{coerced}) catch return;
|
|
_ = self.builder.emit(.{ .call = .{ .callee = fids.push, .args = args } }, .void);
|
|
}
|
|
|
|
/// Emit a trace-buffer clear at an absorbing site (`catch` / `or value` /
|
|
/// destructure). No-op when traces are disabled.
|
|
fn emitTraceClear(self: *Lowering) void {
|
|
if (!self.tracesEnabled()) return;
|
|
const fids = self.getTraceFids();
|
|
_ = self.builder.emit(.{ .call = .{ .callee = fids.clear, .args = &.{} } }, .void);
|
|
}
|
|
|
|
/// The trace frame value for a failure site (ERR E3.0 slice 3a). Emits the
|
|
/// niladic `.trace_frame` op (span-stamped via `Builder.current_span`); each
|
|
/// backend resolves it to a real frame — `emit_llvm` to a `Frame*`, `interp`
|
|
/// to a packed `(func_id, offset)`. The result feeds `sx_trace_push`.
|
|
fn placeholderTraceFrame(self: *Lowering) Ref {
|
|
return self.builder.emit(.{ .trace_frame = {} }, .u64);
|
|
}
|
|
|
|
/// When a namespaced import (`Ns :: #import "..."`) contains foreign-class
|
|
/// declarations, ALSO register them under their qualified name `Ns.Class`
|
|
/// so receiver types like `*Ns.Class` can find the fcd. The recursive
|
|
/// scan/lower already handles bare-name registration; this only adds the
|
|
/// qualified-name entry, so cross-class refs in method signatures
|
|
/// (`*View` → bare lookup) still work.
|
|
fn registerNamespacedForeignClasses(self: *Lowering, ns: ast.NamespaceDecl) void {
|
|
for (ns.decls) |inner| {
|
|
if (inner.data == .foreign_class_decl) {
|
|
const fcd = &inner.data.foreign_class_decl;
|
|
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ ns.name, fcd.name }) catch fcd.name;
|
|
self.program_index.foreign_class_map.put(qualified, fcd) catch {};
|
|
} else if (inner.data == .namespace_decl) {
|
|
// Nested namespaces — qualify with both prefixes.
|
|
self.registerNamespacedForeignClasses(inner.data.namespace_decl);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ── Protocol dispatch ──────────────────────────────────────────
|
|
|
|
/// Check if a type name is a registered protocol.
|
|
fn isProtocolType(self: *Lowering, type_name: []const u8) bool {
|
|
return self.program_index.protocol_decl_map.contains(type_name);
|
|
}
|
|
|
|
/// Get protocol info for a TypeId (if it's a protocol type).
|
|
/// Protocol lookup. Thin delegation to the canonical owner
|
|
/// (`ProtocolResolver`, `protocols.zig`); kept on `Lowering` because ~9
|
|
/// callers (dispatch sites here + `calls.zig`) reach it.
|
|
pub fn getProtocolInfo(self: *Lowering, ty: TypeId) ?ProtocolDeclInfo {
|
|
return self.protocolResolver().getProtocolInfo(ty);
|
|
}
|
|
|
|
/// Get or create thunks for a (protocol, concrete_type) pair.
|
|
/// Returns a slice of FuncIds, one per protocol method.
|
|
fn getOrCreateThunks(self: *Lowering, proto_name: []const u8, concrete_type_name: []const u8) []const FuncId {
|
|
// Key: "Proto\x00Type"
|
|
const key = std.fmt.allocPrint(self.alloc, "{s}\x00{s}", .{ proto_name, concrete_type_name }) catch return &.{};
|
|
if (self.protocol_thunk_map.get(key)) |thunks| return thunks;
|
|
|
|
// PLANNING: which methods need a thunk (owned by the registry).
|
|
const methods = self.protocolResolver().protocolMethodInfos(proto_name) orelse return &.{};
|
|
var thunk_ids = std.ArrayList(FuncId).empty;
|
|
defer thunk_ids.deinit(self.alloc);
|
|
|
|
// EMISSION: materialize one thunk per method (stays in Lowering).
|
|
for (methods) |method| {
|
|
const thunk_id = self.createProtocolThunk(proto_name, concrete_type_name, method);
|
|
thunk_ids.append(self.alloc, thunk_id) catch unreachable;
|
|
}
|
|
|
|
const owned = self.alloc.dupe(FuncId, thunk_ids.items) catch unreachable;
|
|
self.protocol_thunk_map.put(key, owned) catch {};
|
|
return owned;
|
|
}
|
|
|
|
/// Emit the process-wide default Context as an LLVM static constant.
|
|
///
|
|
/// @__sx_default_context = internal constant %Context {
|
|
/// %Allocator { ptr null,
|
|
/// ptr @__thunk_CAllocator_Allocator_alloc,
|
|
/// ptr @__thunk_CAllocator_Allocator_dealloc },
|
|
/// ptr null
|
|
/// }
|
|
///
|
|
/// Used by FFI inbound wrappers (Step 4) and the interp's default-
|
|
/// context call entry (Step 7). Only emitted when the program imports
|
|
/// `std.sx` — without that, Context / Allocator / CAllocator aren't
|
|
/// registered and the global has no purpose.
|
|
fn emitDefaultContextGlobal(self: *Lowering) void {
|
|
const saved_edc = self.emitting_default_context;
|
|
self.emitting_default_context = true;
|
|
defer self.emitting_default_context = saved_edc;
|
|
const tbl = &self.module.types;
|
|
const ctx_name_id = tbl.internString("Context");
|
|
const ctx_ty = tbl.findByName(ctx_name_id) orelse return;
|
|
if (tbl.findByName(tbl.internString("Allocator")) == null) return;
|
|
if (tbl.findByName(tbl.internString("CAllocator")) == null) return;
|
|
|
|
// Force the CAllocator → Allocator thunks to exist so we can
|
|
// reference them by FuncId in the static initializer.
|
|
const thunks = self.getOrCreateThunks("Allocator", "CAllocator");
|
|
if (thunks.len < 2) return;
|
|
|
|
// Inline Allocator value: { ctx: *void, alloc_fn: *void, dealloc_fn: *void }
|
|
// CAllocator is stateless, so ctx is null.
|
|
const alloc_fields = self.alloc.alloc(inst_mod.ConstantValue, 3) catch return;
|
|
alloc_fields[0] = .null_val;
|
|
alloc_fields[1] = .{ .func_ref = thunks[0] };
|
|
alloc_fields[2] = .{ .func_ref = thunks[1] };
|
|
|
|
// Context value: { allocator: Allocator, data: *void }
|
|
const ctx_fields = self.alloc.alloc(inst_mod.ConstantValue, 2) catch return;
|
|
ctx_fields[0] = .{ .aggregate = alloc_fields };
|
|
ctx_fields[1] = .null_val;
|
|
|
|
const global_name = "__sx_default_context";
|
|
const global_name_id = tbl.internString(global_name);
|
|
const gid = self.module.addGlobal(.{
|
|
.name = global_name_id,
|
|
.ty = ctx_ty,
|
|
.init_val = .{ .aggregate = ctx_fields },
|
|
.is_const = true,
|
|
});
|
|
self.putGlobal(self.current_source_file, global_name, .{ .id = gid, .ty = ctx_ty });
|
|
}
|
|
|
|
/// Create a thunk function: __thunk_ConcreteType_Protocol_method(ctx: *void, args...) -> ret
|
|
/// The thunk calls ConcreteType.method(ctx, args...).
|
|
fn createProtocolThunk(self: *Lowering, proto_name: []const u8, concrete_type_name: []const u8, method: ProtocolMethodInfo) FuncId {
|
|
// Build params: [__sx_ctx]? + ctx: *void + method params.
|
|
// Thunks are sx-side functions, so they get the implicit __sx_ctx
|
|
// at slot 0 when it's enabled program-wide. The concrete protocol
|
|
// receiver (ctx) follows at slot 1; user method args at slot 2+.
|
|
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
|
defer params.deinit(self.alloc);
|
|
const void_ptr = self.module.types.ptrTo(.void);
|
|
const thunk_has_ctx = self.implicit_ctx_enabled;
|
|
if (thunk_has_ctx) {
|
|
params.append(self.alloc, .{ .name = self.module.types.internString("__sx_ctx"), .ty = void_ptr }) catch unreachable;
|
|
}
|
|
params.append(self.alloc, .{ .name = self.module.types.internString("ctx"), .ty = void_ptr }) catch unreachable;
|
|
for (method.param_types, 0..) |pty, i| {
|
|
var buf: [32]u8 = undefined;
|
|
const pname = std.fmt.bufPrint(&buf, "a{d}", .{i}) catch "arg";
|
|
params.append(self.alloc, .{ .name = self.module.types.internString(pname), .ty = pty }) catch unreachable;
|
|
}
|
|
|
|
// Generate unique name
|
|
var name_buf: [192]u8 = undefined;
|
|
const thunk_name = std.fmt.bufPrint(&name_buf, "__thunk_{s}_{s}_{s}", .{ concrete_type_name, proto_name, method.name }) catch "__thunk";
|
|
const thunk_name_id = self.module.types.internString(thunk_name);
|
|
|
|
// Save builder state
|
|
const saved_func = self.builder.func;
|
|
const saved_block = self.builder.current_block;
|
|
const saved_counter = self.builder.inst_counter;
|
|
const saved_ctx_ref_thunk = self.current_ctx_ref;
|
|
defer self.current_ctx_ref = saved_ctx_ref_thunk;
|
|
|
|
const owned_params = self.alloc.dupe(inst_mod.Function.Param, params.items) catch unreachable;
|
|
var func = inst_mod.Function.init(thunk_name_id, owned_params, method.ret_type);
|
|
func.has_implicit_ctx = thunk_has_ctx;
|
|
const func_id = self.module.addFunction(func);
|
|
self.builder.func = func_id;
|
|
self.builder.inst_counter = @intCast(owned_params.len);
|
|
if (thunk_has_ctx) self.current_ctx_ref = Ref.fromIndex(0);
|
|
const entry_block = self.builder.appendBlock(self.module.types.internString("entry"), &.{});
|
|
self.builder.switchToBlock(entry_block);
|
|
|
|
// Ensure the concrete method is lowered
|
|
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ concrete_type_name, method.name }) catch method.name;
|
|
if (!self.lowered_functions.contains(qualified)) {
|
|
if (self.program_index.fn_ast_map.contains(qualified)) {
|
|
self.lazyLowerFunction(qualified);
|
|
} else if (self.struct_instance_template.get(concrete_type_name)) |tmpl_name| {
|
|
// Generic-struct instance (`Combined__s64_s64`): the impl method
|
|
// is registered under the template name (`Combined.get`).
|
|
// Monomorphize it for this instance's bindings so the thunk has a
|
|
// concrete `Combined__s64_s64.get` to call.
|
|
const tmpl_qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tmpl_name, method.name }) catch method.name;
|
|
if (self.program_index.fn_ast_map.get(tmpl_qualified)) |fd| {
|
|
if (self.struct_instance_bindings.getPtr(concrete_type_name)) |bindings| {
|
|
self.monomorphizeFunction(fd, qualified, bindings);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Call the concrete method: ConcreteType.method(__sx_ctx?, ctx, args...).
|
|
// The concrete method is itself an sx function that takes the
|
|
// implicit __sx_ctx at slot 0 (when implicit_ctx is enabled); we
|
|
// forward the thunk's own __sx_ctx.
|
|
if (self.resolveFuncByName(qualified)) |concrete_fid| {
|
|
const concrete_func = &self.module.functions.items[@intFromEnum(concrete_fid)];
|
|
var call_args = std.ArrayList(Ref).empty;
|
|
defer call_args.deinit(self.alloc);
|
|
|
|
// Slot offsets inside the thunk: __sx_ctx at 0 (if present),
|
|
// protocol receiver (ctx) at slot user_base, user args at +1, +2...
|
|
const user_base: u32 = if (thunk_has_ctx) 1 else 0;
|
|
|
|
// Forward our __sx_ctx to the concrete method's __sx_ctx slot.
|
|
if (concrete_func.has_implicit_ctx) {
|
|
call_args.append(self.alloc, self.current_ctx_ref) catch unreachable;
|
|
}
|
|
|
|
// Pass ctx as the next arg (it's the concrete *Type disguised as *void).
|
|
// If the concrete method expects a value (e.g., f32) not a pointer, load from ctx.
|
|
const ctx_ref = Ref.fromIndex(user_base);
|
|
const concrete_receiver_idx: usize = if (concrete_func.has_implicit_ctx) 1 else 0;
|
|
if (concrete_receiver_idx < concrete_func.params.len) {
|
|
const first_concrete_ty = concrete_func.params[concrete_receiver_idx].ty;
|
|
const first_info = self.module.types.get(first_concrete_ty);
|
|
if (first_info != .pointer) {
|
|
// Concrete expects value — load from ctx pointer
|
|
call_args.append(self.alloc, self.builder.load(ctx_ref, first_concrete_ty)) catch unreachable;
|
|
} else {
|
|
call_args.append(self.alloc, ctx_ref) catch unreachable;
|
|
}
|
|
} else {
|
|
call_args.append(self.alloc, ctx_ref) catch unreachable;
|
|
}
|
|
for (method.param_types, 0..) |proto_pty, i| {
|
|
var arg_ref = Ref.fromIndex(@intCast(user_base + 1 + i));
|
|
// If protocol param is a pointer (Self→*void) but concrete method
|
|
// expects a value type, load the value from the pointer.
|
|
const concrete_idx = concrete_receiver_idx + 1 + i;
|
|
if (concrete_idx < concrete_func.params.len) {
|
|
const concrete_pty = concrete_func.params[concrete_idx].ty;
|
|
const proto_info = self.module.types.get(proto_pty);
|
|
const concrete_info = self.module.types.get(concrete_pty);
|
|
if (proto_info == .pointer and concrete_info != .pointer) {
|
|
arg_ref = self.builder.load(arg_ref, concrete_pty);
|
|
}
|
|
}
|
|
call_args.append(self.alloc, arg_ref) catch unreachable;
|
|
}
|
|
const owned_args = self.alloc.dupe(Ref, call_args.items) catch unreachable;
|
|
const concrete_ret = concrete_func.ret;
|
|
const result = self.builder.call(concrete_fid, owned_args, concrete_ret);
|
|
if (method.ret_type != .void) {
|
|
// If protocol returns *void (Self) but concrete returns a value type,
|
|
// box the value: alloca+store and return the pointer
|
|
const ret_info = self.module.types.get(method.ret_type);
|
|
const concrete_ret_info = self.module.types.get(concrete_ret);
|
|
if (ret_info == .pointer and concrete_ret_info != .pointer) {
|
|
const slot = self.builder.alloca(concrete_ret);
|
|
self.builder.store(slot, result);
|
|
self.builder.ret(slot, method.ret_type);
|
|
} else {
|
|
self.builder.ret(result, method.ret_type);
|
|
}
|
|
} else {
|
|
self.builder.retVoid();
|
|
}
|
|
} else {
|
|
// Can't resolve concrete method — emit unreachable
|
|
_ = self.builder.emit(.{ .@"unreachable" = {} }, .void);
|
|
}
|
|
self.builder.finalize();
|
|
|
|
// Restore builder state
|
|
self.builder.func = saved_func;
|
|
self.builder.current_block = saved_block;
|
|
self.builder.inst_counter = saved_counter;
|
|
|
|
return func_id;
|
|
}
|
|
|
|
/// Build a protocol value from a concrete pointer.
|
|
/// For inline protocols: struct_init { ctx, thunk1, thunk2, ... }
|
|
/// For vtable protocols: struct_init { ctx, vtable_ptr } where vtable is stack-allocated
|
|
/// When `heap_copy` is true, the concrete data is heap-copied so the protocol value
|
|
/// outlives the current stack frame (used when source is a value, not an explicit pointer).
|
|
/// When false, the pointer is used directly (user manages the pointee's lifetime).
|
|
fn buildProtocolValue(self: *Lowering, concrete_ptr: Ref, proto_name: []const u8, concrete_type_name: []const u8, proto_ty: TypeId, concrete_ty: TypeId, heap_copy: bool) Ref {
|
|
const pd = self.program_index.protocol_decl_map.get(proto_name) orelse return concrete_ptr;
|
|
const thunks = self.getOrCreateThunks(proto_name, concrete_type_name);
|
|
if (thunks.len != pd.methods.len) return concrete_ptr;
|
|
|
|
const void_ptr_ty = self.module.types.ptrTo(.void);
|
|
|
|
// When source is a value (not an explicit pointer), heap-allocate
|
|
// so the protocol value outlives the current stack frame.
|
|
// When source is an explicit pointer (xx @obj), use it directly —
|
|
// the user is responsible for the pointee's lifetime.
|
|
var ctx_ptr = concrete_ptr;
|
|
if (heap_copy) {
|
|
const concrete_size = self.module.types.typeSizeBytes(concrete_ty);
|
|
const size_ref = self.builder.constInt(@intCast(concrete_size), .s64);
|
|
const heap_ptr = self.allocViaContext(size_ref, void_ptr_ty);
|
|
_ = self.callForeign("memcpy", &.{ heap_ptr, concrete_ptr, size_ref }, void_ptr_ty);
|
|
ctx_ptr = heap_ptr;
|
|
}
|
|
|
|
if (pd.is_inline) {
|
|
// Inline: { ctx, fn1, fn2, ... }
|
|
var field_vals = std.ArrayList(Ref).empty;
|
|
defer field_vals.deinit(self.alloc);
|
|
field_vals.append(self.alloc, ctx_ptr) catch unreachable;
|
|
for (thunks) |thunk_id| {
|
|
const fn_ref = self.builder.emit(.{ .func_ref = thunk_id }, void_ptr_ty);
|
|
field_vals.append(self.alloc, fn_ref) catch unreachable;
|
|
}
|
|
const owned = self.alloc.dupe(Ref, field_vals.items) catch unreachable;
|
|
return self.builder.emit(.{ .struct_init = .{ .fields = owned } }, proto_ty);
|
|
} else {
|
|
// Vtable: { ctx, vtable_ptr }
|
|
// Vtable is a global constant (same function pointers for every instance
|
|
// of the same Protocol+ConcreteType pair). Cached per pair.
|
|
const vtable_ty = self.protocol_vtable_type_map.get(proto_name) orelse return concrete_ptr;
|
|
|
|
// Build cache key: "Proto\x00Type"
|
|
const key = std.fmt.allocPrint(self.alloc, "{s}\x00{s}", .{ proto_name, concrete_type_name }) catch unreachable;
|
|
|
|
const vtable_global_id = self.protocol_vtable_global_map.get(key) orelse blk: {
|
|
// Create vtable global with function pointer initializer
|
|
const global_name = std.fmt.allocPrint(self.alloc, "__{s}__{s}__vtable", .{ proto_name, concrete_type_name }) catch unreachable;
|
|
const global_name_id = self.module.types.strings.intern(self.alloc, global_name);
|
|
const thunk_ids = self.alloc.dupe(FuncId, thunks) catch unreachable;
|
|
const gid = self.module.addGlobal(.{
|
|
.name = global_name_id,
|
|
.ty = vtable_ty,
|
|
.init_val = .{ .vtable = thunk_ids },
|
|
.is_const = true,
|
|
});
|
|
self.protocol_vtable_global_map.put(key, gid) catch {};
|
|
break :blk gid;
|
|
};
|
|
|
|
// Reference the vtable global's address
|
|
const vtable_ptr_ty = self.module.types.ptrTo(vtable_ty);
|
|
const vtable_addr = self.builder.emit(.{ .global_addr = vtable_global_id }, vtable_ptr_ty);
|
|
|
|
// Build protocol struct: { ctx, &vtable }
|
|
var proto_fields = std.ArrayList(Ref).empty;
|
|
defer proto_fields.deinit(self.alloc);
|
|
proto_fields.append(self.alloc, ctx_ptr) catch unreachable;
|
|
proto_fields.append(self.alloc, vtable_addr) catch unreachable;
|
|
const proto_owned = self.alloc.dupe(Ref, proto_fields.items) catch unreachable;
|
|
return self.builder.emit(.{ .struct_init = .{ .fields = proto_owned } }, proto_ty);
|
|
}
|
|
}
|
|
|
|
/// Emit protocol method dispatch for a protocol-typed receiver.
|
|
/// Returns the call result ref.
|
|
fn emitProtocolDispatch(self: *Lowering, receiver: Ref, proto_info: ProtocolDeclInfo, method_name: []const u8, args: []const Ref, proto_ty: TypeId) Ref {
|
|
// Find method index
|
|
var method_idx: ?usize = null;
|
|
var method_info: ?ProtocolMethodInfo = null;
|
|
for (proto_info.methods, 0..) |m, i| {
|
|
if (std.mem.eql(u8, m.name, method_name)) {
|
|
method_idx = i;
|
|
method_info = m;
|
|
break;
|
|
}
|
|
}
|
|
const mi = method_info orelse return self.emitError(method_name, null);
|
|
const midx = method_idx orelse 0;
|
|
|
|
// Extract ctx from protocol struct (field 0)
|
|
const void_ptr = self.module.types.ptrTo(.void);
|
|
const ctx = self.builder.structGet(receiver, 0, void_ptr);
|
|
|
|
// Extract fn_ptr
|
|
const fn_ptr = if (proto_info.is_inline) blk: {
|
|
// Inline: fn_ptr at field 1+method_idx
|
|
break :blk self.builder.structGet(receiver, @intCast(1 + midx), void_ptr);
|
|
} else blk: {
|
|
// Vtable: load vtable struct, extract fn_ptr at method_idx
|
|
const vtable_ptr = self.builder.structGet(receiver, 1, void_ptr);
|
|
const vtable_ty = self.protocol_vtable_type_map.get(proto_info.name) orelse return self.emitError("vtable", null);
|
|
const vtable = self.builder.emit(.{ .deref = .{ .operand = vtable_ptr } }, vtable_ty);
|
|
break :blk self.builder.structGet(vtable, @intCast(midx), void_ptr);
|
|
};
|
|
_ = proto_ty;
|
|
|
|
// Build call args: [__sx_ctx]? + receiver_ctx + user args.
|
|
// Protocol thunks are sx-side, so they carry the implicit __sx_ctx
|
|
// at slot 0 when the program uses Context — forward our caller's
|
|
// ctx so the thunk's body (and the concrete method it forwards to)
|
|
// sees the same Context as the dispatching code.
|
|
var call_args = std.ArrayList(Ref).empty;
|
|
defer call_args.deinit(self.alloc);
|
|
if (self.implicit_ctx_enabled) {
|
|
call_args.append(self.alloc, self.current_ctx_ref) catch unreachable;
|
|
}
|
|
call_args.append(self.alloc, ctx) catch unreachable;
|
|
for (args, 0..) |a, i| {
|
|
const expected_ty = if (i < mi.param_types.len) mi.param_types[i] else void_ptr;
|
|
const arg_ty = self.builder.getRefType(a);
|
|
|
|
// Untargeted `null` lowers as const_null with type .void. Re-emit it
|
|
// as a null of the expected pointer type instead of alloca'ing void.
|
|
if (arg_ty == .void and expected_ty == void_ptr) {
|
|
call_args.append(self.alloc, self.builder.constNull(void_ptr)) catch unreachable;
|
|
continue;
|
|
}
|
|
// A protocol method that expects `*void` accepts any single-pointer
|
|
// value directly (`*T`, `[*]T`). Only wrap non-pointer values in an
|
|
// alloca-slot — wrapping a pointer would pass the stack slot's
|
|
// address instead of the actual pointer, and the callee would read
|
|
// 8 bytes of pointer plus garbage from beyond the stack.
|
|
const is_pointer_ty = if (!arg_ty.isBuiltin()) blk: {
|
|
const info = self.module.types.get(arg_ty);
|
|
break :blk info == .pointer or info == .many_pointer;
|
|
} else false;
|
|
if (expected_ty == void_ptr and arg_ty != void_ptr and !is_pointer_ty) {
|
|
const slot = self.builder.alloca(arg_ty);
|
|
self.builder.store(slot, a);
|
|
call_args.append(self.alloc, slot) catch unreachable;
|
|
} else {
|
|
// Coerce to match declared parameter type (critical for WASM strict signatures)
|
|
const coerced = self.coerceToType(a, arg_ty, expected_ty);
|
|
call_args.append(self.alloc, coerced) catch unreachable;
|
|
}
|
|
}
|
|
const owned = self.alloc.dupe(Ref, call_args.items) catch unreachable;
|
|
const raw_result = self.builder.emit(.{ .call_indirect = .{ .callee = fn_ptr, .args = owned } }, mi.ret_type);
|
|
|
|
// If the protocol method was declared `-> Self` (encoded here as *void)
|
|
// and the caller expects a value type, unbox: load the concrete value
|
|
// from the returned pointer. A literal `-> *void` return is NOT
|
|
// auto-loaded — it's a real pointer whose pointee size we don't know.
|
|
if (mi.ret_is_self) {
|
|
if (self.target_type) |target| {
|
|
const target_info = self.module.types.get(target);
|
|
if (target_info != .pointer) {
|
|
return self.builder.load(raw_result, target);
|
|
}
|
|
}
|
|
}
|
|
return raw_result;
|
|
}
|
|
|
|
/// Resolve the concrete type name for protocol erasure.
|
|
/// Handles both direct types and pointer-to-types.
|
|
pub fn resolveConcreteTypeName(self: *Lowering, ty: TypeId) ?[]const u8 {
|
|
if (ty.isBuiltin()) {
|
|
// Primitive types like s64 — check if they have toName()
|
|
return self.module.types.typeName(ty);
|
|
}
|
|
const info = self.module.types.get(ty);
|
|
if (info == .pointer) {
|
|
// *ConcreteType → resolve pointee
|
|
const pointee = info.pointer.pointee;
|
|
if (pointee.isBuiltin()) return self.module.types.typeName(pointee);
|
|
const pi = self.module.types.get(pointee);
|
|
if (pi == .@"struct") return self.module.types.getString(pi.@"struct".name);
|
|
return null;
|
|
}
|
|
if (info == .@"struct") return self.module.types.getString(info.@"struct".name);
|
|
return null;
|
|
}
|
|
|
|
// ── Helpers ─────────────────────────────────────────────────────
|
|
|
|
/// Infer the type of an expression from its AST node (used for untyped var decls).
|
|
pub fn inferExprType(self: *Lowering, node: *const Node) TypeId {
|
|
return switch (node.data) {
|
|
.call => |*c| self.callResolver().resultType(c),
|
|
else => self.exprTyper().inferType(node),
|
|
};
|
|
}
|
|
|
|
fn exprTyper(self: *Lowering) ExprTyper {
|
|
return .{ .l = self };
|
|
}
|
|
|
|
fn callResolver(self: *Lowering) CallResolver {
|
|
return .{ .l = self };
|
|
}
|
|
|
|
/// A `Resolver` facade over the borrowed Phase A import facts (Phase B). Cheap
|
|
/// by-value; `collectVisibleAuthors`'s `AuthorSet.flat` slice is backed by
|
|
/// `self.alloc` and owned by the caller (`selectPlainCallableAuthor` frees it).
|
|
fn resolver(self: *Lowering) resolver_mod.Resolver {
|
|
return resolver_mod.Resolver.init(&self.program_index, self.alloc);
|
|
}
|
|
|
|
pub fn genericResolver(self: *Lowering) GenericResolver {
|
|
return .{ .l = self };
|
|
}
|
|
|
|
pub fn protocolResolver(self: *Lowering) ProtocolResolver {
|
|
return .{ .l = self };
|
|
}
|
|
|
|
pub fn coercionResolver(self: *Lowering) CoercionResolver {
|
|
return .{ .l = self };
|
|
}
|
|
|
|
pub fn errorAnalysis(self: *Lowering) ErrorAnalysis {
|
|
return .{ .l = self };
|
|
}
|
|
|
|
pub fn errorFlow(self: *Lowering) ErrorFlow {
|
|
return .{ .l = self };
|
|
}
|
|
|
|
pub fn objc(self: *Lowering) ObjcLowering {
|
|
return .{ .l = self };
|
|
}
|
|
|
|
/// Lower the `xx` operator (type coercion).
|
|
/// Uses self.target_type for context when available. Handles:
|
|
/// - Any → concrete type: unbox_any
|
|
/// - int → int: widen/narrow
|
|
/// - int ↔ float: int_to_float/float_to_int
|
|
fn lowerXX(self: *Lowering, operand: Ref, operand_node: *const Node) Ref {
|
|
// Use the operand's *actual* lowered Ref type rather than reaching
|
|
// back through inferExprType — the latter doesn't cover every
|
|
// expression shape (notably lambdas), and a wrong src_ty here can
|
|
// route the cast through coerceToType (e.g. a bogus s64→ptr bitcast)
|
|
// and silently skip the user-space Into fallback.
|
|
const src_ty = self.builder.getRefType(operand);
|
|
const target_explicit = self.target_type != null;
|
|
const dst_ty = self.target_type orelse .unresolved;
|
|
|
|
// PLANNING: the `xx`-head decision (conversions.zig). `.coerce` falls
|
|
// through to the built-in ladder + the user-`Into` fallback below.
|
|
switch (self.coercionResolver().classifyXX(src_ty, dst_ty)) {
|
|
// Any → concrete type: unbox.
|
|
.unbox_any => {
|
|
// When inside a float match arm covering both f32 and f64,
|
|
// and target is f64, we need a mini-dispatch to unbox correctly.
|
|
// f32 values are stored as zext(bitcast(f32→i32), i64) in Any,
|
|
// so bitcasting i64→f64 directly gives wrong results for f32.
|
|
if (dst_ty == .f64) {
|
|
if (self.current_match_tags) |tags| {
|
|
var has_f32 = false;
|
|
var has_f64 = false;
|
|
for (tags) |t| {
|
|
const tid = TypeId.fromIndex(@intCast(t));
|
|
if (tid == .f32) has_f32 = true;
|
|
if (tid == .f64) has_f64 = true;
|
|
}
|
|
if (has_f32 and has_f64) {
|
|
return self.lowerAnyToF64Dispatch(operand);
|
|
}
|
|
if (has_f32 and !has_f64) {
|
|
// Only f32 values: unbox as f32, then widen
|
|
const f32_val = self.builder.emit(.{ .unbox_any = .{
|
|
.operand = operand,
|
|
} }, .f32);
|
|
return self.builder.emit(.{ .widen = .{ .operand = f32_val, .from = .f32, .to = .f64 } }, .f64);
|
|
}
|
|
}
|
|
}
|
|
return self.builder.emit(.{ .unbox_any = .{
|
|
.operand = operand,
|
|
} }, dst_ty);
|
|
},
|
|
// Same type: no-op.
|
|
.no_op => return operand,
|
|
// Concrete → Protocol: build protocol value.
|
|
.erase_protocol => return self.buildProtocolErasure(operand, operand_node, src_ty, dst_ty),
|
|
// Protocol → pointer: recover the typed ctx pointer (field 0).
|
|
// The protocol value is `{ ctx, fn1, fn2, ... }` (inline) or
|
|
// `{ ctx, vtable_ptr }` — either way, ctx lives at field 0.
|
|
.protocol_to_pointer => {
|
|
const void_ptr_ty = self.module.types.ptrTo(.void);
|
|
const ctx_ref = self.builder.emit(.{ .struct_get = .{ .base = operand, .field_index = 0 } }, void_ptr_ty);
|
|
if (dst_ty == void_ptr_ty) return ctx_ref;
|
|
return self.builder.emit(.{ .bitcast = .{ .operand = ctx_ref, .from = void_ptr_ty, .to = dst_ty } }, dst_ty);
|
|
},
|
|
.coerce => {},
|
|
}
|
|
|
|
const result = self.coerceExplicit(operand, src_ty, dst_ty);
|
|
|
|
// User-space fallback via `impl Into(Target) for Source`. Only fires
|
|
// when the target was explicitly named (not the .s64 default), src and
|
|
// dst differ, and the built-in ladder made no progress. Built-ins
|
|
// always win.
|
|
if (target_explicit and src_ty != dst_ty and result == operand) {
|
|
if (self.tryUserConversion(operand, operand_node, src_ty, dst_ty)) |converted| {
|
|
return converted;
|
|
}
|
|
// Pointer-target fallback: `xx <expr>` whose surrounding context
|
|
// expects `*T` (a fn arg slot, a var typed as a pointer-to-aggregate)
|
|
// can be satisfied by `impl Into(T) for src` plus an implicit
|
|
// alloca+store on the result. Lets users write
|
|
// `fn(xx () => { ... })` instead of materialising a named Block local
|
|
// just to take its address.
|
|
if (!dst_ty.isBuiltin()) {
|
|
const dst_info = self.module.types.get(dst_ty);
|
|
if (dst_info == .pointer) {
|
|
const pointee = dst_info.pointer.pointee;
|
|
if (pointee != src_ty) {
|
|
if (self.tryUserConversion(operand, operand_node, src_ty, pointee)) |converted| {
|
|
const slot = self.builder.alloca(pointee);
|
|
self.builder.store(slot, converted);
|
|
return slot;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// Detect the `xx closure : Block` cast pattern so `tryUserConversion`
|
|
/// can emit a focused diagnostic when no `Into(Block) for Closure(...)`
|
|
/// impl is reachable. Replaces what was briefly a compiler-synthesised
|
|
/// trampoline path with a "declare an impl" requirement — the stdlib
|
|
/// covers common signatures (see modules/std/objc_block.sx), users
|
|
/// add their own for unusual ones.
|
|
fn isClosureToBlockCast(self: *Lowering, src_ty: TypeId, dst_ty: TypeId) bool {
|
|
if (src_ty.isBuiltin()) return false;
|
|
const src_info = self.module.types.get(src_ty);
|
|
if (src_info != .closure) return false;
|
|
if (dst_ty.isBuiltin()) return false;
|
|
const dst_info = self.module.types.get(dst_ty);
|
|
if (dst_info != .@"struct") return false;
|
|
const block_name = self.module.types.internString("Block");
|
|
return dst_info.@"struct".name == block_name;
|
|
}
|
|
|
|
/// Pack-variadic impl matching. Walks `param_impl_pack_map[pack_key]`
|
|
/// and returns a call ref when a single pack impl matches `src_ty`'s
|
|
/// shape (concrete src closure / fn with the same fixed prefix as
|
|
/// the impl's source pack closure). Binds the pack-var to the source's
|
|
/// tail param types and the return-var (when generic) to the source's
|
|
/// return type, then monomorphises the convert method.
|
|
/// Returns null if no pack impls registered for this (proto, dst) or
|
|
/// none of them match `src_ty`'s shape.
|
|
fn tryPackImplMatch(
|
|
self: *Lowering,
|
|
operand: Ref,
|
|
operand_node: *const Node,
|
|
src_ty: TypeId,
|
|
dst_ty: TypeId,
|
|
proto_name: []const u8,
|
|
pack_key: []const u8,
|
|
guard_key: u64,
|
|
) ?Ref {
|
|
_ = operand_node;
|
|
// PLANNING: select the matching pack impl + its `convert` (registry).
|
|
const match = self.protocolResolver().matchPackImpl(src_ty, pack_key) orelse return null;
|
|
const entry = match.entry;
|
|
const fd = match.convert_fd;
|
|
const src_params = match.src_params;
|
|
const src_ret = match.src_ret;
|
|
const table = &self.module.types;
|
|
// EMISSION: bind the pack tail + ret-var, monomorphise, call (Lowering).
|
|
|
|
// Build bindings. Target → dst_ty (already in the protocol's type
|
|
// params), pack-var → src tail TypeIds, ret-var (when generic) →
|
|
// src ret.
|
|
const ent_pack_start = table.get(entry.source_pack_ty).closure.pack_start.?;
|
|
const tail = src_params[ent_pack_start..];
|
|
const tail_owned = self.alloc.dupe(TypeId, tail) catch return null;
|
|
|
|
var bindings = std.StringHashMap(TypeId).init(self.alloc);
|
|
defer bindings.deinit();
|
|
const pd = self.program_index.protocol_ast_map.get(proto_name) orelse return null;
|
|
bindings.put(pd.type_params[0].name, dst_ty) catch return null;
|
|
if (entry.ret_var_name) |rv| bindings.put(rv, src_ret) catch return null;
|
|
|
|
var pack_bindings = std.StringHashMap([]const TypeId).init(self.alloc);
|
|
defer pack_bindings.deinit();
|
|
pack_bindings.put(entry.pack_var_name, tail_owned) catch return null;
|
|
|
|
// Mangled name keyed on the CONCRETE source so distinct shapes
|
|
// monomorphise separately. Same scheme as the concrete path:
|
|
// "<src>.convert__<dst>".
|
|
const mangled = std.fmt.allocPrint(self.alloc, "{s}.convert__{s}", .{
|
|
self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty),
|
|
}) catch return null;
|
|
|
|
self.xx_reentrancy.put(guard_key, {}) catch {};
|
|
defer _ = self.xx_reentrancy.remove(guard_key);
|
|
|
|
if (!self.lowered_functions.contains(mangled)) {
|
|
const saved_pack = self.pack_bindings;
|
|
self.pack_bindings = pack_bindings;
|
|
defer self.pack_bindings = saved_pack;
|
|
self.monomorphizeFunction(fd, mangled, &bindings);
|
|
}
|
|
|
|
const fid = self.resolveFuncByName(mangled) orelse return null;
|
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
|
const ret_ty = func.ret;
|
|
const params = func.params;
|
|
var single = [_]Ref{operand};
|
|
const final_args = self.prependCtxIfNeeded(func, single[0..]);
|
|
self.coerceCallArgs(final_args, params);
|
|
return self.builder.call(fid, final_args, ret_ty);
|
|
}
|
|
|
|
/// Look up `Into(dst_ty)` impl for `src_ty` and, if found, monomorphise
|
|
/// the impl's `convert` method and emit a direct call. Returns null when
|
|
/// no impl matches (caller falls back to the built-in result, which is
|
|
/// the unchanged operand — Phase 3 emits no diagnostic for v0).
|
|
fn tryUserConversion(self: *Lowering, operand: Ref, operand_node: *const Node, src_ty: TypeId, dst_ty: TypeId) ?Ref {
|
|
// Reentrancy guard — pack (src, dst) into a u64.
|
|
const guard_key: u64 = (@as(u64, src_ty.index()) << 32) | @as(u64, dst_ty.index());
|
|
if (self.xx_reentrancy.contains(guard_key)) {
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, operand_node.span, "recursive xx conversion from '{s}' to '{s}'", .{
|
|
self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty),
|
|
});
|
|
}
|
|
return operand;
|
|
}
|
|
|
|
// Build lookup key: "Into\x00<dst_mangled>\x00<src_mangled>".
|
|
// Hardcoded to the "Into" protocol for v1. Generalising to other
|
|
// parameterised protocols would walk protocol_decl_map looking for
|
|
// protocols that take a single type-param and have a `convert` method.
|
|
const proto_name = "Into";
|
|
const pd = self.program_index.protocol_ast_map.get(proto_name) orelse return null;
|
|
if (pd.type_params.len != 1) return null;
|
|
|
|
var key_buf = std.ArrayList(u8).empty;
|
|
key_buf.appendSlice(self.alloc, proto_name) catch return null;
|
|
key_buf.append(self.alloc, 0) catch return null;
|
|
key_buf.appendSlice(self.alloc, self.mangleTypeName(dst_ty)) catch return null;
|
|
key_buf.append(self.alloc, 0) catch return null;
|
|
key_buf.appendSlice(self.alloc, self.mangleTypeName(src_ty)) catch return null;
|
|
const key = key_buf.items;
|
|
|
|
// Pack-only key (proto + dst) — used if the concrete lookup misses.
|
|
// Same prefix as the concrete key, minus the `\x00<src_mangled>` tail.
|
|
const dst_mangled_len = self.mangleTypeName(dst_ty).len;
|
|
const pack_key = key_buf.items[0 .. proto_name.len + 1 + dst_mangled_len];
|
|
|
|
const entries_opt = self.param_impl_map.get(key);
|
|
const has_concrete = entries_opt != null and entries_opt.?.items.len > 0;
|
|
if (!has_concrete) {
|
|
// Concrete miss — try the pack map before emitting a diagnostic.
|
|
if (self.tryPackImplMatch(operand, operand_node, src_ty, dst_ty, proto_name, pack_key, guard_key)) |result| {
|
|
return result;
|
|
}
|
|
if (self.isClosureToBlockCast(src_ty, dst_ty)) {
|
|
if (self.diagnostics) |diags| {
|
|
const saved = diags.current_source_file;
|
|
diags.current_source_file = operand_node.source_file orelse self.current_source_file;
|
|
defer diags.current_source_file = saved;
|
|
diags.addFmt(.err, operand_node.span, "no `Into(Block) for {s}` impl — add a per-signature `__block_invoke_<sig>` trampoline + Into impl alongside the existing ones in modules/std/objc_block.sx, or declare it in your own code", .{self.mangleTypeName(src_ty)});
|
|
}
|
|
return operand;
|
|
}
|
|
return null;
|
|
}
|
|
const entries = entries_opt.?;
|
|
|
|
// Filter by import visibility: only impls in modules that the current
|
|
// file transitively imports (or the current file itself) are reachable.
|
|
// Falls open when import_graph isn't wired (e.g. comptime callers).
|
|
var visible_impls = std.ArrayList(ParamImplEntry).empty;
|
|
defer visible_impls.deinit(self.alloc);
|
|
self.protocolResolver().findVisibleImpls(entries.items, &visible_impls);
|
|
|
|
if (visible_impls.items.len == 0) {
|
|
if (self.diagnostics) |diags| {
|
|
const saved = diags.current_source_file;
|
|
diags.current_source_file = operand_node.source_file orelse self.current_source_file;
|
|
defer diags.current_source_file = saved;
|
|
diags.addFmt(.err, operand_node.span, "no visible xx conversion from '{s}' to '{s}' — impl exists in another module but is not imported", .{
|
|
self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty),
|
|
});
|
|
}
|
|
return operand;
|
|
}
|
|
if (visible_impls.items.len > 1) {
|
|
if (self.diagnostics) |diags| {
|
|
const saved = diags.current_source_file;
|
|
diags.current_source_file = operand_node.source_file orelse self.current_source_file;
|
|
defer diags.current_source_file = saved;
|
|
diags.addFmt(.err, operand_node.span, "duplicate xx conversion from '{s}' to '{s}': impls in {s} and {s}", .{
|
|
self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty),
|
|
visible_impls.items[0].defining_module, visible_impls.items[1].defining_module,
|
|
});
|
|
}
|
|
return operand;
|
|
}
|
|
const entry = visible_impls.items[0];
|
|
|
|
// Find the `convert` method on this impl.
|
|
var convert_fd: ?*const ast.FnDecl = null;
|
|
for (entry.methods) |m| {
|
|
if (std.mem.eql(u8, m.name, "convert")) {
|
|
convert_fd = m;
|
|
break;
|
|
}
|
|
}
|
|
const fd = convert_fd orelse return null;
|
|
|
|
// Bind Target → dst_ty.
|
|
var bindings = std.StringHashMap(TypeId).init(self.alloc);
|
|
defer bindings.deinit();
|
|
bindings.put(pd.type_params[0].name, dst_ty) catch return null;
|
|
|
|
// Mangled name: "<src>.convert__<dst>".
|
|
const mangled = std.fmt.allocPrint(self.alloc, "{s}.convert__{s}", .{
|
|
self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty),
|
|
}) catch return null;
|
|
|
|
self.xx_reentrancy.put(guard_key, {}) catch {};
|
|
defer _ = self.xx_reentrancy.remove(guard_key);
|
|
|
|
if (!self.lowered_functions.contains(mangled)) {
|
|
self.monomorphizeFunction(fd, mangled, &bindings);
|
|
}
|
|
|
|
const fid = self.resolveFuncByName(mangled) orelse return null;
|
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
|
const ret_ty = func.ret;
|
|
const params = func.params;
|
|
var single = [_]Ref{operand};
|
|
const final_args = self.prependCtxIfNeeded(func, single[0..]);
|
|
self.coerceCallArgs(final_args, params);
|
|
return self.builder.call(fid, final_args, ret_ty);
|
|
}
|
|
|
|
/// True for expression shapes that name an addressable storage location
|
|
/// (variables, fields, array elements, dereferenced pointers). Used by
|
|
/// `xx <struct-typed expr>` to decide between borrow (lvalue → take the
|
|
/// address) and heap-copy (rvalue → allocate a fresh copy).
|
|
fn isLvalueExpr(self: *Lowering, node: *const Node) bool {
|
|
_ = self;
|
|
return switch (node.data) {
|
|
.identifier, .field_access, .index_expr, .deref_expr => true,
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
/// Build a protocol value from a concrete value via xx conversion.
|
|
/// Coerce `val` (type `src`) to `dst`: if `dst` is a protocol, `xx`-erase
|
|
/// the concrete value into it; otherwise fall back to numeric/struct
|
|
/// coercion. Used to materialize a pack into a protocol-typed tuple field.
|
|
fn coerceOrErase(self: *Lowering, val: Ref, src: TypeId, dst: TypeId, node: *const Node) Ref {
|
|
if (src == dst) return val;
|
|
if (!dst.isBuiltin()) {
|
|
const di = self.module.types.get(dst);
|
|
if (di == .@"struct" and di.@"struct".is_protocol) {
|
|
return self.buildProtocolErasure(val, node, src, dst);
|
|
}
|
|
}
|
|
return self.coerceToType(val, src, dst);
|
|
}
|
|
|
|
fn buildProtocolErasure(self: *Lowering, operand: Ref, operand_node: *const Node, src_ty: TypeId, dst_ty: TypeId) Ref {
|
|
const dst_info = self.module.types.get(dst_ty);
|
|
if (dst_info != .@"struct") return operand;
|
|
const proto_name = self.module.types.getString(dst_info.@"struct".name);
|
|
|
|
// Determine concrete type name and type — resolve through pointer if needed
|
|
var concrete_ptr = operand;
|
|
var concrete_type_name: ?[]const u8 = null;
|
|
var concrete_ty: TypeId = src_ty;
|
|
var heap_copy = false;
|
|
|
|
if (!src_ty.isBuiltin()) {
|
|
const src_info = self.module.types.get(src_ty);
|
|
if (src_info == .pointer) {
|
|
// xx @acc — operand is already a pointer (user manages lifetime)
|
|
const pointee = src_info.pointer.pointee;
|
|
concrete_type_name = self.resolveConcreteTypeName(pointee);
|
|
concrete_ty = pointee;
|
|
heap_copy = false;
|
|
} else if (src_info == .@"struct") {
|
|
// Struct-typed operand. Split on lvalue-ness:
|
|
// - lvalue (identifier, field, index, deref): borrow the
|
|
// storage the operand already names. No heap copy; the
|
|
// protocol value's ctx points at the caller's slot, and
|
|
// mutations through the protocol are visible to the
|
|
// original. Lifetime is the caller's responsibility.
|
|
// - rvalue (struct literal, call result, etc.): heap-copy
|
|
// into a fresh allocation so the protocol value is
|
|
// self-contained and outlives this expression.
|
|
concrete_type_name = self.module.types.getString(src_info.@"struct".name);
|
|
concrete_ty = src_ty;
|
|
if (self.isLvalueExpr(operand_node)) {
|
|
concrete_ptr = self.lowerExprAsPtr(operand_node);
|
|
heap_copy = false;
|
|
} else {
|
|
heap_copy = true;
|
|
const slot = self.builder.alloca(src_ty);
|
|
self.builder.store(slot, operand);
|
|
concrete_ptr = slot;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also try from the operand node for struct literals: xx Accumulator.{ total = 0 }
|
|
if (concrete_type_name == null) {
|
|
concrete_type_name = self.inferConcreteTypeName(operand_node);
|
|
if (concrete_type_name != null) heap_copy = true;
|
|
}
|
|
|
|
if (concrete_type_name) |ctn| {
|
|
return self.buildProtocolValue(concrete_ptr, proto_name, ctn, dst_ty, concrete_ty, heap_copy);
|
|
}
|
|
return operand;
|
|
}
|
|
|
|
/// Try to infer the concrete type name from an AST node (for struct literals etc.)
|
|
fn inferConcreteTypeName(self: *Lowering, node: *const Node) ?[]const u8 {
|
|
return switch (node.data) {
|
|
.struct_literal => |sl| if (sl.struct_name) |n| n else null,
|
|
.unary_op => |uop| if (uop.op == .address_of) self.inferConcreteTypeName(uop.operand) else null,
|
|
.identifier => |id| blk: {
|
|
// Check if identifier's type resolves to a struct
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(id.name)) |binding| {
|
|
if (!binding.ty.isBuiltin()) {
|
|
const bi = self.module.types.get(binding.ty);
|
|
if (bi == .@"struct") break :blk self.module.types.getString(bi.@"struct".name);
|
|
if (bi == .pointer) {
|
|
const pointee = bi.pointer.pointee;
|
|
if (!pointee.isBuiltin()) {
|
|
const pi = self.module.types.get(pointee);
|
|
if (pi == .@"struct") break :blk self.module.types.getString(pi.@"struct".name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break :blk null;
|
|
},
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
/// Generate a mini-dispatch for unboxing Any to f64 when the value might be f32 or f64.
|
|
/// Uses alloca-based merge: create result slot, branch, store in each arm, load after merge.
|
|
fn lowerAnyToF64Dispatch(self: *Lowering, any_val: Ref) Ref {
|
|
// Create result alloca BEFORE the branch
|
|
const result_slot = self.builder.alloca(.f64);
|
|
|
|
// Extract type tag from Any
|
|
const tag = self.builder.structGet(any_val, 0, .s64);
|
|
|
|
const f32_bb = self.freshBlock("f32.unbox");
|
|
const f64_bb = self.freshBlock("f64.unbox");
|
|
const merge_bb = self.freshBlock("float.merge");
|
|
|
|
// Branch: tag == f32_tag ? f32_bb : f64_bb
|
|
const f32_tag = self.builder.constInt(TypeId.f32.index(), .s64);
|
|
const cond = self.builder.emit(.{ .cmp_eq = .{ .lhs = tag, .rhs = f32_tag } }, .bool);
|
|
self.builder.condBr(cond, f32_bb, &.{}, f64_bb, &.{});
|
|
|
|
// f32 block: unbox as f32, fpext to f64, store
|
|
self.builder.switchToBlock(f32_bb);
|
|
const f32_val = self.builder.emit(.{ .unbox_any = .{
|
|
.operand = any_val,
|
|
} }, .f32);
|
|
const f64_from_f32 = self.builder.emit(.{ .widen = .{ .operand = f32_val, .from = .f32, .to = .f64 } }, .f64);
|
|
self.builder.store(result_slot, f64_from_f32);
|
|
self.builder.br(merge_bb, &.{});
|
|
|
|
// f64 block: unbox as f64 directly, store
|
|
self.builder.switchToBlock(f64_bb);
|
|
const f64_val = self.builder.emit(.{ .unbox_any = .{
|
|
.operand = any_val,
|
|
} }, .f64);
|
|
self.builder.store(result_slot, f64_val);
|
|
self.builder.br(merge_bb, &.{});
|
|
|
|
// Merge block: load result
|
|
self.builder.switchToBlock(merge_bb);
|
|
return self.builder.load(result_slot, .f64);
|
|
}
|
|
|
|
/// Produce a default value for a type, applying struct field defaults.
|
|
/// For structs with defaults (e.g., `b: s32 = 99`), creates a struct_literal with defaults applied.
|
|
/// For other types, returns a zero value.
|
|
fn buildDefaultValue(self: *Lowering, ty: TypeId) Ref {
|
|
if (ty.isBuiltin()) return self.builder.constInt(0, ty);
|
|
const info = self.module.types.get(ty);
|
|
if (info != .@"struct" and info != .tuple) return self.zeroValue(ty);
|
|
// For tuples, build a zero-initialized tuple
|
|
if (info == .tuple) {
|
|
var field_vals = std.ArrayList(Ref).empty;
|
|
defer field_vals.deinit(self.alloc);
|
|
for (info.tuple.fields) |f| {
|
|
field_vals.append(self.alloc, self.zeroValue(f)) catch unreachable;
|
|
}
|
|
return self.builder.emit(.{
|
|
.tuple_init = .{ .fields = self.alloc.dupe(Ref, field_vals.items) catch unreachable },
|
|
}, ty);
|
|
}
|
|
// Check for struct defaults
|
|
const struct_name_str = self.module.types.getString(info.@"struct".name);
|
|
const field_defaults = self.struct_defaults_map.get(struct_name_str) orelse
|
|
return self.builder.constUndef(ty);
|
|
const fields = info.@"struct".fields;
|
|
var field_vals = std.ArrayList(Ref).empty;
|
|
defer field_vals.deinit(self.alloc);
|
|
for (fields, 0..) |f, i| {
|
|
if (i < field_defaults.len) {
|
|
if (field_defaults[i]) |default_expr| {
|
|
field_vals.append(self.alloc, self.lowerCoercedDefault(default_expr, f.ty)) catch unreachable;
|
|
} else {
|
|
field_vals.append(self.alloc, self.zeroValue(f.ty)) catch unreachable;
|
|
}
|
|
} else {
|
|
field_vals.append(self.alloc, self.zeroValue(f.ty)) catch unreachable;
|
|
}
|
|
}
|
|
return self.builder.emit(.{
|
|
.struct_init = .{ .fields = self.alloc.dupe(Ref, field_vals.items) catch unreachable },
|
|
}, ty);
|
|
}
|
|
|
|
/// Wrap ty in ?ty, but flatten: if ty is already ?U, return ?U (not ??U)
|
|
pub fn optionalOfFlattened(self: *Lowering, ty: TypeId) TypeId {
|
|
if (!ty.isBuiltin()) {
|
|
const info = self.module.types.get(ty);
|
|
if (info == .optional) return ty;
|
|
}
|
|
return self.module.types.optionalOf(ty);
|
|
}
|
|
|
|
/// Produce a zero/default value for any type — constInt(0) for integers,
|
|
/// constNull for pointers, constUndef for structs/complex types.
|
|
fn zeroValue(self: *Lowering, ty: TypeId) Ref {
|
|
if (ty.isBuiltin()) return self.builder.constInt(0, ty);
|
|
const info = self.module.types.get(ty);
|
|
return switch (info) {
|
|
// Arbitrary-width integer types (u1, u2, s4, ...) interned as
|
|
// `.signed`/`.unsigned` variants — fall through `isBuiltin()`.
|
|
.signed, .unsigned => self.builder.constInt(0, ty),
|
|
.pointer, .tuple, .optional => self.builder.constNull(ty),
|
|
.@"struct", .array, .slice, .many_pointer => self.builder.constNull(ty),
|
|
else => self.builder.constUndef(ty),
|
|
};
|
|
}
|
|
|
|
fn emitModuleConst(self: *Lowering, ci: ModuleConstInfo) Ref {
|
|
// An integer-typed const whose initializer is a compile-time integer —
|
|
// an int literal/expression, OR an INTEGRAL float that `typedConstInitFits`
|
|
// accepted under the unified narrowing rule — materializes as its folded
|
|
// int through the SAME `program_index.foldCountI64` the count / array-dim
|
|
// path uses, so the const's emitted VALUE and its use as a COUNT come from
|
|
// one fold (`K : s64 : 4.0` → 4; `K : s64 : M + 2.0` → 4; and a float-const-
|
|
// leaf `KF : s64 : F + 1.5` → 4, which the int-only folder could not reach).
|
|
// A non-integral float never arrives (it was rejected at registration); any
|
|
// other non-foldable shape falls through to the per-kind emitters below.
|
|
if (self.isIntEx(ci.ty)) {
|
|
switch (program_index_mod.foldCountI64(ci.value, self)) {
|
|
.int => |iv| return self.builder.constInt(iv, ci.ty),
|
|
.non_integral, .not_const => {},
|
|
}
|
|
}
|
|
switch (ci.value.data) {
|
|
.int_literal => |lit| {
|
|
// If declared type is float, convert integer value to float constant
|
|
if (ci.ty == .f32 or ci.ty == .f64) {
|
|
return self.builder.constFloat(@floatFromInt(lit.value), ci.ty);
|
|
}
|
|
return self.builder.constInt(lit.value, ci.ty);
|
|
},
|
|
.float_literal => |lit| return self.builder.constFloat(lit.value, ci.ty),
|
|
.bool_literal => |lit| return self.builder.emit(.{ .const_bool = lit.value }, .bool),
|
|
.string_literal => |lit| {
|
|
const str = if (lit.is_raw) lit.raw else unescape.unescapeString(self.alloc, lit.raw) catch lit.raw;
|
|
const sid = self.module.types.internString(str);
|
|
return self.builder.constString(sid);
|
|
},
|
|
.undef_literal => return self.builder.constUndef(ci.ty),
|
|
.null_literal => return self.builder.constNull(ci.ty),
|
|
else => {
|
|
// Complex expressions (struct_literal, call, etc.) — lower on demand
|
|
const saved_target = self.target_type;
|
|
self.target_type = ci.ty;
|
|
const result = self.lowerExpr(ci.value);
|
|
self.target_type = saved_target;
|
|
return result;
|
|
},
|
|
}
|
|
}
|
|
|
|
fn emitPlaceholder(self: *Lowering, name: []const u8) Ref {
|
|
const sid = self.module.types.internString(name);
|
|
return self.builder.emit(.{ .placeholder = sid }, .s64);
|
|
}
|
|
|
|
/// Check if a name refers to a known type (primitive or registered struct/enum/union).
|
|
/// Used to distinguish type-as-value (silent placeholder) from genuinely unresolved names.
|
|
pub fn isKnownTypeName(self: *Lowering, name: []const u8) bool {
|
|
if (type_bridge.resolveTypePrimitive(name) != null) return true;
|
|
if (self.type_bindings) |bindings| {
|
|
if (bindings.get(name) != null) return true;
|
|
}
|
|
if (self.program_index.type_alias_map.get(name) != null) return true;
|
|
const name_id = self.module.types.internString(name);
|
|
return self.module.types.findByName(name_id) != null;
|
|
}
|
|
|
|
/// Update `self.current_source_file` and mirror it onto `diags.current_source_file`,
|
|
/// so any diagnostic emitted from inside a function lowered from another module is
|
|
/// attributed to that module — not whichever file the diagnostics list was init'd with.
|
|
fn setCurrentSourceFile(self: *Lowering, source_file: ?[]const u8) void {
|
|
self.current_source_file = source_file;
|
|
if (self.diagnostics) |d| d.current_source_file = source_file;
|
|
}
|
|
|
|
/// Stamp a caller-provided comptime `$`-arg node with the caller's source
|
|
/// file. When the node is later substituted into the (defining-module-pinned)
|
|
/// metaprogram body and lowered, lowerExpr's per-node source switch resolves
|
|
/// its bare names in the CALLER's visibility context — not the callee's — so
|
|
/// a caller-owned helper passed to an imported metaprogram stays visible.
|
|
/// Only stamps a node with no source yet, and only when the caller context
|
|
/// is known; an unknown caller source leaves the node's fall-open intact.
|
|
fn stampCallerSource(self: *Lowering, node: *Node) void {
|
|
if (node.source_file != null) return;
|
|
if (self.current_source_file) |src| node.source_file = src;
|
|
}
|
|
|
|
fn emitError(self: *Lowering, name: []const u8, span: ?ast.Span) Ref {
|
|
if (self.diagnostics) |diags| {
|
|
// The literal message carries the lowering's `current_source_file`
|
|
// and enclosing function name. The diagnostic renderer's
|
|
// `source_file` -> `file:line:col` prefix can drift when a span is
|
|
// offset into one source but the diagnostic falls back to another
|
|
// (e.g. synthetic AST nodes inserted from `#insert` take their
|
|
// span from the call site, not from the string being inserted).
|
|
// Embedding the file + function in the message means a
|
|
// misattributed span can never hide WHERE the lookup actually
|
|
// failed. Setting SX_TRACE_UNRESOLVED=1 also dumps a Zig stack
|
|
// trace at the emit site to surface the calling lowering path.
|
|
const sf = self.current_source_file orelse "<unknown>";
|
|
const fn_name: []const u8 = if (self.builder.func) |fid|
|
|
self.module.types.getString(self.module.functions.items[@intFromEnum(fid)].name)
|
|
else
|
|
"<top-level>";
|
|
if (std.c.getenv("SX_TRACE_UNRESOLVED") != null) {
|
|
std.debug.print("\n== unresolved '{s}' (in {s} fn {s}) ==\n", .{ name, sf, fn_name });
|
|
std.debug.dumpCurrentStackTrace(.{ .first_address = @returnAddress() });
|
|
}
|
|
diags.addFmt(.err, span, "unresolved '{s}' (in {s} fn {s})", .{ name, sf, fn_name });
|
|
}
|
|
return self.emitPlaceholder(name);
|
|
}
|
|
|
|
fn emitFieldError(self: *Lowering, obj_ty: TypeId, field: []const u8, span: ast.Span) Ref {
|
|
// A field access on an already-`.unresolved` object is a cascade from an
|
|
// upstream type-resolution failure that was ALREADY diagnosed (e.g. an
|
|
// unresolvable / oversized array dimension — issue 0083). The
|
|
// `.unresolved` sentinel never exists without an accompanying error, so
|
|
// piling a second "field not found on unresolved" onto the real one is
|
|
// pure noise; stay silent and return a placeholder so lowering finishes
|
|
// and `hasErrors()` aborts the build on the genuine diagnostic.
|
|
if (obj_ty != .unresolved) {
|
|
if (self.diagnostics) |diags| {
|
|
const ty_name = self.formatTypeName(obj_ty);
|
|
diags.addFmt(.err, span, "field '{s}' not found on type '{s}'", .{ field, ty_name });
|
|
}
|
|
}
|
|
return self.emitPlaceholder(field);
|
|
}
|
|
|
|
/// Emit the unified non-integral float→int narrowing diagnostic (F0.11 /
|
|
/// issue 0095). ONE wording, ONE place: every site that rejects an implicit
|
|
/// narrowing of a non-integral compile-time float to an integer type calls
|
|
/// this, so the message + fix-it stay identical across the typed-binding
|
|
/// coerce arm, the field/param-default sites, the typed-const path, and the
|
|
/// global-initializer path.
|
|
fn diagNonIntegralNarrow(self: *Lowering, span: ast.Span, value: f64, dst_ty: TypeId) void {
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, span, "cannot implicitly narrow non-integral float '{d}' to '{s}'; use an explicit cast (`xx`/`cast`)", .{ value, self.formatTypeName(dst_ty) });
|
|
}
|
|
|
|
/// Apply the unified float→int narrowing rule to a typed-binding initializer
|
|
/// EXPRESSION `node` whose declared type is `dst` (a typed local, a struct
|
|
/// field default, or a call argument incl. an expanded param default). When
|
|
/// `node` is a COMPILE-TIME float narrowing into an integer type:
|
|
/// - an INTEGRAL value (`4.0`, `M + 2.0`) folds to its `constInt`;
|
|
/// - a NON-integral value (`1.5`, `M + 0.5`) emits the narrowing
|
|
/// diagnostic and returns a placeholder so lowering finishes.
|
|
/// Returns null — so the caller lowers `node` normally — when the rule does
|
|
/// not apply: `dst` is not an integer, `node` is not statically float-typed,
|
|
/// or `node` is not a compile-time constant (a genuine runtime float keeps
|
|
/// truncating, and `xx` / `cast` keep their explicit-truncation escape since
|
|
/// a cast node's inferred type is the destination integer, not a float).
|
|
/// Reuses `program_index.evalConstIntExpr` (exact integral fold) +
|
|
/// `evalConstFloatExpr` (non-integral detection) + `floatToIntExact`.
|
|
fn foldComptimeFloatInit(self: *Lowering, node: *const Node, dst: TypeId) ?Ref {
|
|
if (!self.isIntEx(dst)) return null;
|
|
// PURE & side-effect-free, so it runs FIRST: a runtime / non-comptime /
|
|
// non-numeric node — incl. a `$pack[i]` index expression — folds to null
|
|
// and is left to the normal path untouched. (Calling `inferExprType` on
|
|
// a pack-index value before this guard would spuriously resolve the
|
|
// enclosing pack type outside an active binding.)
|
|
const fv = program_index_mod.evalConstFloatExpr(node, self) orelse return null;
|
|
// Only a FLOAT-flavored initializer narrows here; a plain comptime int
|
|
// (`5`, `M + 2`) is left to the normal integer path. Safe to infer now —
|
|
// `evalConstFloatExpr` only succeeds for literal / const-arithmetic
|
|
// nodes, never an unbound pack index. `inferExprType` is the primary
|
|
// signal, but it reads a const's DECLARED type — which is a placeholder
|
|
// `s64` for an untyped float-EXPRESSION const (`ME :: 4.0 + 1.0`), so
|
|
// `ME / 2` would look like integer division; `isFloatValuedExpr` (judging
|
|
// by VALUE) catches that case so it narrows under the unified rule too.
|
|
if (!isFloat(self.inferExprType(node)) and !program_index_mod.isFloatValuedExpr(node, self)) return null;
|
|
// Integral comptime float folds to its int (`floatToIntExact`, the same
|
|
// facility the array-dim / `$K: Count` paths use); a non-integral one is
|
|
// the narrowing error.
|
|
if (program_index_mod.floatToIntExact(fv)) |iv| return self.builder.constInt(iv, dst);
|
|
self.diagNonIntegralNarrow(node.span, fv, dst);
|
|
return self.builder.constInt(0, dst);
|
|
}
|
|
|
|
/// Lower a struct field default `default_expr`, coerced to the field type
|
|
/// `field_ty`. A compile-time float default narrowing into an integer field
|
|
/// follows the unified rule via `foldComptimeFloatInit`; everything else
|
|
/// lowers under the field type as target and coerces at the IR level.
|
|
fn lowerCoercedDefault(self: *Lowering, default_expr: *const Node, field_ty: TypeId) Ref {
|
|
if (self.foldComptimeFloatInit(default_expr, field_ty)) |folded| return folded;
|
|
const saved_tt = self.target_type;
|
|
self.target_type = field_ty;
|
|
const raw = self.lowerExpr(default_expr);
|
|
self.target_type = saved_tt;
|
|
return self.coerceToType(raw, self.builder.getRefType(raw), field_ty);
|
|
}
|
|
|
|
/// How a float→int conversion is treated. An IMPLICIT coercion (a typed
|
|
/// binding initializer) folds an integral compile-time float to its int and
|
|
/// REJECTS a non-integral one; an EXPLICIT `xx` / `cast` always truncates.
|
|
const CoerceMode = enum { implicit, explicit };
|
|
|
|
/// Insert a conversion if src_ty and dst_ty differ.
|
|
/// Handles int widening/narrowing, float widening/narrowing, and int↔float.
|
|
/// IMPLICIT coercion — the typed-binding initializer path. A compile-time
|
|
/// float narrowing to an integer folds when integral, errors when not.
|
|
fn coerceToType(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId) Ref {
|
|
return self.coerceMode(val, src_ty, dst_ty, .implicit);
|
|
}
|
|
|
|
/// EXPLICIT coercion — the `xx` / `cast(T)` escape hatch. A float→int here
|
|
/// always truncates, bypassing the integral-fold / non-integral-error rule.
|
|
fn coerceExplicit(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId) Ref {
|
|
return self.coerceMode(val, src_ty, dst_ty, .explicit);
|
|
}
|
|
|
|
fn coerceMode(self: *Lowering, val: Ref, src_ty: TypeId, dst_ty: TypeId, mode: CoerceMode) Ref {
|
|
// PLANNING: classify the built-in coercion (conversions.zig).
|
|
// EMISSION: each arm below reproduces the original lowering.
|
|
switch (self.coercionResolver().classify(src_ty, dst_ty)) {
|
|
.no_op, .none => return val,
|
|
// Unbox Any → concrete type
|
|
.unbox_any => return self.builder.emit(.{ .unbox_any = .{ .operand = val } }, dst_ty),
|
|
// Box concrete → Any
|
|
.box_any => return self.builder.boxAny(val, src_ty),
|
|
// Closure VALUE → bare function-pointer slot: not soundly representable.
|
|
// A bare `(T) -> U` slot is called as `fn_ptr(ctx, args)` with NO env
|
|
// arg, but a closure's underlying fn takes an env slot — so passing a
|
|
// closure value's fn_ptr drops the env and shifts the args (UB for a
|
|
// matching ABI, a wrong-tuple read for ∅-widening, a segfault when the
|
|
// closure captures). Only a closure LITERAL can cross this boundary,
|
|
// via the static adapter `lowerLambda` emits (so a literal arrives here
|
|
// already typed `.function`). Reject the variable case loudly.
|
|
.closure_to_fn_reject => {
|
|
if (self.diagnostics) |d| {
|
|
const cs = self.builder.current_span;
|
|
d.addFmt(.err, ast.Span{ .start = cs.start, .end = cs.end }, "a closure value cannot be passed as a bare function-pointer `(...) -> ...` — its environment can't be carried across the bare ABI; pass the closure literal directly at the call site, or declare the parameter type as `Closure(...)`", .{});
|
|
}
|
|
return val;
|
|
},
|
|
// Tuple → Tuple element-wise coercion (e.g. a `(s64, s64)` literal
|
|
// flowing into a `(s32, s32)` slot — the multi-value failable success
|
|
// tuple). Same arity: extract each slot, coerce it, rebuild.
|
|
.tuple_elementwise => {
|
|
const si = self.module.types.get(src_ty);
|
|
const di = self.module.types.get(dst_ty);
|
|
var elems = std.ArrayList(Ref).empty;
|
|
defer elems.deinit(self.alloc);
|
|
for (si.tuple.fields, di.tuple.fields, 0..) |sf, df, i| {
|
|
const fv = self.builder.emit(.{ .tuple_get = .{ .base = val, .field_index = @intCast(i), .base_type = src_ty } }, sf);
|
|
elems.append(self.alloc, self.coerceMode(fv, sf, df, mode)) catch unreachable;
|
|
}
|
|
return self.builder.emit(.{ .tuple_init = .{ .fields = self.alloc.dupe(Ref, elems.items) catch unreachable } }, dst_ty);
|
|
},
|
|
// Optional → Concrete unwrapping (flow-sensitive narrowing coercion)
|
|
.optional_unwrap => {
|
|
const child_ty = self.module.types.get(src_ty).optional.child;
|
|
const unwrapped = self.builder.emit(.{ .optional_unwrap = .{ .operand = val } }, child_ty);
|
|
return self.coerceMode(unwrapped, child_ty, dst_ty, mode);
|
|
},
|
|
// void → Optional: produce null (void is the type of null_literal)
|
|
.void_to_optional => return self.builder.constNull(dst_ty),
|
|
// Concrete → Optional wrapping (coerce to the inner type first)
|
|
.optional_wrap => {
|
|
const child_ty = self.module.types.get(dst_ty).optional.child;
|
|
const coerced = self.coerceMode(val, src_ty, child_ty, mode);
|
|
return self.builder.emit(.{ .optional_wrap = .{ .operand = coerced } }, dst_ty);
|
|
},
|
|
// Concrete → Protocol (auto type erasure)
|
|
.erase_protocol => {
|
|
const proto_name = self.module.types.getString(self.module.types.get(dst_ty).@"struct".name);
|
|
const ctn = self.resolveConcreteTypeName(src_ty).?;
|
|
// If src is a pointer, use directly; otherwise alloca+store + heap-copy
|
|
var concrete_ptr = val;
|
|
var concrete_ty = src_ty;
|
|
var heap_copy = false;
|
|
if (!src_ty.isBuiltin()) {
|
|
const si = self.module.types.get(src_ty);
|
|
if (si == .pointer) {
|
|
concrete_ty = si.pointer.pointee;
|
|
heap_copy = false;
|
|
} else {
|
|
const slot = self.builder.alloca(src_ty);
|
|
self.builder.store(slot, val);
|
|
concrete_ptr = slot;
|
|
heap_copy = true;
|
|
}
|
|
}
|
|
return self.buildProtocolValue(concrete_ptr, proto_name, ctn, dst_ty, concrete_ty, heap_copy);
|
|
},
|
|
.int_to_float => return self.builder.emit(.{ .int_to_float = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty),
|
|
.float_to_int => {
|
|
// Implicit float→int narrowing follows the unified rule (the
|
|
// same `floatToIntExact` the array-dim / `$K: Count` paths use):
|
|
// a compile-time INTEGRAL float folds to its int, a NON-integral
|
|
// one is a compile error. Explicit `xx` / `cast` (mode
|
|
// `.explicit`) skips this and truncates. A runtime float has no
|
|
// compile-time value to fold — it truncates as before.
|
|
if (mode == .implicit) {
|
|
if (self.builder.constFloatInfo(val)) |info| {
|
|
if (program_index_mod.floatToIntExact(info.value)) |iv| {
|
|
return self.builder.constInt(iv, dst_ty);
|
|
}
|
|
// Non-integral: diagnose, then fall through to the
|
|
// truncating op below so lowering finishes and
|
|
// `hasErrors()` aborts the build.
|
|
self.diagNonIntegralNarrow(.{ .start = info.span.start, .end = info.span.end }, info.value, dst_ty);
|
|
}
|
|
}
|
|
return self.builder.emit(.{ .float_to_int = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty);
|
|
},
|
|
// Ptr ↔ Int — explicit `xx ptr` to/from an integer-typed slot.
|
|
// Emits a `bitcast` IR op; emit_llvm.zig's bitcast arm dispatches
|
|
// to LLVMBuildPtrToInt / LLVMBuildIntToPtr at the LLVM level
|
|
// since LLVMBuildBitCast itself doesn't accept ptr↔int.
|
|
.ptr_int_bitcast => return self.builder.emit(.{ .bitcast = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty),
|
|
.narrow => return self.builder.emit(.{ .narrow = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty),
|
|
.widen => return self.builder.emit(.{ .widen = .{ .operand = val, .from = src_ty, .to = dst_ty } }, dst_ty),
|
|
.array_to_slice => return self.builder.emit(.{ .array_to_slice = .{ .operand = val } }, dst_ty),
|
|
}
|
|
}
|
|
|
|
/// Get the alloca Ref for an expression, if it's a simple variable reference.
|
|
/// Returns null for complex expressions (field access, function calls, etc.)
|
|
fn getExprAlloca(self: *Lowering, node: *const Node) ?Ref {
|
|
const name = switch (node.data) {
|
|
.identifier => |id| id.name,
|
|
.type_expr => |te| te.name,
|
|
else => return null,
|
|
};
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(name)) |binding| {
|
|
if (binding.is_alloca) return binding.ref;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Get the element type for a slice/array/string type. A non-collection
|
|
/// type has no element type — return `.unresolved` (asking for it is a bug)
|
|
/// rather than a plausible `.s64`.
|
|
pub fn getElementType(self: *Lowering, ty: TypeId) TypeId {
|
|
if (ty == .string) return .u8;
|
|
if (ty.isBuiltin()) return .unresolved;
|
|
const info = self.module.types.get(ty);
|
|
return switch (info) {
|
|
.slice => |s| s.element,
|
|
.array => |a| a.element,
|
|
.vector => |v| v.element,
|
|
.many_pointer => |p| p.element,
|
|
else => .unresolved,
|
|
};
|
|
}
|
|
|
|
pub fn isFloat(ty: TypeId) bool {
|
|
return ty == .f32 or ty == .f64;
|
|
}
|
|
|
|
/// Result type of an arithmetic / bitwise / shift binary op over two
|
|
/// scalar operand types. This is the single promotion rule shared by the
|
|
/// value path (`lowerBinaryOp`) and AST-level inference
|
|
/// (`ExprTyper.inferType`'s binary-op arm), so static typing reports
|
|
/// exactly the type the lowered value carries. An integer LHS with a
|
|
/// floating-point RHS promotes to the float (`s64 + f64` → `f64`); every
|
|
/// other pairing — including vectors / structs, whose `isInt` is false —
|
|
/// takes the LHS type. Comparison / logical ops never reach here (they
|
|
/// are `.bool` at both sites).
|
|
pub fn arithResultType(lhs_ty: TypeId, rhs_ty: TypeId) TypeId {
|
|
if (isInt(lhs_ty) and isFloat(rhs_ty)) return rhs_ty;
|
|
return lhs_ty;
|
|
}
|
|
|
|
fn isInt(ty: TypeId) bool {
|
|
return switch (ty) {
|
|
.s8, .s16, .s32, .s64, .u8, .u16, .u32, .u64, .usize, .isize => true,
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
pub fn isIntEx(self: *Lowering, ty: TypeId) bool {
|
|
if (isInt(ty)) return true;
|
|
if (!ty.isBuiltin()) {
|
|
const info = self.module.types.get(ty);
|
|
return switch (info) {
|
|
.signed, .unsigned => true,
|
|
else => false,
|
|
};
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Operands valid for a scalar numeric op (`+ - * / %`): ints (incl.
|
|
/// custom widths), floats, SIMD vectors, and pointers (pointer
|
|
/// arithmetic). `.unresolved` returns true so a type we couldn't infer
|
|
/// is never diagnosed — the check only fires on a concretely
|
|
/// incompatible operand (e.g. `string`, a struct, an enum).
|
|
fn isArithOperand(self: *Lowering, ty: TypeId) bool {
|
|
if (ty == .unresolved) return true;
|
|
if (isInt(ty) or isFloat(ty)) return true;
|
|
if (ty.isBuiltin()) return false;
|
|
return switch (self.module.types.get(ty)) {
|
|
.signed, .unsigned, .vector, .pointer, .many_pointer => true,
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
/// Operands valid for ordering comparisons (`< <= > >=`): numbers
|
|
/// (incl. custom int widths), enums (ordinal), pointers (address
|
|
/// order), bool, and SIMD vectors. NOT strings (no lexicographic `<`
|
|
/// lowering exists) or any other aggregate. `.unresolved` passes so an
|
|
/// un-inferable operand is never falsely diagnosed.
|
|
fn isOrderingOperand(self: *Lowering, ty: TypeId) bool {
|
|
if (ty == .unresolved) return true;
|
|
if (isInt(ty) or isFloat(ty) or ty == .bool) return true;
|
|
if (ty.isBuiltin()) return false;
|
|
return switch (self.module.types.get(ty)) {
|
|
.signed, .unsigned, .@"enum", .pointer, .many_pointer, .vector => true,
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
/// Operands valid for bitwise/shift ops (`& | ^ << >>`): integers
|
|
/// (incl. custom widths), enums (flags are int-backed), bool, and SIMD
|
|
/// vectors. NOT floats, strings, pointers, or aggregates. `.unresolved`
|
|
/// passes (see `isOrderingOperand`).
|
|
fn isBitwiseOperand(self: *Lowering, ty: TypeId) bool {
|
|
if (ty == .unresolved) return true;
|
|
if (isInt(ty) or ty == .bool) return true;
|
|
if (ty.isBuiltin()) return false;
|
|
return switch (self.module.types.get(ty)) {
|
|
.signed, .unsigned, .@"enum", .vector => true,
|
|
else => false,
|
|
};
|
|
}
|
|
|
|
/// The named error-set TypeId of `node`'s type, or null if not an
|
|
/// error-set-typed expression.
|
|
fn errorSetTypeOf(self: *Lowering, node: *const Node) ?TypeId {
|
|
const t = self.inferExprType(node);
|
|
if (t.isBuiltin()) return null;
|
|
return if (self.module.types.get(t) == .error_set) t else null;
|
|
}
|
|
|
|
/// True when `node` is an `error.X` tag literal (`field_access` whose
|
|
/// object is the `error` keyword, parsed as identifier "error").
|
|
pub fn isErrorTagLiteralNode(node: *const Node) bool {
|
|
if (node.data != .field_access) return false;
|
|
const obj = node.data.field_access.object;
|
|
return obj.data == .identifier and std.mem.eql(u8, obj.data.identifier.name, "error");
|
|
}
|
|
|
|
/// Lower `==` / `!=` when an error-set value or `error.X` tag is involved.
|
|
/// Returns null when neither operand is error-related (general path runs).
|
|
/// Both operands must be a tag (an `error.X` literal or an error-set value);
|
|
/// otherwise it's a type error (e.g. comparing a tag to a raw integer).
|
|
fn tryLowerErrorSetEquality(self: *Lowering, bop: *const ast.BinaryOp) ?Ref {
|
|
const l_set = self.errorSetTypeOf(bop.lhs);
|
|
const r_set = self.errorSetTypeOf(bop.rhs);
|
|
const l_tag = isErrorTagLiteralNode(bop.lhs);
|
|
const r_tag = isErrorTagLiteralNode(bop.rhs);
|
|
if (l_set == null and r_set == null and !l_tag and !r_tag) return null;
|
|
|
|
const l_ok = l_set != null or l_tag;
|
|
const r_ok = r_set != null or r_tag;
|
|
if (!l_ok or !r_ok) {
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, bop.lhs.span, "an error-set value compares only with an `error.X` tag or another error-set value; coerce with `xx` to compare the raw id", .{});
|
|
}
|
|
return self.builder.constBool(false);
|
|
}
|
|
|
|
// Lower both sides with the set type as context so an `error.X` literal
|
|
// resolves to it (and validates membership). Two bare tag literals with
|
|
// no set context lower to global u32 ids (cross-set comparison is OK).
|
|
const set_ty = l_set orelse r_set;
|
|
const saved = self.target_type;
|
|
if (set_ty) |st| self.target_type = st;
|
|
const lv = self.lowerExpr(bop.lhs);
|
|
const rv = self.lowerExpr(bop.rhs);
|
|
self.target_type = saved;
|
|
return if (bop.op == .eq)
|
|
self.builder.cmpEq(lv, rv)
|
|
else
|
|
self.builder.emit(.{ .cmp_ne = .{ .lhs = lv, .rhs = rv } }, .bool);
|
|
}
|
|
|
|
/// The declared return type of the function currently being lowered (the
|
|
/// inlined body's type wins while inlining a comptime call), or null when
|
|
/// there is no enclosing function.
|
|
fn effectiveReturnType(self: *Lowering) ?TypeId {
|
|
if (self.inline_return_target) |iri| return iri.ret_ty;
|
|
if (self.builder.func) |fid| return self.module.functions.items[@intFromEnum(fid)].ret;
|
|
return null;
|
|
}
|
|
|
|
/// If `ret_ty` belongs to a failable function, the TypeId of its error
|
|
/// channel; else null. `-> !Named` / `-> !` resolve the error set directly;
|
|
/// `-> (T..., !)` carries it as the last tuple field (the locked ABI).
|
|
pub fn errorChannelOf(self: *Lowering, ret_ty: TypeId) ?TypeId {
|
|
if (ret_ty.isBuiltin()) return null;
|
|
switch (self.module.types.get(ret_ty)) {
|
|
.error_set => return ret_ty,
|
|
.tuple => |t| {
|
|
if (t.fields.len == 0) return null;
|
|
const last = t.fields[t.fields.len - 1];
|
|
if (last.isBuiltin()) return null;
|
|
return if (self.module.types.get(last) == .error_set) last else null;
|
|
},
|
|
else => return null,
|
|
}
|
|
}
|
|
|
|
/// True for the bare-`!` inferred placeholder error set (reserved name "!").
|
|
fn isInferredErrorSet(self: *Lowering, set: TypeId) bool {
|
|
if (set.isBuiltin()) return false;
|
|
const info = self.module.types.get(set);
|
|
if (info != .error_set) return false;
|
|
return std.mem.eql(u8, self.module.types.getString(info.error_set.name), "!");
|
|
}
|
|
|
|
/// Diagnose every tag of `src` that is not also a member of `dst` (the
|
|
/// enclosing function's named error set). Both must be `.error_set` types.
|
|
fn checkErrorSetSubset(self: *Lowering, src: TypeId, dst: TypeId, span: ast.Span) void {
|
|
if (src.isBuiltin()) return;
|
|
const src_info = self.module.types.get(src);
|
|
if (src_info != .error_set) return;
|
|
self.diagTagsNotInSet(src_info.error_set.tags, dst, span);
|
|
}
|
|
|
|
/// Diagnose every tag id in `src_tags` that is not a member of the named
|
|
/// error set `dst`. Shared by the named-set subset check and E1.4b's
|
|
/// inferred-callee widening (where the callee's tags come from the SCC,
|
|
/// not a `.error_set` TypeId).
|
|
fn diagTagsNotInSet(self: *Lowering, src_tags: []const u32, dst: TypeId, span: ast.Span) void {
|
|
if (dst.isBuiltin()) return;
|
|
const dst_info = self.module.types.get(dst);
|
|
if (dst_info != .error_set) return;
|
|
for (src_tags) |tag| {
|
|
var found = false;
|
|
for (dst_info.error_set.tags) |d| {
|
|
if (d == tag) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, span, "error tag 'error.{s}' is not in caller's error set '{s}'", .{ self.module.types.getTagName(tag), self.module.types.getString(dst_info.error_set.name) });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `raise EXPR;` — terminate the enclosing failable function via the error
|
|
/// channel. E1.3 lowers the **pure-failable** shape (`-> !` / `-> !Named`,
|
|
/// whose return type IS the error set): emit `ret(EXPR)`. The value-carrying
|
|
/// shape (`-> (T..., !)`) needs the value slots set to `undef` alongside the
|
|
/// error slot — that tuple ABI lands in E2.1/E2.2, so we bail loudly here
|
|
/// rather than ship a half-built return that silently corrupts value slots.
|
|
fn lowerRaise(self: *Lowering, rs: *const ast.RaiseStmt, span: ast.Span) void {
|
|
// (1) `raise` is legal only inside a failable function.
|
|
const ret_ty = self.effectiveReturnType() orelse {
|
|
self.diagRaiseNotFailable(span);
|
|
return;
|
|
};
|
|
const err_set = self.errorChannelOf(ret_ty) orelse {
|
|
self.diagRaiseNotFailable(span);
|
|
return;
|
|
};
|
|
const inferred = self.isInferredErrorSet(err_set);
|
|
|
|
// (2) Set check. Lowering EXPR with the function's error set as the
|
|
// target type makes a literal `raise error.X` validate `X ∈ set`
|
|
// inside lowerErrorTagLiteral (the inferred placeholder accepts any
|
|
// tag). The variable form `raise e` is subset-checked below.
|
|
const saved_target = self.target_type;
|
|
self.target_type = err_set;
|
|
const tag_ref = self.lowerExpr(rs.tag);
|
|
self.target_type = saved_target;
|
|
|
|
if (!inferred and !isErrorTagLiteralNode(rs.tag)) {
|
|
if (self.errorSetTypeOf(rs.tag)) |src_set| {
|
|
self.checkErrorSetSubset(src_set, err_set, span);
|
|
}
|
|
}
|
|
|
|
// (3) Push a trace frame: `raise` always escapes the function (ERR E3.2).
|
|
// Before cleanup, so the frame records the raise site itself.
|
|
self.emitTracePush(self.placeholderTraceFrame());
|
|
|
|
// (4) Emit the failure return. Pure-failable: the return type IS the
|
|
// error set, so return the tag value directly.
|
|
if (ret_ty == err_set) {
|
|
const tag_ty = self.builder.getRefType(tag_ref);
|
|
const coerced = if (tag_ty != err_set) self.coerceToType(tag_ref, tag_ty, err_set) else tag_ref;
|
|
self.emitErrorCleanup(self.func_defer_base, coerced);
|
|
if (self.inline_return_target) |iri| {
|
|
self.builder.store(iri.slot, coerced);
|
|
self.builder.br(iri.done_bb, &.{});
|
|
} else {
|
|
self.builder.ret(coerced, err_set);
|
|
}
|
|
} else {
|
|
// Value-carrying `-> (T..., !)`: the error path leaves the value
|
|
// slots undefined and carries the tag in the error slot (ERR E2.1).
|
|
const tag_ty = self.builder.getRefType(tag_ref);
|
|
const coerced_tag = if (tag_ty != err_set) self.coerceToType(tag_ref, tag_ty, err_set) else tag_ref;
|
|
self.emitErrorCleanup(self.func_defer_base, coerced_tag);
|
|
const fields = self.module.types.get(ret_ty).tuple.fields;
|
|
var slots = std.ArrayList(Ref).empty;
|
|
defer slots.deinit(self.alloc);
|
|
for (fields[0 .. fields.len - 1]) |vty| {
|
|
slots.append(self.alloc, self.builder.constUndef(vty)) catch unreachable;
|
|
}
|
|
const tup = self.buildFailableTuple(ret_ty, slots.items, coerced_tag);
|
|
self.emitTupleRet(ret_ty, tup);
|
|
}
|
|
}
|
|
|
|
/// Return a value-carrying failable function's success tuple
|
|
/// `{value(s)..., 0}` from `ref` (the user-returned value part). Forwarding
|
|
/// a full failable tuple (`return other_failable()` / explicit `return
|
|
/// (v, e)`) returns it as-is. Single-value `-> (T, !)` takes `ref` as the
|
|
/// lone value; multi-value `-> (T1, ..., !)` takes `ref` as a value-tuple
|
|
/// `(T1, ...)` and re-assembles its slots alongside the success error slot.
|
|
fn lowerFailableSuccessReturn(self: *Lowering, ref: Ref, ret_ty: TypeId, span: ast.Span) void {
|
|
const fields = self.module.types.get(ret_ty).tuple.fields;
|
|
const err_ty = fields[fields.len - 1];
|
|
const val_ty = self.builder.getRefType(ref);
|
|
if (val_ty == ret_ty) {
|
|
// The expression already IS the full failable tuple (forwarding).
|
|
self.emitTupleRet(ret_ty, ref);
|
|
return;
|
|
}
|
|
const n_vals = fields.len - 1;
|
|
if (n_vals == 1) {
|
|
const cv = self.coerceToType(ref, val_ty, fields[0]);
|
|
const tup = self.buildFailableTuple(ret_ty, &.{cv}, self.builder.constInt(0, err_ty));
|
|
self.emitTupleRet(ret_ty, tup);
|
|
return;
|
|
}
|
|
// Multi-value: `ref` must be a value-tuple `(T1, ..., Tn)`. Extract
|
|
// each value slot, coerce to the declared field type, and re-assemble
|
|
// with the success error slot (0).
|
|
if (val_ty.isBuiltin() or self.module.types.get(val_ty) != .tuple or self.module.types.get(val_ty).tuple.fields.len != n_vals) {
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, span, "a multi-value failable function (`-> (T1, ..., !)`) must `return` a {d}-tuple of its value types", .{n_vals});
|
|
}
|
|
return;
|
|
}
|
|
const vfields = self.module.types.get(val_ty).tuple.fields;
|
|
var vals = std.ArrayList(Ref).empty;
|
|
defer vals.deinit(self.alloc);
|
|
for (0..n_vals) |i| {
|
|
const fv = self.builder.emit(.{ .tuple_get = .{ .base = ref, .field_index = @intCast(i), .base_type = val_ty } }, vfields[i]);
|
|
vals.append(self.alloc, self.coerceToType(fv, vfields[i], fields[i])) catch unreachable;
|
|
}
|
|
const tup = self.buildFailableTuple(ret_ty, vals.items, self.builder.constInt(0, err_ty));
|
|
self.emitTupleRet(ret_ty, tup);
|
|
}
|
|
|
|
/// Build a failable return tuple `{value_refs..., tag}` typed `ret_ty`.
|
|
fn buildFailableTuple(self: *Lowering, ret_ty: TypeId, value_refs: []const Ref, tag: Ref) Ref {
|
|
var fields = std.ArrayList(Ref).empty;
|
|
defer fields.deinit(self.alloc);
|
|
fields.appendSlice(self.alloc, value_refs) catch unreachable;
|
|
fields.append(self.alloc, tag) catch unreachable;
|
|
return self.builder.emit(.{ .tuple_init = .{ .fields = self.alloc.dupe(Ref, fields.items) catch unreachable } }, ret_ty);
|
|
}
|
|
|
|
/// The success (value-part) type of a value-carrying failable tuple
|
|
/// `op_ty` (`-> (T..., !)`): the lone value type for a single-value
|
|
/// failable, or a synthesized value-tuple `(T1, ..., Tn)` (error slot
|
|
/// dropped) for a multi-value one. Callers must pass a value-carrying
|
|
/// tuple — a pure `-> !`'s success type is `void`, handled separately.
|
|
pub fn failableSuccessType(self: *Lowering, op_ty: TypeId) TypeId {
|
|
const fields = self.module.types.get(op_ty).tuple.fields;
|
|
const n_vals = fields.len - 1;
|
|
if (n_vals == 1) return fields[0];
|
|
return self.module.types.intern(.{ .tuple = .{
|
|
.fields = self.alloc.dupe(TypeId, fields[0..n_vals]) catch unreachable,
|
|
.names = null,
|
|
} });
|
|
}
|
|
|
|
/// The `target_type` to lower a returned expression against. For a
|
|
/// value-carrying failable (`-> (T..., !)`) a BARE returned value resolves
|
|
/// against the success value type (so a bare enum literal gets its real
|
|
/// ordinal); an EXPLICIT full failable tuple literal (`return (v..., e)`,
|
|
/// arity == full-tuple field count) keeps the failable-tuple target so its
|
|
/// trailing error element resolves against the error set and is forwarded
|
|
/// as-is. Every other return type passes through unchanged.
|
|
fn failableReturnTarget(self: *Lowering, ret_ty: TypeId, value_node: ?*const Node) TypeId {
|
|
if (ret_ty.isBuiltin()) return ret_ty;
|
|
if (self.module.types.get(ret_ty) != .tuple) return ret_ty;
|
|
if (self.errorChannelOf(ret_ty) == null) return ret_ty;
|
|
if (value_node) |vn| {
|
|
if (vn.data == .tuple_literal and
|
|
vn.data.tuple_literal.elements.len == self.module.types.get(ret_ty).tuple.fields.len)
|
|
return ret_ty;
|
|
}
|
|
return self.failableSuccessType(ret_ty);
|
|
}
|
|
|
|
/// Extract the success value from an evaluated value-carrying failable
|
|
/// tuple `result` (type `op_ty`): the lone value slot for single-value,
|
|
/// or an assembled value-tuple (typed `succ_ty`) for multi-value.
|
|
fn extractSuccessValue(self: *Lowering, result: Ref, op_ty: TypeId, succ_ty: TypeId) Ref {
|
|
const fields = self.module.types.get(op_ty).tuple.fields;
|
|
const n_vals = fields.len - 1;
|
|
if (n_vals == 1) {
|
|
return self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = 0, .base_type = op_ty } }, fields[0]);
|
|
}
|
|
var vals = std.ArrayList(Ref).empty;
|
|
defer vals.deinit(self.alloc);
|
|
for (0..n_vals) |i| {
|
|
vals.append(self.alloc, self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = @intCast(i), .base_type = op_ty } }, fields[i])) catch unreachable;
|
|
}
|
|
return self.builder.emit(.{ .tuple_init = .{ .fields = self.alloc.dupe(Ref, vals.items) catch unreachable } }, succ_ty);
|
|
}
|
|
|
|
/// Extract the error slot (always the last field) of an evaluated
|
|
/// value-carrying failable tuple `result`, typed as `err_set`.
|
|
fn extractErrorSlot(self: *Lowering, result: Ref, op_ty: TypeId, err_set: TypeId) Ref {
|
|
const fields = self.module.types.get(op_ty).tuple.fields;
|
|
return self.builder.emit(.{ .tuple_get = .{ .base = result, .field_index = @intCast(fields.len - 1), .base_type = op_ty } }, err_set);
|
|
}
|
|
|
|
/// Emit a return of an already-assembled tuple, honoring inline-comptime
|
|
/// return targets (store + branch) vs a real function return.
|
|
fn emitTupleRet(self: *Lowering, ret_ty: TypeId, tup: Ref) void {
|
|
if (self.inline_return_target) |iri| {
|
|
self.builder.store(iri.slot, tup);
|
|
self.builder.br(iri.done_bb, &.{});
|
|
} else {
|
|
self.builder.ret(tup, ret_ty);
|
|
}
|
|
}
|
|
|
|
fn diagRaiseNotFailable(self: *Lowering, span: ast.Span) void {
|
|
if (self.diagnostics) |diags| {
|
|
if (self.in_lambda_body) {
|
|
diags.addFmt(.err, span, "lambda body raises; declare its return type explicitly with `-> (T, !)` or `-> (T, !Named)`", .{});
|
|
} else {
|
|
diags.addFmt(.err, span, "`raise` is only valid inside a failable function (a return type with `!` or `!Named`)", .{});
|
|
}
|
|
}
|
|
}
|
|
|
|
/// True if `node`'s value is failable — a `try` (the result is its
|
|
/// operand's success value, but the expression itself routes an error) or
|
|
/// any expression whose type carries an error channel (a bare failable
|
|
/// call). Used to detect failable `or` chains (deferred to E1.4b).
|
|
pub fn exprIsFailable(self: *Lowering, node: *const Node) bool {
|
|
if (node.data == .try_expr) return true;
|
|
return self.errorChannelOf(self.inferExprType(node)) != null;
|
|
}
|
|
|
|
/// `try X` — a fallible attempt (ERR step E1.4a: the STANDALONE form, whose
|
|
/// failure target is function-propagation). Evaluates X; on failure, runs
|
|
/// the function's defers and returns the error to the caller; on success,
|
|
/// continues with X's value. E1.4a lowers the pure-failable shape (callee
|
|
/// `-> !` / `-> !Named`, caller likewise pure-failable). Value-carrying
|
|
/// callees, propagation from a value-carrying caller, and `try` inside an
|
|
/// `or` chain need the error-channel tuple ABI / fallback routing — those
|
|
/// land in E1.4b/E2, so we bail loudly here.
|
|
/// Synthesize a `Source_Location` value for a `#caller_location` marker
|
|
/// (ERR E4.1b). The node's `span`/`source_file` are the CALL site (rewritten
|
|
/// by `expandCallDefaults`); resolve them to file / line:col against the
|
|
/// source text and stamp the enclosing (caller) function name.
|
|
fn lowerCallerLocation(self: *Lowering, node: *const Node) Ref {
|
|
const sl_tid = self.module.types.findByName(self.module.types.internString("Source_Location")) orelse {
|
|
if (self.diagnostics) |d| d.addFmt(.err, node.span, "`#caller_location` needs `Source_Location` (from std.sx) in scope", .{});
|
|
return self.builder.constInt(0, .void);
|
|
};
|
|
const file = node.source_file orelse self.current_source_file orelse (self.main_file orelse "");
|
|
const src = self.sourceForFile(file);
|
|
const loc = errors.SourceLoc.compute(src, node.span.start);
|
|
const func_name = self.currentFunctionName();
|
|
var fields = [_]Ref{
|
|
self.builder.constString(self.module.types.internString(file)),
|
|
self.builder.constInt(@intCast(loc.line), .s32),
|
|
self.builder.constInt(@intCast(loc.col), .s32),
|
|
self.builder.constString(self.module.types.internString(func_name)),
|
|
};
|
|
return self.builder.emit(.{ .struct_init = .{ .fields = self.alloc.dupe(Ref, &fields) catch unreachable } }, sl_tid);
|
|
}
|
|
|
|
/// The source text for `file`, via the diagnostics' file→source map (which
|
|
/// includes the main file). Empty if unavailable — line:col then degrade to
|
|
/// 1:1 rather than crash.
|
|
fn sourceForFile(self: *Lowering, file: []const u8) []const u8 {
|
|
const diags = self.diagnostics orelse return "";
|
|
if (diags.import_sources) |is| {
|
|
if (is.get(file)) |s| return s;
|
|
}
|
|
return diags.source;
|
|
}
|
|
|
|
/// Name of the function currently being lowered (the caller, at a
|
|
/// `#caller_location` site), or "" outside any function.
|
|
fn currentFunctionName(self: *Lowering) []const u8 {
|
|
const fid = self.builder.func orelse return "";
|
|
return self.module.types.getString(self.module.functions.items[@intFromEnum(fid)].name);
|
|
}
|
|
|
|
fn lowerTry(self: *Lowering, operand: *const Node, span: ast.Span) Ref {
|
|
// (1) `try` is legal only inside a failable function.
|
|
const caller_ret = self.effectiveReturnType() orelse {
|
|
self.diagTryNotFailable(span);
|
|
return self.builder.constInt(0, .void);
|
|
};
|
|
const caller_set = self.errorChannelOf(caller_ret) orelse {
|
|
self.diagTryNotFailable(span);
|
|
return self.builder.constInt(0, .void);
|
|
};
|
|
|
|
// (2) The operand must be failable. This is the sole failable-operand
|
|
// check (the parser imposes none — see E0.2).
|
|
const op_ty = self.inferExprType(operand);
|
|
const callee_set = self.errorChannelOf(op_ty) orelse {
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, span, "`try` requires a failable expression; operand has type '{s}'", .{self.formatTypeName(op_ty)});
|
|
}
|
|
return self.builder.constInt(0, .void);
|
|
};
|
|
|
|
// A value-carrying callee (`-> (T..., !)`) returns a tuple
|
|
// `{v..., err}`; a pure-failable callee (`-> !`) returns the bare
|
|
// error tag.
|
|
const callee_value_carrying = op_ty != callee_set;
|
|
|
|
// (3) Widening: the callee's escape set must be ⊆ the caller's named
|
|
// set. For an inferred caller (`!`) the absorption happens in the
|
|
// whole-program SCC (E1.4b) — no check here.
|
|
self.checkEscapeWidening(operand, callee_set, caller_set, span);
|
|
|
|
// (4) Lower: evaluate the operand, then branch on its error tag (which
|
|
// is the bare result for a pure callee, or the last tuple slot for
|
|
// a value-carrying one).
|
|
const result = self.lowerExpr(operand);
|
|
const err_val = if (callee_value_carrying)
|
|
self.extractErrorSlot(result, op_ty, callee_set)
|
|
else
|
|
result;
|
|
const err_ty = self.builder.getRefType(err_val);
|
|
const is_err = self.builder.emit(.{ .cmp_ne = .{ .lhs = err_val, .rhs = self.builder.constInt(0, err_ty) } }, .bool);
|
|
|
|
const prop_bb = self.freshBlock("try.prop");
|
|
const ok_bb = self.freshBlock("try.ok");
|
|
self.builder.condBr(is_err, prop_bb, &.{}, ok_bb, &.{});
|
|
|
|
// Propagation: push a trace frame (this `try` failure escapes to the
|
|
// caller — ERR E3.2), run the function's cleanups (defers + onfails,
|
|
// since this is an error exit), then return the caller's failure
|
|
// carrying this tag (pure caller → `ret(tag)`; value-carrying →
|
|
// `ret {undef…, tag}`).
|
|
self.builder.switchToBlock(prop_bb);
|
|
self.emitTracePush(self.placeholderTraceFrame());
|
|
self.emitErrorCleanup(self.func_defer_base, err_val);
|
|
self.emitErrorReturn(caller_ret, caller_set, err_val);
|
|
|
|
// Success: a value-carrying callee yields its value part (the lone
|
|
// value, or a value-tuple); a pure-failable callee has no value (void).
|
|
self.builder.switchToBlock(ok_bb);
|
|
if (callee_value_carrying) {
|
|
const succ_ty = self.failableSuccessType(op_ty);
|
|
return self.extractSuccessValue(result, op_ty, succ_ty);
|
|
}
|
|
return self.builder.constInt(0, .void);
|
|
}
|
|
|
|
/// Return the enclosing function's failure carrying error tag `err`. A
|
|
/// pure-failable caller (`-> !`) returns the tag directly; a value-carrying
|
|
/// caller (`-> (T..., !)`) returns `{undef value slots..., tag}`. Honors
|
|
/// inline-comptime return targets. The caller emits defers first.
|
|
fn emitErrorReturn(self: *Lowering, caller_ret: TypeId, caller_set: TypeId, err: Ref) void {
|
|
const ety = self.builder.getRefType(err);
|
|
const coerced = if (ety != caller_set) self.coerceToType(err, ety, caller_set) else err;
|
|
if (caller_ret == caller_set) {
|
|
if (self.inline_return_target) |iri| {
|
|
self.builder.store(iri.slot, coerced);
|
|
self.builder.br(iri.done_bb, &.{});
|
|
} else {
|
|
self.builder.ret(coerced, caller_set);
|
|
}
|
|
} else {
|
|
const fields = self.module.types.get(caller_ret).tuple.fields;
|
|
var undefs = std.ArrayList(Ref).empty;
|
|
defer undefs.deinit(self.alloc);
|
|
for (fields[0 .. fields.len - 1]) |vty| {
|
|
undefs.append(self.alloc, self.builder.constUndef(vty)) catch unreachable;
|
|
}
|
|
const tup = self.buildFailableTuple(caller_ret, undefs.items, coerced);
|
|
self.emitTupleRet(caller_ret, tup);
|
|
}
|
|
}
|
|
|
|
fn diagTryNotFailable(self: *Lowering, span: ast.Span) void {
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, span, "`try` is only valid inside a failable function (a return type with `!` or `!Named`)", .{});
|
|
}
|
|
}
|
|
|
|
/// `expr catch [e] BODY` — inline failure handler (ERR step E1.5,
|
|
/// pure-failable slice). Evaluates `expr`; on failure, binds the tag to
|
|
/// `e` (if present) and runs BODY; on success, the value is `void` (a
|
|
/// pure-failable LHS has no success value). BODY either diverges (via
|
|
/// `noreturn` — E1.4c) or falls through. `catch` consumes the error
|
|
/// locally, so — unlike `try` / `raise` — it needs no failable *enclosing*
|
|
/// function. Value-carrying LHS (binding the success value / a
|
|
/// value-producing body unifying with the success tuple) needs the
|
|
/// error-channel tuple ABI and lands in E2 — bail loudly here.
|
|
fn lowerCatch(self: *Lowering, ce: *const ast.CatchExpr, span: ast.Span) Ref {
|
|
// A failable `or` chain operand (`(try a or try b) catch e …`) routes
|
|
// its total failure to the catch handler — not the function — via the
|
|
// chain-fail target (ERR E2.4). A chain's value type is non-failable
|
|
// `T`, so it wouldn't pass the `errorChannelOf` check below.
|
|
if (ce.operand.data == .binary_op and ce.operand.data.binary_op.op == .or_op and
|
|
self.orIsFailableChain(&ce.operand.data.binary_op))
|
|
{
|
|
return self.lowerCatchOverChain(ce, span);
|
|
}
|
|
|
|
const op_ty = self.inferExprType(ce.operand);
|
|
const err_set = self.errorChannelOf(op_ty) orelse {
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, span, "`catch` requires a failable expression; operand has type '{s}'", .{self.formatTypeName(op_ty)});
|
|
}
|
|
return self.builder.constInt(0, .void);
|
|
};
|
|
// Pure-failable LHS (`-> !`): no success value. Run the body on the
|
|
// error path; both paths fall through to a value-less merge.
|
|
if (op_ty == err_set) {
|
|
const err_val = self.lowerExpr(ce.operand);
|
|
const err_ty = self.builder.getRefType(err_val);
|
|
const is_err = self.builder.emit(.{ .cmp_ne = .{ .lhs = err_val, .rhs = self.builder.constInt(0, err_ty) } }, .bool);
|
|
const handle_bb = self.freshBlock("catch.handle");
|
|
const merge_bb = self.freshBlock("catch.merge");
|
|
self.builder.condBr(is_err, handle_bb, &.{}, merge_bb, &.{});
|
|
self.builder.switchToBlock(handle_bb);
|
|
_ = self.runCatchBody(ce, err_val, err_set, null);
|
|
// The handler can inspect the trace (`trace.print_current()`); the
|
|
// absorption clear fires once it completes WITHOUT re-raising (a
|
|
// fall-through). A diverging body (`raise` / `return`) keeps /
|
|
// discards the buffer on its own path (ERR E3.2; reconciles
|
|
// PLAN-ERR §clear-points "cleared before body" with §catch-over-or
|
|
// "frames still in the buffer when the body runs").
|
|
if (!self.currentBlockHasTerminator()) {
|
|
self.emitTraceClear();
|
|
self.builder.br(merge_bb, &.{});
|
|
}
|
|
self.builder.switchToBlock(merge_bb);
|
|
return self.builder.constInt(0, .void);
|
|
}
|
|
|
|
// Value-carrying LHS (`-> (T..., !)`): on success the catch yields the
|
|
// value part (the lone value, or a value-tuple); on error it yields
|
|
// the handler body's value. The paths merge through a block-parameter
|
|
// (phi).
|
|
const succ_ty = self.failableSuccessType(op_ty);
|
|
const result = self.lowerExpr(ce.operand);
|
|
const err_val = self.extractErrorSlot(result, op_ty, err_set);
|
|
const succ_val = self.extractSuccessValue(result, op_ty, succ_ty);
|
|
const is_err = self.builder.emit(.{ .cmp_ne = .{ .lhs = err_val, .rhs = self.builder.constInt(0, err_set) } }, .bool);
|
|
|
|
const handle_bb = self.freshBlock("catch.handle");
|
|
const merge_bb = self.freshBlockWithParams("catch.merge", &.{succ_ty});
|
|
// Success → merge with the value slot; error → run the handler.
|
|
self.builder.condBr(is_err, handle_bb, &.{}, merge_bb, &.{succ_val});
|
|
|
|
self.builder.switchToBlock(handle_bb);
|
|
const body_val = self.runCatchBody(ce, err_val, err_set, succ_ty);
|
|
if (!self.currentBlockHasTerminator()) {
|
|
self.finishCatchHandler(body_val, succ_ty, merge_bb, span);
|
|
}
|
|
|
|
self.builder.switchToBlock(merge_bb);
|
|
return self.builder.blockParam(merge_bb, 0, succ_ty);
|
|
}
|
|
|
|
/// `(failable or-chain) catch [e] BODY` (ERR E2.4). The chain's operands
|
|
/// route per the chain rules; its TOTAL failure (the final operand failing)
|
|
/// is redirected to the catch handler via `chain_fail_target` rather than
|
|
/// propagating to the function. `e` binds the final error tag; the handler's
|
|
/// value (or divergence) joins the chain's success value at the merge.
|
|
fn lowerCatchOverChain(self: *Lowering, ce: *const ast.CatchExpr, span: ast.Span) Ref {
|
|
const chain = &ce.operand.data.binary_op;
|
|
|
|
// The error tag reaching the handler is the final operand's (left-assoc
|
|
// chain → the top-level rhs). A value-terminator last operand means the
|
|
// chain can't fail — nothing for `catch` to absorb.
|
|
const last = unwrapTryNode(chain.rhs);
|
|
const last_ty = self.inferExprType(last);
|
|
const err_set = self.errorChannelOf(last_ty) orelse {
|
|
if (self.diagnostics) |d| d.addFmt(.err, span, "`catch` here is redundant — the `or` chain already absorbs every failure via its value terminator", .{});
|
|
return self.builder.constInt(0, .void);
|
|
};
|
|
|
|
const succ_ty = self.orChainSuccessType(chain);
|
|
const has_value = succ_ty != .void;
|
|
|
|
const handle_bb = self.freshBlockWithParams("catch.handle", &.{err_set});
|
|
const merge_bb = if (has_value)
|
|
self.freshBlockWithParams("catch.merge", &.{succ_ty})
|
|
else
|
|
self.freshBlock("catch.merge");
|
|
|
|
// Lower the chain with its total failure routed to the handler.
|
|
const saved = self.chain_fail_target;
|
|
self.chain_fail_target = .{ .bb = handle_bb, .set = err_set };
|
|
const chain_val = self.lowerExpr(ce.operand);
|
|
self.chain_fail_target = saved;
|
|
// Chain success → merge with its value (the buffer was already cleared
|
|
// at the succeeding operand inside the chain).
|
|
if (has_value) {
|
|
const cv = self.coerceToType(chain_val, self.builder.getRefType(chain_val), succ_ty);
|
|
self.builder.br(merge_bb, &.{cv});
|
|
} else {
|
|
self.builder.br(merge_bb, &.{});
|
|
}
|
|
|
|
// Handler: bind the final tag, run the body. The buffer still holds the
|
|
// chain's frames (handler may inspect them); absorb on non-diverging exit.
|
|
self.builder.switchToBlock(handle_bb);
|
|
const tag = self.builder.blockParam(handle_bb, 0, err_set);
|
|
const body_val = self.runCatchBody(ce, tag, err_set, if (has_value) succ_ty else null);
|
|
if (!self.currentBlockHasTerminator()) {
|
|
self.finishCatchHandler(body_val, succ_ty, merge_bb, span);
|
|
}
|
|
|
|
self.builder.switchToBlock(merge_bb);
|
|
return if (has_value) self.builder.blockParam(merge_bb, 0, succ_ty) else self.builder.constInt(0, .void);
|
|
}
|
|
|
|
/// Close a non-terminated `catch` handler block. `succ_ty` is the catch's
|
|
/// result type (`.void` for a pure-failable / void-chain catch — the merge
|
|
/// block then has no parameter). A `body_val` typed `noreturn` (e.g. a
|
|
/// `process.exit` / other noreturn call, which is NOT an IR terminator)
|
|
/// diverges: close with `unreachable` and skip the merge edge so its
|
|
/// "value" never reaches a phi. Otherwise clear the absorbed trace and
|
|
/// branch to the merge (coercing the body value, or diagnosing a missing /
|
|
/// void value for a value-carrying catch).
|
|
fn finishCatchHandler(self: *Lowering, body_val: ?Ref, succ_ty: TypeId, merge_bb: BlockId, span: ast.Span) void {
|
|
if (body_val) |v| {
|
|
if (self.builder.getRefType(v) == .noreturn) {
|
|
self.builder.emitUnreachable();
|
|
return;
|
|
}
|
|
}
|
|
self.emitTraceClear();
|
|
if (succ_ty == .void) {
|
|
self.builder.br(merge_bb, &.{});
|
|
return;
|
|
}
|
|
const bv: Ref = blk: {
|
|
if (body_val) |v| {
|
|
const vty = self.builder.getRefType(v);
|
|
if (vty != .void) break :blk self.coerceToType(v, vty, succ_ty);
|
|
}
|
|
if (self.diagnostics) |diags| {
|
|
diags.addFmt(.err, span, "`catch` body must produce a value of type '{s}' (or diverge with `return` / `raise`)", .{self.formatTypeName(succ_ty)});
|
|
}
|
|
break :blk self.builder.constUndef(succ_ty);
|
|
};
|
|
self.builder.br(merge_bb, &.{bv});
|
|
}
|
|
|
|
/// Lower a `catch` body in a child scope that binds the error tag to the
|
|
/// catch binding (if any). When `want_ty` is non-null (value-carrying
|
|
/// catch), returns the body's value (or null if the body diverged); when
|
|
/// null (pure-failable catch), runs the body for effect and returns null.
|
|
fn runCatchBody(self: *Lowering, ce: *const ast.CatchExpr, err_val: Ref, err_set: TypeId, want_ty: ?TypeId) ?Ref {
|
|
var handle_scope = Scope.init(self.alloc, self.scope);
|
|
const saved_scope = self.scope;
|
|
self.scope = &handle_scope;
|
|
defer {
|
|
self.scope = saved_scope;
|
|
handle_scope.deinit();
|
|
}
|
|
if (ce.binding) |name| {
|
|
handle_scope.put(name, .{ .ref = err_val, .ty = err_set, .is_alloca = false });
|
|
}
|
|
if (want_ty == null) {
|
|
if (ce.body.data == .block) self.lowerBlock(ce.body) else _ = self.lowerExpr(ce.body);
|
|
return null;
|
|
}
|
|
const saved_fbv = self.force_block_value;
|
|
self.force_block_value = true;
|
|
defer self.force_block_value = saved_fbv;
|
|
return if (ce.body.data == .block) self.lowerBlockValue(ce.body) else self.lowerExpr(ce.body);
|
|
}
|
|
|
|
/// `lhs or rhs` with a failable LHS (ERR step E2.4a — the value-terminator
|
|
/// form). On LHS success the result is its value part (the lone value, or a
|
|
/// value-tuple); on failure the LHS error is discarded and the result is
|
|
/// `rhs` (a plain value of the success type), so the whole expression is
|
|
/// non-failable. The CHAIN form (`... or try ...` / a failable RHS) needs
|
|
/// the fallback-target routing deferred from E1.4 — bail.
|
|
/// Widening at an escape (function-propagation) site: the escaping set must
|
|
/// be ⊆ the caller's named set. An inferred caller (`!`) absorbs everything
|
|
/// via the whole-program SCC (E1.4b) — no check. A bare-`!` callee carries
|
|
/// no tags on its placeholder TypeId, so check its SCC-converged set.
|
|
/// Shared by `try` propagation and a failable `or` chain's final operand.
|
|
fn checkEscapeWidening(self: *Lowering, callee_node: *const Node, callee_set: TypeId, caller_set: TypeId, span: ast.Span) void {
|
|
if (self.isInferredErrorSet(caller_set)) return;
|
|
if (!self.isInferredErrorSet(callee_set)) {
|
|
self.checkErrorSetSubset(callee_set, caller_set, span);
|
|
return;
|
|
}
|
|
// Bare-`!` callee: either a named top-level function (its converged set
|
|
// is name-keyed) or a closure/fn-type SLOT (its set is shape-keyed,
|
|
// shared program-wide by value-signature).
|
|
if (callTargetName(callee_node)) |nm| {
|
|
if (self.inferred_error_sets.get(nm)) |tags| {
|
|
self.diagTagsNotInSet(tags, caller_set, span);
|
|
return;
|
|
}
|
|
}
|
|
if (self.shapeKeyOfCallee(callee_node)) |key| {
|
|
if (self.shape_inferred_sets.get(key)) |tags| {
|
|
self.diagTagsNotInSet(tags, caller_set, span);
|
|
}
|
|
// Empty union (no closure of this shape ever raises) → silently
|
|
// allowed: the slot's `!` resolves to ∅ (ERR E5.1 sub-feature 6).
|
|
}
|
|
}
|
|
|
|
/// Structural test: is this `or` a *failable* construct (value-terminator or
|
|
/// chain), rather than a boolean / optional-unwrap `or`? True when either
|
|
/// operand is failable-like — a `try`, an error-channel-typed expression, or
|
|
/// itself a nested failable `or` chain. Kept separate from `inferExprType`:
|
|
/// a `try`-chain's *value* type is its success type `T` (non-failable), so
|
|
/// the chain-ness is structural, not type-derived.
|
|
pub fn orIsFailableChain(self: *Lowering, bop: *const ast.BinaryOp) bool {
|
|
return self.operandIsFailableLike(bop.lhs) or self.operandIsFailableLike(bop.rhs);
|
|
}
|
|
|
|
fn operandIsFailableLike(self: *Lowering, node: *const Node) bool {
|
|
if (node.data == .try_expr) return true;
|
|
if (node.data == .binary_op and node.data.binary_op.op == .or_op) {
|
|
return self.orIsFailableChain(&node.data.binary_op);
|
|
}
|
|
return self.errorChannelOf(self.inferExprType(node)) != null;
|
|
}
|
|
|
|
/// The success (value) type of a failable `or` chain: descend to the
|
|
/// leftmost operand, unwrap any `try`, and take its failable success type
|
|
/// (`void` for a pure-`-> !` chain). All operands share this type.
|
|
pub fn orChainSuccessType(self: *Lowering, bop: *const ast.BinaryOp) TypeId {
|
|
var lhs = bop.lhs;
|
|
while (lhs.data == .binary_op and lhs.data.binary_op.op == .or_op and
|
|
self.orIsFailableChain(&lhs.data.binary_op))
|
|
{
|
|
lhs = lhs.data.binary_op.lhs;
|
|
}
|
|
const ft = self.inferExprType(unwrapTryNode(lhs));
|
|
const fset = self.errorChannelOf(ft) orelse return .unresolved;
|
|
return if (ft == fset) .void else self.failableSuccessType(ft);
|
|
}
|
|
|
|
/// `try X` → `X` (the underlying failable); any other node unchanged. In an
|
|
/// `or` chain the `try` marker's routing IS the chain, so the chain lowers
|
|
/// the underlying failable directly rather than re-entering `lowerTry`.
|
|
fn unwrapTryNode(node: *const Node) *const Node {
|
|
return if (node.data == .try_expr) node.data.try_expr.operand else node;
|
|
}
|
|
|
|
/// Flatten a left-associative failable `or` chain into its operands,
|
|
/// left-to-right. `a or b or c` parses as `(a or b) or c`; this collects
|
|
/// `[a, b, c]`. Walks the left spine only while it stays a failable
|
|
/// `or` chain (a parenthesized non-chain `or` on the left stops the walk).
|
|
fn flattenOrChain(self: *Lowering, bop: *const ast.BinaryOp, list: *std.ArrayList(*const Node)) void {
|
|
if (bop.lhs.data == .binary_op and bop.lhs.data.binary_op.op == .or_op and
|
|
self.orIsFailableChain(&bop.lhs.data.binary_op))
|
|
{
|
|
self.flattenOrChain(&bop.lhs.data.binary_op, list);
|
|
} else {
|
|
list.append(self.alloc, bop.lhs) catch unreachable;
|
|
}
|
|
list.append(self.alloc, bop.rhs) catch unreachable;
|
|
}
|
|
|
|
/// Lower a failable `or` (ERR E2.4): a value-terminator (`lhs or value`) or
|
|
/// a chain (`try a or try b or …`, possibly with a trailing value
|
|
/// terminator). Left-to-right, short-circuit: each failable operand's
|
|
/// failure routes to the next operand; the final operand either absorbs
|
|
/// (value terminator) or propagates to the enclosing function. Each failed
|
|
/// attempt pushes a trace frame; an absorbing resolution (any operand
|
|
/// succeeding, or the value terminator) clears the buffer; total failure
|
|
/// preserves the frames for the caller.
|
|
fn lowerFailableOr(self: *Lowering, bop: *const ast.BinaryOp) Ref {
|
|
const span = bop.lhs.span;
|
|
|
|
var operands = std.ArrayList(*const Node).empty;
|
|
defer operands.deinit(self.alloc);
|
|
self.flattenOrChain(bop, &operands);
|
|
const last_idx = operands.items.len - 1;
|
|
const last_is_value = !self.operandIsFailableLike(operands.items[last_idx]);
|
|
|
|
// The chain's total-failure routing. An absorbing consumer (`catch`)
|
|
// sets this so the final operand's failure reaches the handler; cleared
|
|
// while lowering operands so a nested operand doesn't inherit it.
|
|
const fail_target = self.chain_fail_target;
|
|
self.chain_fail_target = null;
|
|
defer self.chain_fail_target = fail_target;
|
|
|
|
// Success type from the first operand (a failable; unwrap any `try`).
|
|
const first_ty = self.inferExprType(unwrapTryNode(operands.items[0]));
|
|
const first_set = self.errorChannelOf(first_ty) orelse {
|
|
if (self.diagnostics) |d| d.addFmt(.err, span, "the left operand of a failable `or` must be failable; got '{s}'", .{self.formatTypeName(first_ty)});
|
|
return self.builder.constInt(0, .void);
|
|
};
|
|
const has_value = first_ty != first_set;
|
|
const succ_ty = if (has_value) self.failableSuccessType(first_ty) else TypeId.void;
|
|
|
|
// Pure-failable LHS (`-> !`) with a value terminator: nothing to fall
|
|
// back to.
|
|
if (!has_value and last_is_value) {
|
|
if (self.diagnostics) |d| d.addFmt(.err, span, "`or value` requires a value-carrying failable (`-> (T, !)`) — a `-> !` has no success value to fall back to; use `catch` to absorb the error", .{});
|
|
return self.builder.constInt(0, .void);
|
|
}
|
|
|
|
// Caller failability — only needed when the chain can propagate to the
|
|
// function (final operand is failable AND no absorbing consumer target).
|
|
var caller_ret: TypeId = .void;
|
|
var caller_set: TypeId = .void;
|
|
if (!last_is_value and fail_target == null) {
|
|
const cret = self.effectiveReturnType();
|
|
const cset = if (cret) |r| self.errorChannelOf(r) else null;
|
|
if (cset == null) {
|
|
if (self.diagnostics) |d| d.addFmt(.err, span, "a failable `or` chain propagates on total failure, so it is only valid inside a failable function — add a value terminator (`… or value`) or wrap with `catch`", .{});
|
|
return self.builder.constInt(0, .void);
|
|
}
|
|
caller_ret = cret.?;
|
|
caller_set = cset.?;
|
|
}
|
|
|
|
const merge_bb = if (has_value)
|
|
self.freshBlockWithParams("orc.merge", &.{succ_ty})
|
|
else
|
|
self.freshBlock("orc.merge");
|
|
|
|
for (operands.items, 0..) |operand, i| {
|
|
const is_last = i == last_idx;
|
|
|
|
if (is_last and last_is_value) {
|
|
// Value terminator: absorbs every prior failure.
|
|
self.emitTraceClear();
|
|
const saved = self.target_type;
|
|
self.target_type = succ_ty;
|
|
const v = self.lowerExpr(operand);
|
|
self.target_type = saved;
|
|
const vc = self.coerceToType(v, self.builder.getRefType(v), succ_ty);
|
|
self.builder.br(merge_bb, &.{vc});
|
|
break;
|
|
}
|
|
|
|
// Failable operand (`try X` marker or a bare failable). Lower the
|
|
// underlying failable; the `try` marker's routing IS the chain.
|
|
const underlying = unwrapTryNode(operand);
|
|
const op_ty = self.inferExprType(underlying);
|
|
const op_set = self.errorChannelOf(op_ty) orelse {
|
|
if (self.diagnostics) |d| d.addFmt(.err, operand.span, "operand of a failable `or` chain must be failable; got '{s}'", .{self.formatTypeName(op_ty)});
|
|
return self.builder.constInt(0, .void);
|
|
};
|
|
const op_value_carrying = op_ty != op_set;
|
|
|
|
// Widening applies only when the final failure escapes to the
|
|
// function (no absorbing consumer); a `catch` target absorbs it.
|
|
if (is_last and fail_target == null) self.checkEscapeWidening(underlying, op_set, caller_set, operand.span);
|
|
|
|
const result = self.lowerExpr(underlying);
|
|
const err_val = if (op_value_carrying) self.extractErrorSlot(result, op_ty, op_set) else result;
|
|
const err_ty = self.builder.getRefType(err_val);
|
|
const is_err = self.builder.emit(.{ .cmp_ne = .{ .lhs = err_val, .rhs = self.builder.constInt(0, err_ty) } }, .bool);
|
|
|
|
const ok_bb = self.freshBlock("orc.ok");
|
|
const fail_bb = self.freshBlock(if (is_last) "orc.prop" else "orc.next");
|
|
self.builder.condBr(is_err, fail_bb, &.{}, ok_bb, &.{});
|
|
|
|
// Success: the chain resolved here — clear the buffer, merge value.
|
|
self.builder.switchToBlock(ok_bb);
|
|
self.emitTraceClear();
|
|
if (has_value) {
|
|
const sv = self.extractSuccessValue(result, op_ty, succ_ty);
|
|
const svc = self.coerceToType(sv, self.builder.getRefType(sv), succ_ty);
|
|
self.builder.br(merge_bb, &.{svc});
|
|
} else {
|
|
self.builder.br(merge_bb, &.{});
|
|
}
|
|
|
|
// Failure: push a trace frame, then either route to the next
|
|
// operand (same block — no function exit, so `onfail` does not
|
|
// fire) or, for the final operand, resolve the total failure: to an
|
|
// absorbing consumer (`catch`) if one set a target, else propagate
|
|
// to the caller.
|
|
self.builder.switchToBlock(fail_bb);
|
|
self.emitTracePush(self.placeholderTraceFrame());
|
|
if (is_last) {
|
|
if (fail_target) |t| {
|
|
const ec = self.coerceToType(err_val, self.builder.getRefType(err_val), t.set);
|
|
self.builder.br(t.bb, &.{ec});
|
|
} else {
|
|
self.emitErrorCleanup(self.func_defer_base, err_val);
|
|
self.emitErrorReturn(caller_ret, caller_set, err_val);
|
|
}
|
|
}
|
|
// else: fall through — the next operand is lowered in fail_bb.
|
|
}
|
|
|
|
self.builder.switchToBlock(merge_bb);
|
|
return if (has_value) self.builder.blockParam(merge_bb, 0, succ_ty) else self.builder.constInt(0, .void);
|
|
}
|
|
|
|
// ── ERR E1.4b: whole-program inferred-error-set convergence ──────────
|
|
|
|
/// The bare callee name of a call expression (`g(...)` → "g"), or null if
|
|
/// the node isn't a direct call to a named function. E1.4b resolves only
|
|
/// the bare identifier (top-level functions); UFCS / mangled-local callees
|
|
/// aren't tracked by the SCC.
|
|
pub fn callTargetName(node: *const Node) ?[]const u8 {
|
|
if (node.data != .call) return null;
|
|
const callee = node.data.call.callee;
|
|
return if (callee.data == .identifier) callee.data.identifier.name else null;
|
|
}
|
|
|
|
/// True when `rt` is a pure bare-`!` failable return (`-> !`, the inferred
|
|
/// set) — NOT `!Named` and NOT a value-carrying `-> (T..., !)` tuple.
|
|
pub fn astIsPureBareInferred(rt: ?*const Node) bool {
|
|
const n = rt orelse return false;
|
|
return n.data == .error_type_expr and n.data.error_type_expr.name == null;
|
|
}
|
|
|
|
/// The named-set name of a pure `-> !Named` return (`"Named"`), or null for
|
|
/// bare-`!`, value-carrying, or non-failable returns.
|
|
pub fn astPureNamedSet(rt: ?*const Node) ?[]const u8 {
|
|
const n = rt orelse return null;
|
|
if (n.data != .error_type_expr) return null;
|
|
return n.data.error_type_expr.name;
|
|
}
|
|
|
|
/// The declared tags of a named error set, by name; null if not a
|
|
/// registered error set.
|
|
pub fn namedSetTags(self: *Lowering, name: []const u8) ?[]const u32 {
|
|
const sid = self.module.types.internString(name);
|
|
const tid = self.module.types.findByName(sid) orelse return null;
|
|
if (tid.isBuiltin()) return null;
|
|
const info = self.module.types.get(tid);
|
|
return if (info == .error_set) info.error_set.tags else null;
|
|
}
|
|
|
|
/// Whole-program inferred-error-set convergence. Thin delegation to the
|
|
/// canonical owner (`ErrorAnalysis`, `error_analysis.zig`); kept on
|
|
/// `Lowering` as a `pub` entry point because the lowering pipeline + the
|
|
/// E1.4b unit test call it.
|
|
pub fn convergeInferredErrorSets(self: *Lowering) void {
|
|
self.errorAnalysis().convergeInferredErrorSets();
|
|
}
|
|
|
|
pub fn containsTag(tags: []const u32, t: u32) bool {
|
|
for (tags) |x| if (x == t) return true;
|
|
return false;
|
|
}
|
|
|
|
/// Whole-program closure-shape error-set convergence. Thin delegation to the
|
|
/// canonical owner (`ErrorAnalysis`, `error_analysis.zig`); kept on
|
|
/// `Lowering` as a `pub` entry point because the lowering pipeline calls it.
|
|
pub fn convergeClosureShapeSets(self: *Lowering) void {
|
|
self.errorAnalysis().convergeClosureShapeSets();
|
|
}
|
|
|
|
/// Record one closure literal's contribution to its value-signature shape's
|
|
/// inferred-`!` union. No-op unless the literal is a CONCRETE (non-generic)
|
|
/// bare-`!` failable closure; named-set / non-failable literals add no tags.
|
|
pub fn recordClosureShape(self: *Lowering, lam: *const ast.Lambda) void {
|
|
if (lam.type_params.len > 0) return; // generic shapes out of scope (sub-feature 8)
|
|
const rt_node = lam.return_type orelse return; // no annotation → non-failable infer
|
|
const ret = self.resolveType(rt_node);
|
|
const es = self.errorChannelOf(ret) orelse return; // not failable
|
|
if (!self.isInferredErrorSet(es)) return; // `!Named` → its own set, not the inferred union
|
|
|
|
var ptys = std.ArrayList(TypeId).empty;
|
|
defer ptys.deinit(self.alloc);
|
|
for (lam.params) |p| {
|
|
if (p.is_variadic or p.is_pack or p.is_comptime) return; // not a plain fn-type slot
|
|
ptys.append(self.alloc, self.resolveType(p.type_expr)) catch return;
|
|
}
|
|
const key = self.closureShapeKey(ptys.items, self.returnValuePart(ret));
|
|
|
|
var tags = std.ArrayList(u32).empty;
|
|
defer tags.deinit(self.alloc);
|
|
var edges = std.ArrayList([]const u8).empty;
|
|
defer edges.deinit(self.alloc);
|
|
self.errorAnalysis().collectErrorSites(lam.body, &tags, &edges);
|
|
for (edges.items) |callee| {
|
|
for (self.calleeEscapeTags(callee)) |t| {
|
|
if (!containsTag(tags.items, t)) tags.append(self.alloc, t) catch {};
|
|
}
|
|
}
|
|
self.unionShapeTags(key, tags.items);
|
|
}
|
|
|
|
/// The escape tags of a callee referenced by name from a `try g()` edge:
|
|
/// a bare-`!` callee's converged set, or a `-> !Named` callee's declared set.
|
|
fn calleeEscapeTags(self: *Lowering, callee: []const u8) []const u32 {
|
|
if (self.inferred_error_sets.get(callee)) |t| return t;
|
|
if (self.program_index.fn_ast_map.get(callee)) |cfd| {
|
|
if (astPureNamedSet(cfd.return_type)) |nm| return self.namedSetTags(nm) orelse &.{};
|
|
}
|
|
return &.{};
|
|
}
|
|
|
|
/// Merge `new_tags` into the shape node `key` (sorted, deduped). The map is
|
|
/// content-keyed (StringHashMap), so re-`put` with a fresh equal key string
|
|
/// overwrites the existing node's value in place.
|
|
fn unionShapeTags(self: *Lowering, key: []const u8, new_tags: []const u32) void {
|
|
var list = std.ArrayList(u32).empty;
|
|
defer list.deinit(self.alloc);
|
|
if (self.shape_inferred_sets.get(key)) |existing| list.appendSlice(self.alloc, existing) catch {};
|
|
for (new_tags) |t| {
|
|
if (!containsTag(list.items, t)) list.append(self.alloc, t) catch {};
|
|
}
|
|
const sorted = self.alloc.dupe(u32, list.items) catch return;
|
|
std.mem.sort(u32, sorted, {}, std.sort.asc(u32));
|
|
self.shape_inferred_sets.put(key, sorted) catch {};
|
|
}
|
|
|
|
/// Canonical key for a callable VALUE-signature: param types + the value
|
|
/// part of the return (error slot excluded). Bare-`!` and non-failable
|
|
/// shapes of the same value-sig — and `.function` vs `.closure` of that
|
|
/// sig — collapse to one key, so all occurrences share one inferred node.
|
|
fn closureShapeKey(self: *Lowering, params: []const TypeId, value_ret: TypeId) []const u8 {
|
|
var buf = std.ArrayList(u8).empty;
|
|
buf.appendSlice(self.alloc, "shape") catch return "shape";
|
|
for (params) |p| {
|
|
buf.append(self.alloc, '_') catch return "shape";
|
|
buf.appendSlice(self.alloc, self.mangleTypeName(p)) catch return "shape";
|
|
}
|
|
buf.appendSlice(self.alloc, "__") catch return "shape";
|
|
buf.appendSlice(self.alloc, self.mangleTypeName(value_ret)) catch return "shape";
|
|
return buf.items;
|
|
}
|
|
|
|
/// The value part of a (possibly failable) return type, error slot dropped:
|
|
/// `(T, !)` → T (or a value-tuple); pure `-> !` → void; non-failable → self.
|
|
fn returnValuePart(self: *Lowering, ret: TypeId) TypeId {
|
|
const es = self.errorChannelOf(ret) orelse return ret;
|
|
if (ret == es) return .void;
|
|
return self.failableSuccessType(ret);
|
|
}
|
|
|
|
/// Shape key of a call's callee expression when it's a closure/fn-type slot
|
|
/// (variable, field, index — anything with a `.closure`/`.function` type),
|
|
/// for the program-wide shape-union widening lookup. Null for non-callables.
|
|
fn shapeKeyOfCallee(self: *Lowering, node: *const Node) ?[]const u8 {
|
|
if (node.data != .call) return null;
|
|
const fty = self.inferExprType(node.data.call.callee);
|
|
if (fty.isBuiltin()) return null;
|
|
const info = self.module.types.get(fty);
|
|
const params: []const TypeId = switch (info) {
|
|
.closure => |c| c.params,
|
|
.function => |f| f.params,
|
|
else => return null,
|
|
};
|
|
const ret: TypeId = switch (info) {
|
|
.closure => |c| c.ret,
|
|
.function => |f| f.ret,
|
|
else => return null,
|
|
};
|
|
return self.closureShapeKey(params, self.returnValuePart(ret));
|
|
}
|
|
|
|
/// Human-readable description of a typed module-const initializer, used in
|
|
/// the issue-0088 type-mismatch diagnostic. A literal names its kind; a
|
|
/// const-expression is described by its inferred type category, so the
|
|
/// message is accurate for `N : string : M + 2` ("an integer expression")
|
|
/// as well as for `N : string : 4` ("an integer literal").
|
|
fn initializerDescription(self: *Lowering, node: *const Node) []const u8 {
|
|
return switch (node.data) {
|
|
.int_literal => "an integer literal",
|
|
.float_literal => "a float literal",
|
|
.bool_literal => "a boolean literal",
|
|
.string_literal => "a string literal",
|
|
.null_literal => "null",
|
|
.undef_literal => "'---'",
|
|
else => self.constExprDescription(self.inferExprType(node)),
|
|
};
|
|
}
|
|
|
|
fn constExprDescription(self: *Lowering, init_ty: TypeId) []const u8 {
|
|
if (self.isIntEx(init_ty)) return "an integer expression";
|
|
if (isFloat(init_ty)) return "a floating-point expression";
|
|
if (init_ty == .bool) return "a boolean expression";
|
|
if (init_ty == .string) return "a string expression";
|
|
return "an expression of an incompatible type";
|
|
}
|
|
|
|
fn binOpSymbol(op: ast.BinaryOp.Op) []const u8 {
|
|
return switch (op) {
|
|
.add => "+",
|
|
.sub => "-",
|
|
.mul => "*",
|
|
.div => "/",
|
|
.mod => "%",
|
|
.eq => "==",
|
|
.neq => "!=",
|
|
.lt => "<",
|
|
.lte => "<=",
|
|
.gt => ">",
|
|
.gte => ">=",
|
|
.and_op => "and",
|
|
.or_op => "or",
|
|
.bit_and => "&",
|
|
.bit_or => "|",
|
|
.bit_xor => "^",
|
|
.shl => "<<",
|
|
.shr => ">>",
|
|
.in_op => "in",
|
|
};
|
|
}
|
|
|
|
fn typeBits(ty: TypeId) u32 {
|
|
return switch (ty) {
|
|
.bool => 1,
|
|
.s8, .u8 => 8,
|
|
.s16, .u16 => 16,
|
|
.s32, .u32 => 32,
|
|
.s64, .u64 => 64,
|
|
.usize, .isize => 0, // target-dependent — use typeBitsEx
|
|
.f32 => 32,
|
|
.f64 => 64,
|
|
else => 0,
|
|
};
|
|
}
|
|
|
|
pub fn typeBitsEx(self: *Lowering, ty: TypeId) u32 {
|
|
if (ty == .usize or ty == .isize) return @as(u32, self.module.types.pointer_size) * 8;
|
|
const b = typeBits(ty);
|
|
if (b > 0) return b;
|
|
if (!ty.isBuiltin()) {
|
|
const info = self.module.types.get(ty);
|
|
return switch (info) {
|
|
.signed => |w| @as(u32, w),
|
|
.unsigned => |w| @as(u32, w),
|
|
else => 0,
|
|
};
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/// Apply C default argument promotion to variadic-tail args. These rules
|
|
/// (bool/s8/s16/u8/u16 → s32, f32 → f64) match the C calling convention's
|
|
/// implicit promotions when an argument is passed through `...`.
|
|
fn promoteCVariadicArgs(self: *Lowering, args: []Ref, fixed_count: usize) void {
|
|
if (args.len <= fixed_count) return;
|
|
for (args[fixed_count..]) |*arg| {
|
|
const src_ty = self.builder.getRefType(arg.*);
|
|
const promoted: TypeId = switch (src_ty) {
|
|
.bool, .s8, .s16, .u8, .u16 => .s32,
|
|
.f32 => .f64,
|
|
else => continue,
|
|
};
|
|
arg.* = self.coerceToType(arg.*, src_ty, promoted);
|
|
}
|
|
}
|
|
|
|
/// Coerce call arguments in-place to match function parameter types.
|
|
fn coerceCallArgs(self: *Lowering, args: []Ref, params: []const Function.Param) void {
|
|
for (0..@min(args.len, params.len)) |i| {
|
|
const src_ty = self.builder.getRefType(args[i]);
|
|
const dst_ty = params[i].ty;
|
|
if (!src_ty.isBuiltin() and !dst_ty.isBuiltin()) {
|
|
const src_info = self.module.types.get(src_ty);
|
|
const dst_info = self.module.types.get(dst_ty);
|
|
// Array → many_pointer decay: alloca the array, GEP to first element
|
|
if (src_info == .array and dst_info == .many_pointer) {
|
|
const slot = self.builder.alloca(src_ty);
|
|
self.builder.store(slot, args[i]);
|
|
const zero = self.builder.constInt(0, .s64);
|
|
args[i] = self.builder.emit(.{ .index_gep = .{ .lhs = slot, .rhs = zero } }, dst_ty);
|
|
continue;
|
|
}
|
|
// Implicit address-of: passing T value where *T is expected → alloca + store
|
|
// Only when the pointee type matches the source type.
|
|
if (dst_info == .pointer and src_info != .pointer and dst_info.pointer.pointee == src_ty) {
|
|
const slot = self.builder.alloca(src_ty);
|
|
self.builder.store(slot, args[i]);
|
|
args[i] = slot;
|
|
continue;
|
|
}
|
|
}
|
|
args[i] = self.coerceToType(args[i], src_ty, dst_ty);
|
|
}
|
|
}
|
|
|
|
fn ensureTerminator(self: *Lowering, ret_ty: TypeId) void {
|
|
if (self.currentBlockHasTerminator()) return;
|
|
if (ret_ty == .noreturn) {
|
|
// A `-> noreturn` function never returns; if control reaches the
|
|
// end of the body it's genuinely unreachable (the body is expected
|
|
// to diverge — call another noreturn, loop forever, etc.).
|
|
self.builder.emitUnreachable();
|
|
} else if (ret_ty == .void) {
|
|
self.builder.retVoid();
|
|
} else {
|
|
// Use const_undef for complex types (string, struct, etc.)
|
|
const default_val = if (ret_ty == .string or !ret_ty.isBuiltin())
|
|
self.builder.constUndef(ret_ty)
|
|
else
|
|
self.builder.constInt(0, ret_ty);
|
|
self.builder.ret(default_val, ret_ty);
|
|
}
|
|
}
|
|
|
|
/// Emit a C-ABI exported function for every bodied method on a
|
|
/// `#jni_main #jni_class("...")` declaration. The symbol name follows
|
|
/// JNI's name-mangling convention so Android's JNI runtime can resolve
|
|
/// `private native sx_<method>(...)` (declared in the bundled
|
|
/// classes.dex by `jni_java_emit`) without an explicit `RegisterNatives`
|
|
/// call — i.e. `Java_<pkg-mangled>_<Class>_sx_1<method-mangled>`.
|
|
///
|
|
/// Param ABI: prepended `(env: *void, self: *void)` (JNIEnv* + jobject
|
|
/// receiver), followed by the user-declared params with pointer types
|
|
/// type-erased to `*void` (JNI carries jobjects, not sx-typed handles —
|
|
/// future work can keep richer typing inside the body when needed).
|
|
/// Eagerly lower bodied instance methods on every sx-defined
|
|
/// `#objc_class`. The Obj-C runtime invokes these via the IMP
|
|
/// pointers wired up in M1.2 A.4 — no sx-side call path triggers
|
|
/// lazy lowering, so we walk the cache and force-lower here.
|
|
/// `lowerFunction` sets `current_foreign_class` automatically based
|
|
/// on the qualified name, so `*Self` substitutions in the body
|
|
/// resolve correctly (M1.2 A.2b). After the bodies are lowered,
|
|
/// `emitObjcDefinedClassImps` wraps each with a C-ABI trampoline
|
|
/// (M1.2 A.4b.ii).
|
|
fn lowerObjcDefinedClassMethods(self: *Lowering) void {
|
|
for (self.module.objc_defined_class_cache.items) |entry| {
|
|
const fcd = entry.decl;
|
|
for (fcd.members) |m| {
|
|
const method = switch (m) {
|
|
.method => |md| md,
|
|
else => continue,
|
|
};
|
|
if (method.body == null) continue;
|
|
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fcd.name, method.name }) catch continue;
|
|
self.lazyLowerFunction(qualified);
|
|
}
|
|
}
|
|
// Now the bodies are lowered — emit the C-ABI IMP trampolines
|
|
// that bridge `objc_msgSend` invocations to them.
|
|
self.emitObjcDefinedClassImps();
|
|
}
|
|
|
|
/// If `obj_expr` is typed as a pointer to a foreign Obj-C class
|
|
/// and that class (or any of its `#extends` ancestors) declares a
|
|
/// `#property` field with the given name, return the
|
|
/// `ForeignFieldDecl`. M2.2 + M2.3.
|
|
pub fn lookupObjcPropertyOnPointer(self: *Lowering, obj_expr: *const ast.Node, field_name: []const u8) ?ast.ForeignFieldDecl {
|
|
const obj_ty = self.inferExprType(obj_expr);
|
|
if (obj_ty.isBuiltin()) return null;
|
|
const ptr_info = self.module.types.get(obj_ty);
|
|
if (ptr_info != .pointer) return null;
|
|
const pointee_info = self.module.types.get(ptr_info.pointer.pointee);
|
|
if (pointee_info != .@"struct") return null;
|
|
const struct_name = self.module.types.getString(pointee_info.@"struct".name);
|
|
const fcd = self.program_index.foreign_class_map.get(struct_name) orelse return null;
|
|
if (fcd.runtime != .objc_class and fcd.runtime != .objc_protocol) return null;
|
|
return self.findForeignPropertyInChain(fcd, field_name);
|
|
}
|
|
|
|
/// Walk the `#extends` chain looking for a method by name. M2.3.
|
|
/// Returns the owning fcd + the method decl, or null if no ancestor
|
|
/// declares it. Depth-capped at 16 to break accidental cycles
|
|
/// (real Obj-C class chains rarely exceed 6 levels).
|
|
fn findForeignMethodInChain(self: *Lowering, fcd: *const ast.ForeignClassDecl, method_name: []const u8) ?struct { fcd: *const ast.ForeignClassDecl, method: ast.ForeignMethodDecl } {
|
|
var current: *const ast.ForeignClassDecl = fcd;
|
|
var depth: u32 = 0;
|
|
while (depth < 16) : (depth += 1) {
|
|
for (current.members) |m| switch (m) {
|
|
.method => |md| if (std.mem.eql(u8, md.name, method_name)) return .{ .fcd = current, .method = md },
|
|
else => {},
|
|
};
|
|
// Not on this level — follow `#extends ParentName`.
|
|
const parent = blk: {
|
|
for (current.members) |m| switch (m) {
|
|
.extends => |p| break :blk p,
|
|
else => {},
|
|
};
|
|
break :blk null;
|
|
} orelse return null;
|
|
current = self.program_index.foreign_class_map.get(parent) orelse return null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Walk the `#extends` chain looking for a `#property` field by
|
|
/// name. M2.3 companion to findForeignMethodInChain.
|
|
fn findForeignPropertyInChain(self: *Lowering, fcd: *const ast.ForeignClassDecl, field_name: []const u8) ?ast.ForeignFieldDecl {
|
|
var current: *const ast.ForeignClassDecl = fcd;
|
|
var depth: u32 = 0;
|
|
while (depth < 16) : (depth += 1) {
|
|
for (current.members) |m| switch (m) {
|
|
.field => |f| if (f.is_property and std.mem.eql(u8, f.name, field_name)) return f,
|
|
else => {},
|
|
};
|
|
const parent = blk: {
|
|
for (current.members) |m| switch (m) {
|
|
.extends => |p| break :blk p,
|
|
else => {},
|
|
};
|
|
break :blk null;
|
|
} orelse return null;
|
|
current = self.program_index.foreign_class_map.get(parent) orelse return null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const ObjcDefinedStateField = struct {
|
|
field_ty: TypeId,
|
|
state_ty: TypeId,
|
|
field_idx: u32,
|
|
fcd: *const ast.ForeignClassDecl,
|
|
};
|
|
|
|
/// State-field-access info: if obj_expr is *<sx-defined-class>
|
|
/// and `field_name` is in the state struct (not a property),
|
|
/// returns the field's TypeId, the state struct's TypeId, and
|
|
/// the field's index. M1.2 A.3 supports.
|
|
pub fn lookupObjcDefinedStateFieldOnPointer(self: *Lowering, obj_expr: *const ast.Node, field_name: []const u8) ?ObjcDefinedStateField {
|
|
const obj_ty = self.inferExprType(obj_expr);
|
|
if (obj_ty.isBuiltin()) return null;
|
|
const ptr_info = self.module.types.get(obj_ty);
|
|
if (ptr_info != .pointer) return null;
|
|
const pointee_info = self.module.types.get(ptr_info.pointer.pointee);
|
|
if (pointee_info != .@"struct") return null;
|
|
const struct_name = self.module.types.getString(pointee_info.@"struct".name);
|
|
const fcd = self.program_index.foreign_class_map.get(struct_name) orelse return null;
|
|
// Only sx-defined Obj-C classes have a state struct. Foreign
|
|
// classes' fields are purely declaration metadata (no state).
|
|
if (fcd.is_foreign or fcd.runtime != .objc_class) return null;
|
|
// Skip property fields — those dispatch via the M2.2 getter/setter
|
|
// path. Plain instance fields take the ivar+gep path.
|
|
for (fcd.members) |m| switch (m) {
|
|
.field => |f| {
|
|
if (std.mem.eql(u8, f.name, field_name)) {
|
|
if (f.is_property) return null;
|
|
const state_ty = self.objc().objcDefinedStateStructType(fcd);
|
|
const state_info = self.module.types.get(state_ty);
|
|
if (state_info != .@"struct") return null;
|
|
const fname_id = self.module.types.internString(f.name);
|
|
for (state_info.@"struct".fields, 0..) |sf, idx| {
|
|
if (sf.name == fname_id) {
|
|
return .{
|
|
.field_ty = sf.ty,
|
|
.state_ty = state_ty,
|
|
.field_idx = @intCast(idx),
|
|
.fcd = fcd,
|
|
};
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
},
|
|
else => {},
|
|
};
|
|
return null;
|
|
}
|
|
|
|
/// Lower a read of `self.field` (or `obj.field`) on a sx-defined
|
|
/// Obj-C class: `state = object_getIvar(self, load(ivar_global))`
|
|
/// then `struct_gep(state, idx)` + load. M1.2 A.3 — the runtime
|
|
/// hop through the hidden ivar.
|
|
fn lowerObjcDefinedStateFieldRead(
|
|
self: *Lowering,
|
|
obj_expr: *const ast.Node,
|
|
info: ObjcDefinedStateField,
|
|
) Ref {
|
|
const obj_ref = self.lowerExpr(obj_expr);
|
|
const state_ptr = self.lowerObjcDefinedStateForObj(obj_ref, info.fcd) orelse return Ref.none;
|
|
const ptr_void = self.module.types.ptrTo(.void);
|
|
const field_addr = self.builder.emit(.{ .struct_gep = .{
|
|
.base = state_ptr,
|
|
.field_index = info.field_idx,
|
|
.base_type = info.state_ty,
|
|
} }, ptr_void);
|
|
return self.builder.load(field_addr, info.field_ty);
|
|
}
|
|
|
|
/// `state = object_getIvar(obj, load(__<Cls>_state_ivar))`. Shared
|
|
/// helper for state-field read + write (M1.2 A.3).
|
|
fn lowerObjcDefinedStateForObj(self: *Lowering, obj_ref: Ref, fcd: *const ast.ForeignClassDecl) ?Ref {
|
|
const ptr_void = self.module.types.ptrTo(.void);
|
|
const ivar_global_name = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{fcd.name}) catch return null;
|
|
defer self.alloc.free(ivar_global_name);
|
|
const ivar_global_id = self.lookupGlobalIdByName(ivar_global_name) orelse return null;
|
|
const ivar_addr = self.builder.emit(.{ .global_addr = ivar_global_id }, ptr_void);
|
|
const ivar_handle = self.builder.load(ivar_addr, ptr_void);
|
|
const get_ivar_fid = self.ensureCRuntimeDecl("object_getIvar", &.{ ptr_void, ptr_void }, ptr_void);
|
|
const args = self.alloc.alloc(Ref, 2) catch return null;
|
|
args[0] = obj_ref;
|
|
args[1] = ivar_handle;
|
|
return self.builder.emit(.{ .call = .{ .callee = get_ivar_fid, .args = args } }, ptr_void);
|
|
}
|
|
|
|
/// Lower `obj.field` for an Obj-C `#property` field as
|
|
/// `objc_msg_send(obj, sel_<fieldName>)`. M2.2 — getter side.
|
|
/// The setter side lives in the assignment-statement lowering.
|
|
fn lowerObjcPropertyGetter(self: *Lowering, obj_expr: *const ast.Node, field: ast.ForeignFieldDecl, _: []const u8, _: ast.Span) Ref {
|
|
const obj_ref = self.lowerExpr(obj_expr);
|
|
const ret_ty = self.resolveType(field.field_type);
|
|
const vptr_ty = self.module.types.ptrTo(.void);
|
|
// The selector for a property getter is the field name verbatim
|
|
// (Obj-C convention; the override hook is for niche cases like
|
|
// `isHidden` and lands with M2.2's modifier handling).
|
|
const sel_slot_gid = self.internObjcSelector(field.name);
|
|
const slot_ptr = self.builder.emit(.{ .global_addr = sel_slot_gid }, self.module.types.ptrTo(vptr_ty));
|
|
const sel = self.builder.emit(.{ .load = .{ .operand = slot_ptr } }, vptr_ty);
|
|
return self.builder.emit(.{ .objc_msg_send = .{
|
|
.recv = obj_ref,
|
|
.sel = sel,
|
|
.args = &.{},
|
|
} }, ret_ty);
|
|
}
|
|
|
|
/// Lower `obj.field = val` for an Obj-C `#property` field as
|
|
/// `objc_msg_send(obj, sel_set<Field>:, val)`. M2.2 — setter side.
|
|
/// Selector: prepend "set", capitalize the first letter of the
|
|
/// field name, append ":". `backgroundColor` → `setBackgroundColor:`.
|
|
fn lowerObjcPropertySetter(self: *Lowering, obj_expr: *const ast.Node, field: ast.ForeignFieldDecl, val: Ref) void {
|
|
const obj_ref = self.lowerExpr(obj_expr);
|
|
const vptr_ty = self.module.types.ptrTo(.void);
|
|
|
|
// Build the setter selector.
|
|
var sel_buf = std.ArrayList(u8).empty;
|
|
defer sel_buf.deinit(self.alloc);
|
|
sel_buf.appendSlice(self.alloc, "set") catch unreachable;
|
|
if (field.name.len > 0) {
|
|
sel_buf.append(self.alloc, std.ascii.toUpper(field.name[0])) catch unreachable;
|
|
sel_buf.appendSlice(self.alloc, field.name[1..]) catch unreachable;
|
|
}
|
|
sel_buf.append(self.alloc, ':') catch unreachable;
|
|
const sel_str = self.alloc.dupe(u8, sel_buf.items) catch unreachable;
|
|
|
|
const sel_slot_gid = self.internObjcSelector(sel_str);
|
|
const slot_ptr = self.builder.emit(.{ .global_addr = sel_slot_gid }, self.module.types.ptrTo(vptr_ty));
|
|
const sel = self.builder.emit(.{ .load = .{ .operand = slot_ptr } }, vptr_ty);
|
|
const args = self.alloc.alloc(Ref, 1) catch unreachable;
|
|
args[0] = val;
|
|
_ = self.builder.emit(.{ .objc_msg_send = .{
|
|
.recv = obj_ref,
|
|
.sel = sel,
|
|
.args = args,
|
|
} }, .void);
|
|
}
|
|
|
|
/// Get a FuncId for an external C-callconv function. If a function
|
|
/// with this exported name already exists in the module (e.g.
|
|
/// declared by stdlib `#foreign` decl), return it; otherwise
|
|
/// declare it fresh with the given signature.
|
|
///
|
|
/// One helper instead of a `get<Name>Fid` per runtime function —
|
|
/// avoids per-function cache fields and per-function boilerplate.
|
|
fn ensureCRuntimeDecl(self: *Lowering, name: []const u8, param_tys: []const TypeId, ret_ty: TypeId) FuncId {
|
|
const name_id = self.module.types.internString(name);
|
|
for (self.module.functions.items, 0..) |f, i| {
|
|
if (f.name == name_id) return FuncId.fromIndex(@intCast(i));
|
|
}
|
|
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
|
for (param_tys, 0..) |pty, i| {
|
|
// Param names don't matter at the LLVM ABI boundary —
|
|
// synthesize generic ones (`a0`, `a1`, ...) so we don't
|
|
// need a parallel name list per call site.
|
|
const synth = std.fmt.allocPrint(self.alloc, "a{d}", .{i}) catch unreachable;
|
|
params.append(self.alloc, .{
|
|
.name = self.module.types.internString(synth),
|
|
.ty = pty,
|
|
}) catch unreachable;
|
|
}
|
|
const fid = self.builder.declareExtern(name_id, params.toOwnedSlice(self.alloc) catch unreachable, ret_ty);
|
|
self.module.getFunctionMut(fid).call_conv = .c;
|
|
return fid;
|
|
}
|
|
|
|
/// For each bodied instance method on a sx-defined `#objc_class`,
|
|
/// emit a C-ABI IMP trampoline that the Obj-C runtime calls (after
|
|
/// the dispatch path from `objc_msgSend`). The trampoline:
|
|
/// 1. Loads the cached ivar handle from `@__<Cls>_state_ivar`.
|
|
/// 2. Calls `object_getIvar(obj, ivar)` to get the `*<Cls>State`
|
|
/// state pointer.
|
|
/// 3. Calls the sx body `@<Cls>.<method>(__sx_default_context,
|
|
/// state, ...user_args)` (default sx-callconv).
|
|
/// 4. Returns the result (or `ret void`).
|
|
///
|
|
/// IMP name: `__<ClassName>_<methodName>_imp`. emit_llvm's
|
|
/// constructor (A.4b.ii companion) registers this via
|
|
/// `class_addMethod` with a derived selector + type encoding.
|
|
fn emitObjcDefinedClassImps(self: *Lowering) void {
|
|
for (self.module.objc_defined_class_cache.items) |entry| {
|
|
const fcd = entry.decl;
|
|
// Pin to the class's defining module (E4) so the IMP trampolines'
|
|
// method-signature types (`-> BOOL`, param types) resolve where they
|
|
// are visible, not at whatever lowering site triggered emission.
|
|
const saved_src = self.current_source_file;
|
|
defer self.setCurrentSourceFile(saved_src);
|
|
if (fcd.source_file) |src| self.setCurrentSourceFile(src);
|
|
// Synthesize +alloc (M1.2 A.5) and -dealloc (M1.2 A.6). emit_llvm
|
|
// registers +alloc on the metaclass and -dealloc on the class
|
|
// itself after objc_registerClassPair.
|
|
self.emitObjcDefinedClassAllocImp(fcd);
|
|
self.emitObjcDefinedClassDeallocImp(fcd);
|
|
for (fcd.members) |m| {
|
|
switch (m) {
|
|
.method => |method| {
|
|
if (method.body == null) continue;
|
|
self.emitObjcDefinedClassImp(fcd, method);
|
|
},
|
|
.field => |field| {
|
|
// M2.2 second pass — sx-defined property fields
|
|
// synthesize getter (+ setter unless `readonly`)
|
|
// IMPs that GEP into the state struct.
|
|
if (field.is_property) {
|
|
self.emitObjcDefinedClassPropertyImps(fcd, field);
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Lazily declare libobjc's ARC runtime helpers. Idempotent — uses
|
|
/// `ensureCRuntimeDecl` which skips already-declared symbols. Called
|
|
/// from the property setter/getter and -dealloc emission paths when
|
|
/// they need to emit a retain/release/storeWeak/etc.
|
|
fn ensureArcRuntimeDecls(self: *Lowering) void {
|
|
const ptr_void = self.module.types.ptrTo(.void);
|
|
_ = self.ensureCRuntimeDecl("objc_retain", &.{ptr_void}, ptr_void);
|
|
_ = self.ensureCRuntimeDecl("objc_release", &.{ptr_void}, .void);
|
|
_ = self.ensureCRuntimeDecl("objc_storeWeak", &.{ ptr_void, ptr_void }, ptr_void);
|
|
_ = self.ensureCRuntimeDecl("objc_loadWeakRetained", &.{ptr_void}, ptr_void);
|
|
_ = self.ensureCRuntimeDecl("objc_initWeak", &.{ ptr_void, ptr_void }, ptr_void);
|
|
_ = self.ensureCRuntimeDecl("objc_destroyWeak", &.{ptr_void}, .void);
|
|
}
|
|
|
|
/// M2.2 second pass — emit synthesized getter/setter IMPs for a
|
|
/// property field on a sx-defined `#objc_class`. The state struct
|
|
/// already holds the field (via objcDefinedStateStructType); the
|
|
/// IMPs just dispatch a load/store through the `__sx_state` ivar.
|
|
///
|
|
/// Getter IMP: `__<Cls>_<field>_imp(self, _cmd) -> T`
|
|
/// state = object_getIvar(self, load(__<Cls>_state_ivar))
|
|
/// return state.<field>
|
|
///
|
|
/// Setter IMP (skipped if `readonly` in modifiers):
|
|
/// `__<Cls>_set<Field>_imp(self, _cmd, val) -> void`
|
|
/// state = object_getIvar(self, load(__<Cls>_state_ivar))
|
|
/// state.<field> = val
|
|
///
|
|
/// Both IMPs land in the cache's methods slice with appropriate
|
|
/// selectors + encodings; emit_llvm's class_addMethod loop wires
|
|
/// them up like any other instance method.
|
|
fn emitObjcDefinedClassPropertyImps(self: *Lowering, fcd: *const ast.ForeignClassDecl, field: ast.ForeignFieldDecl) void {
|
|
const state_ty = self.objc().objcDefinedStateStructType(fcd);
|
|
const state_info = self.module.types.get(state_ty);
|
|
if (state_info != .@"struct") return;
|
|
// Find the field's index in the state struct.
|
|
const field_name_id = self.module.types.internString(field.name);
|
|
var field_idx: ?u32 = null;
|
|
for (state_info.@"struct".fields, 0..) |sf, i| {
|
|
if (sf.name == field_name_id) {
|
|
field_idx = @intCast(i);
|
|
break;
|
|
}
|
|
}
|
|
const fidx = field_idx orelse return;
|
|
const field_ty = self.resolveType(field.field_type);
|
|
|
|
// M4.B: validate modifiers + resolve ARC kind. Side-effect: emits
|
|
// diagnostics for typos, weak-on-non-object, ambiguous *void, etc.
|
|
// For now the setter/getter still emit bare load/store; subsequent
|
|
// M4.B commits wire the actual ARC ops keyed on this kind.
|
|
_ = self.objc().objcPropertyKind(field);
|
|
|
|
// (1) Getter: __<Cls>_<field>_imp
|
|
self.emitObjcDefinedPropertyGetter(fcd, field, state_ty, fidx, field_ty);
|
|
|
|
// (2) Setter — skipped for `readonly`.
|
|
var is_readonly = false;
|
|
for (field.property_modifiers) |mod| {
|
|
if (std.mem.eql(u8, mod, "readonly")) {
|
|
is_readonly = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!is_readonly) {
|
|
self.emitObjcDefinedPropertySetter(fcd, field, state_ty, fidx, field_ty);
|
|
}
|
|
|
|
// (3) Register in the cache's methods slice. Both IMPs use the
|
|
// method-registration pipeline that lands in class_addMethod
|
|
// calls from emit_llvm.
|
|
self.registerObjcDefinedPropertyMethodEntries(fcd, field, field_ty, is_readonly);
|
|
}
|
|
|
|
fn emitObjcDefinedPropertyGetter(self: *Lowering, fcd: *const ast.ForeignClassDecl, field: ast.ForeignFieldDecl, state_ty: TypeId, fidx: u32, field_ty: TypeId) void {
|
|
const saved_func = self.builder.func;
|
|
const saved_block = self.builder.current_block;
|
|
const saved_counter = self.builder.inst_counter;
|
|
defer {
|
|
self.builder.func = saved_func;
|
|
self.builder.current_block = saved_block;
|
|
self.builder.inst_counter = saved_counter;
|
|
}
|
|
|
|
const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, field.name }) catch return;
|
|
const name_id = self.module.types.internString(imp_name);
|
|
const ptr_void = self.module.types.ptrTo(.void);
|
|
|
|
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
|
params.append(self.alloc, .{ .name = self.module.types.internString("self"), .ty = ptr_void }) catch return;
|
|
params.append(self.alloc, .{ .name = self.module.types.internString("_cmd"), .ty = ptr_void }) catch return;
|
|
const params_slice = params.toOwnedSlice(self.alloc) catch return;
|
|
|
|
_ = self.builder.beginFunction(name_id, params_slice, field_ty);
|
|
const func = self.builder.currentFunc();
|
|
func.linkage = .external;
|
|
func.call_conv = .c;
|
|
func.has_implicit_ctx = false;
|
|
|
|
const entry_name = self.module.types.internString("entry");
|
|
const entry = self.builder.appendBlock(entry_name, &.{});
|
|
self.builder.switchToBlock(entry);
|
|
|
|
// state = object_getIvar(self, load @__<Cls>_state_ivar)
|
|
const self_ref = Ref.fromIndex(0);
|
|
const ivar_global_name = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{fcd.name}) catch return;
|
|
defer self.alloc.free(ivar_global_name);
|
|
const ivar_global_id = self.lookupGlobalIdByName(ivar_global_name) orelse return;
|
|
const ivar_addr = self.builder.emit(.{ .global_addr = ivar_global_id }, ptr_void);
|
|
const ivar_handle = self.builder.load(ivar_addr, ptr_void);
|
|
const get_ivar_fid = self.ensureCRuntimeDecl("object_getIvar", &.{ ptr_void, ptr_void }, ptr_void);
|
|
const get_args = self.alloc.alloc(Ref, 2) catch return;
|
|
get_args[0] = self_ref;
|
|
get_args[1] = ivar_handle;
|
|
const state_ptr = self.builder.emit(.{ .call = .{ .callee = get_ivar_fid, .args = get_args } }, ptr_void);
|
|
|
|
const field_addr = self.builder.emit(.{ .struct_gep = .{ .base = state_ptr, .field_index = fidx, .base_type = state_ty } }, ptr_void);
|
|
|
|
// M4.B getter — weak fields go through objc_loadWeakRetained +
|
|
// objc_autorelease for race-safe reads. The bare-load path
|
|
// (strong/copy/assign) is the common case and reads the slot
|
|
// directly.
|
|
const kind = self.objc().objcPropertyKind(field);
|
|
if (kind == .weak) {
|
|
self.ensureArcRuntimeDecls();
|
|
const load_weak_fid = self.ensureCRuntimeDecl("objc_loadWeakRetained", &.{ptr_void}, ptr_void);
|
|
const autorelease_fid = self.ensureCRuntimeDecl("objc_autorelease", &.{ptr_void}, ptr_void);
|
|
|
|
// retained = objc_loadWeakRetained(field_addr)
|
|
// - atomic upgrade-to-strong via libobjc's side-table; if the
|
|
// target deinitialised, returns null. The caller gets a
|
|
// +1 retained reference (or null).
|
|
const load_args = self.alloc.alloc(Ref, 1) catch return;
|
|
load_args[0] = field_addr;
|
|
const retained = self.builder.emit(.{ .call = .{ .callee = load_weak_fid, .args = load_args } }, ptr_void);
|
|
|
|
// autoreleased = objc_autorelease(retained)
|
|
// - drops it into the current pool so the caller doesn't need
|
|
// to manually release. Returns the same pointer (typed).
|
|
const ar_args = self.alloc.alloc(Ref, 1) catch return;
|
|
ar_args[0] = retained;
|
|
const autoreleased = self.builder.emit(.{ .call = .{ .callee = autorelease_fid, .args = ar_args } }, ptr_void);
|
|
|
|
self.builder.ret(autoreleased, field_ty);
|
|
self.builder.finalize();
|
|
return;
|
|
}
|
|
|
|
// strong / copy / assign — bare load.
|
|
const val = self.builder.load(field_addr, field_ty);
|
|
self.builder.ret(val, field_ty);
|
|
self.builder.finalize();
|
|
}
|
|
|
|
fn emitObjcDefinedPropertySetter(self: *Lowering, fcd: *const ast.ForeignClassDecl, field: ast.ForeignFieldDecl, state_ty: TypeId, fidx: u32, field_ty: TypeId) void {
|
|
const saved_func = self.builder.func;
|
|
const saved_block = self.builder.current_block;
|
|
const saved_counter = self.builder.inst_counter;
|
|
defer {
|
|
self.builder.func = saved_func;
|
|
self.builder.current_block = saved_block;
|
|
self.builder.inst_counter = saved_counter;
|
|
}
|
|
|
|
// Setter selector: set<Field>: → imp name: __<Cls>_set<Field>_imp
|
|
var setter_field_buf = std.ArrayList(u8).empty;
|
|
defer setter_field_buf.deinit(self.alloc);
|
|
setter_field_buf.appendSlice(self.alloc, "set") catch unreachable;
|
|
if (field.name.len > 0) {
|
|
setter_field_buf.append(self.alloc, std.ascii.toUpper(field.name[0])) catch unreachable;
|
|
setter_field_buf.appendSlice(self.alloc, field.name[1..]) catch unreachable;
|
|
}
|
|
const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, setter_field_buf.items }) catch return;
|
|
const name_id = self.module.types.internString(imp_name);
|
|
const ptr_void = self.module.types.ptrTo(.void);
|
|
|
|
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
|
params.append(self.alloc, .{ .name = self.module.types.internString("self"), .ty = ptr_void }) catch return;
|
|
params.append(self.alloc, .{ .name = self.module.types.internString("_cmd"), .ty = ptr_void }) catch return;
|
|
params.append(self.alloc, .{ .name = self.module.types.internString("val"), .ty = field_ty }) catch return;
|
|
const params_slice = params.toOwnedSlice(self.alloc) catch return;
|
|
|
|
_ = self.builder.beginFunction(name_id, params_slice, .void);
|
|
const func = self.builder.currentFunc();
|
|
func.linkage = .external;
|
|
func.call_conv = .c;
|
|
func.has_implicit_ctx = false;
|
|
|
|
const entry_name = self.module.types.internString("entry");
|
|
const entry = self.builder.appendBlock(entry_name, &.{});
|
|
self.builder.switchToBlock(entry);
|
|
|
|
const self_ref = Ref.fromIndex(0);
|
|
const val_ref = Ref.fromIndex(2);
|
|
const ivar_global_name = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{fcd.name}) catch return;
|
|
defer self.alloc.free(ivar_global_name);
|
|
const ivar_global_id = self.lookupGlobalIdByName(ivar_global_name) orelse return;
|
|
const ivar_addr = self.builder.emit(.{ .global_addr = ivar_global_id }, ptr_void);
|
|
const ivar_handle = self.builder.load(ivar_addr, ptr_void);
|
|
const get_ivar_fid = self.ensureCRuntimeDecl("object_getIvar", &.{ ptr_void, ptr_void }, ptr_void);
|
|
const get_args = self.alloc.alloc(Ref, 2) catch return;
|
|
get_args[0] = self_ref;
|
|
get_args[1] = ivar_handle;
|
|
const state_ptr = self.builder.emit(.{ .call = .{ .callee = get_ivar_fid, .args = get_args } }, ptr_void);
|
|
|
|
const field_addr = self.builder.emit(.{ .struct_gep = .{ .base = state_ptr, .field_index = fidx, .base_type = state_ty } }, ptr_void);
|
|
|
|
// M4.B setter — emit ARC ops based on the property's modifier kind.
|
|
const kind = self.objc().objcPropertyKind(field);
|
|
switch (kind) {
|
|
.assign => {
|
|
// Primitives or explicit assign: bare store, no ARC.
|
|
self.builder.store(field_addr, val_ref);
|
|
},
|
|
.strong => {
|
|
// Retain new, release old. Order matters: retain first
|
|
// (in case val == old, we don't release before retain).
|
|
self.ensureArcRuntimeDecls();
|
|
const retain_fid = self.ensureCRuntimeDecl("objc_retain", &.{ptr_void}, ptr_void);
|
|
const release_fid = self.ensureCRuntimeDecl("objc_release", &.{ptr_void}, .void);
|
|
|
|
// old = load field_addr
|
|
const old_val = self.builder.load(field_addr, field_ty);
|
|
// new = objc_retain(val)
|
|
const retain_args = self.alloc.alloc(Ref, 1) catch return;
|
|
retain_args[0] = val_ref;
|
|
_ = self.builder.emit(.{ .call = .{ .callee = retain_fid, .args = retain_args } }, ptr_void);
|
|
// store field_addr, val
|
|
self.builder.store(field_addr, val_ref);
|
|
// objc_release(old) — Apple's runtime treats release(NULL) as a no-op,
|
|
// so we skip an explicit null-check (saves a branch on every assign).
|
|
const release_args = self.alloc.alloc(Ref, 1) catch return;
|
|
release_args[0] = old_val;
|
|
_ = self.builder.emit(.{ .call = .{ .callee = release_fid, .args = release_args } }, .void);
|
|
},
|
|
.weak => {
|
|
// objc_storeWeak(field_addr, val) handles first-store
|
|
// (init) and re-store (destroy old + init new) atomically.
|
|
self.ensureArcRuntimeDecls();
|
|
const store_weak_fid = self.ensureCRuntimeDecl("objc_storeWeak", &.{ ptr_void, ptr_void }, ptr_void);
|
|
const store_args = self.alloc.alloc(Ref, 2) catch return;
|
|
store_args[0] = field_addr;
|
|
store_args[1] = val_ref;
|
|
_ = self.builder.emit(.{ .call = .{ .callee = store_weak_fid, .args = store_args } }, ptr_void);
|
|
},
|
|
.copy => {
|
|
// copy = objc_msgSend(val, sel_copy) — returns retained
|
|
// (NSCopying contract).
|
|
// Release old, then store the copy.
|
|
self.ensureArcRuntimeDecls();
|
|
const release_fid = self.ensureCRuntimeDecl("objc_release", &.{ptr_void}, .void);
|
|
|
|
// Load + cache the `copy` selector slot.
|
|
const sel_copy_gid = self.internObjcSelector("copy");
|
|
const sel_slot_ptr = self.builder.emit(.{ .global_addr = sel_copy_gid }, self.module.types.ptrTo(ptr_void));
|
|
const sel_copy = self.builder.emit(.{ .load = .{ .operand = sel_slot_ptr } }, ptr_void);
|
|
|
|
// copy = [val copy]
|
|
const copy_args = self.alloc.alloc(Ref, 0) catch return;
|
|
const copied = self.builder.emit(.{ .objc_msg_send = .{
|
|
.recv = val_ref,
|
|
.sel = sel_copy,
|
|
.args = copy_args,
|
|
} }, ptr_void);
|
|
|
|
const old_val = self.builder.load(field_addr, field_ty);
|
|
self.builder.store(field_addr, copied);
|
|
const release_args = self.alloc.alloc(Ref, 1) catch return;
|
|
release_args[0] = old_val;
|
|
_ = self.builder.emit(.{ .call = .{ .callee = release_fid, .args = release_args } }, .void);
|
|
},
|
|
}
|
|
self.builder.retVoid();
|
|
self.builder.finalize();
|
|
}
|
|
|
|
/// Append the property's getter (and setter, unless readonly)
|
|
/// entries to the class's method-registration slice so emit_llvm
|
|
/// calls class_addMethod on each. Selectors + encodings derived
|
|
/// from the field type.
|
|
fn registerObjcDefinedPropertyMethodEntries(self: *Lowering, fcd: *const ast.ForeignClassDecl, field: ast.ForeignFieldDecl, field_ty: TypeId, is_readonly: bool) void {
|
|
const cur = self.module.lookupObjcDefinedClass(fcd.name) orelse return;
|
|
_ = cur;
|
|
// Find the existing entry and grow its methods slice.
|
|
var new_methods = std.ArrayList(Module.ObjcDefinedMethodEntry).empty;
|
|
for (self.module.objc_defined_class_cache.items) |entry| {
|
|
if (!std.mem.eql(u8, entry.name, fcd.name)) continue;
|
|
for (entry.methods) |m| new_methods.append(self.alloc, m) catch unreachable;
|
|
|
|
// Getter entry — selector = field name, encoding = "<ret>@:".
|
|
const getter_enc = self.objc().objcTypeEncodingFromSignature(field_ty, &.{}, null) catch return;
|
|
const getter_imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, field.name }) catch return;
|
|
new_methods.append(self.alloc, .{
|
|
.sel = field.name,
|
|
.encoding = getter_enc,
|
|
.imp_name = getter_imp_name,
|
|
.is_class = false,
|
|
}) catch unreachable;
|
|
|
|
// Setter entry — selector = set<Field>:, encoding = "v@:<ty>".
|
|
if (!is_readonly) {
|
|
var sel_buf = std.ArrayList(u8).empty;
|
|
defer sel_buf.deinit(self.alloc);
|
|
sel_buf.appendSlice(self.alloc, "set") catch unreachable;
|
|
if (field.name.len > 0) {
|
|
sel_buf.append(self.alloc, std.ascii.toUpper(field.name[0])) catch unreachable;
|
|
sel_buf.appendSlice(self.alloc, field.name[1..]) catch unreachable;
|
|
}
|
|
sel_buf.append(self.alloc, ':') catch unreachable;
|
|
const setter_sel = self.alloc.dupe(u8, sel_buf.items) catch return;
|
|
|
|
const setter_enc = self.objc().objcTypeEncodingFromSignature(.void, &.{field_ty}, null) catch return;
|
|
|
|
var setter_imp_field_buf = std.ArrayList(u8).empty;
|
|
defer setter_imp_field_buf.deinit(self.alloc);
|
|
setter_imp_field_buf.appendSlice(self.alloc, "set") catch unreachable;
|
|
if (field.name.len > 0) {
|
|
setter_imp_field_buf.append(self.alloc, std.ascii.toUpper(field.name[0])) catch unreachable;
|
|
setter_imp_field_buf.appendSlice(self.alloc, field.name[1..]) catch unreachable;
|
|
}
|
|
const setter_imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, setter_imp_field_buf.items }) catch return;
|
|
|
|
new_methods.append(self.alloc, .{
|
|
.sel = setter_sel,
|
|
.encoding = setter_enc,
|
|
.imp_name = setter_imp_name,
|
|
.is_class = false,
|
|
}) catch unreachable;
|
|
}
|
|
break;
|
|
}
|
|
const slice = new_methods.toOwnedSlice(self.alloc) catch return;
|
|
self.module.setObjcDefinedClassMethods(fcd.name, slice);
|
|
}
|
|
|
|
fn emitObjcDefinedClassImp(self: *Lowering, fcd: *const ast.ForeignClassDecl, md: ast.ForeignMethodDecl) void {
|
|
// Class methods (no `*Self` first param) skip the ivar read —
|
|
// they have no instance state to thread through.
|
|
if (md.is_static) {
|
|
self.emitObjcDefinedClassStaticImp(fcd, md);
|
|
return;
|
|
}
|
|
|
|
// Save+restore builder state — we're switching into a new fn
|
|
// mid-pass and need to restore for the next emit_llvm steps.
|
|
const saved_func = self.builder.func;
|
|
const saved_block = self.builder.current_block;
|
|
const saved_counter = self.builder.inst_counter;
|
|
defer {
|
|
self.builder.func = saved_func;
|
|
self.builder.current_block = saved_block;
|
|
self.builder.inst_counter = saved_counter;
|
|
}
|
|
|
|
const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, md.name }) catch return;
|
|
const name_id = self.module.types.internString(imp_name);
|
|
const ptr_void = self.module.types.ptrTo(.void);
|
|
|
|
// C-ABI signature: (obj: *void, _cmd: *void, ...user_args) -> ret.
|
|
// User params skip index 0 (which is *Self).
|
|
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
|
params.append(self.alloc, .{ .name = self.module.types.internString("obj"), .ty = ptr_void }) catch return;
|
|
params.append(self.alloc, .{ .name = self.module.types.internString("_cmd"), .ty = ptr_void }) catch return;
|
|
|
|
// Set current_foreign_class so *Self in user-param resolution
|
|
// resolves to *<Cls>State (M1.2 A.2b). Save+restore.
|
|
const saved_fc = self.current_foreign_class;
|
|
self.current_foreign_class = fcd;
|
|
defer self.current_foreign_class = saved_fc;
|
|
|
|
const param_start: usize = 1;
|
|
for (md.params[param_start..], 0..) |p_node, i| {
|
|
// User params are reflected at the C-ABI boundary AS-IS —
|
|
// the runtime trampoline forwards them through to the body.
|
|
// *Self here would be a programming error (only the implicit
|
|
// self at index 0 is *Self), but we use resolveType to handle
|
|
// pointer types correctly.
|
|
const pty = self.resolveType(p_node);
|
|
params.append(self.alloc, .{
|
|
.name = self.module.types.internString(md.param_names[param_start + i]),
|
|
.ty = pty,
|
|
}) catch return;
|
|
}
|
|
|
|
const ret_ty: TypeId = if (md.return_type) |rt| self.resolveType(rt) else .void;
|
|
const params_slice = params.toOwnedSlice(self.alloc) catch return;
|
|
|
|
_ = self.builder.beginFunction(name_id, params_slice, ret_ty);
|
|
const func = self.builder.currentFunc();
|
|
func.linkage = .external;
|
|
func.call_conv = .c;
|
|
func.has_implicit_ctx = false;
|
|
|
|
const entry_name = self.module.types.internString("entry");
|
|
const entry = self.builder.appendBlock(entry_name, &.{});
|
|
self.builder.switchToBlock(entry);
|
|
|
|
// Pass the Obj-C receiver pointer through to the sx body as
|
|
// `self`. The body's `self: *Self` type resolves to the
|
|
// foreign-class stub (the opaque Obj-C type), matching Apple's
|
|
// Obj-C semantics where `self` IS the object. `self.field`
|
|
// access on a sx-defined class is rewritten by lowerFieldAccess
|
|
// to go through `object_getIvar(self, __sx_state_ivar)` and
|
|
// a struct_gep on the state struct — see M1.2 A.3.
|
|
const obj_ref = Ref.fromIndex(0);
|
|
|
|
// Call sx body `@<Cls>.<method>(default_ctx, self, ...user_args)`.
|
|
const body_name = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fcd.name, md.name }) catch return;
|
|
defer self.alloc.free(body_name);
|
|
const body_fid = self.resolveFuncByName(body_name) orelse return;
|
|
|
|
const ctx_ref: ?Ref = blk: {
|
|
if (!self.implicit_ctx_enabled) break :blk null;
|
|
const dctx_gi = self.program_index.global_names.get("__sx_default_context") orelse break :blk null;
|
|
break :blk self.builder.emit(.{ .global_addr = dctx_gi.id }, ptr_void);
|
|
};
|
|
|
|
// Build arg list: [ctx?] + self + user_args.
|
|
const num_user_args = params_slice.len - 2; // minus obj + _cmd
|
|
const num_call_args = (if (ctx_ref != null) @as(usize, 1) else 0) + 1 + num_user_args;
|
|
const call_args = self.alloc.alloc(Ref, num_call_args) catch return;
|
|
var idx: usize = 0;
|
|
if (ctx_ref) |c_ref| {
|
|
call_args[idx] = c_ref;
|
|
idx += 1;
|
|
}
|
|
call_args[idx] = obj_ref;
|
|
idx += 1;
|
|
var ip: usize = 2;
|
|
while (ip < params_slice.len) : (ip += 1) {
|
|
call_args[idx] = Ref.fromIndex(@intCast(ip));
|
|
idx += 1;
|
|
}
|
|
|
|
const call_ref = self.builder.emit(.{ .call = .{
|
|
.callee = body_fid,
|
|
.args = call_args,
|
|
} }, ret_ty);
|
|
|
|
// (4) Return.
|
|
if (ret_ty == .void) {
|
|
self.builder.retVoid();
|
|
} else {
|
|
self.builder.ret(call_ref, ret_ty);
|
|
}
|
|
|
|
self.builder.finalize();
|
|
}
|
|
|
|
/// Synthesize the `+alloc` IMP for an sx-defined `#objc_class`.
|
|
/// Class method registered on the metaclass — when `[SxFoo alloc]`
|
|
/// runs from Apple's runtime (Info.plist principal class,
|
|
/// NSCoder unarchive, UIKit reflection), this IMP fires.
|
|
///
|
|
/// C-ABI: `(cls: id, _cmd: SEL) -> id`. No implicit ctx.
|
|
///
|
|
/// Body (M4.0):
|
|
/// %instance = class_createInstance(cls, 0)
|
|
/// %ctx_addr = &__sx_default_context
|
|
/// %state = ctx_addr.allocator.alloc(STATE_SIZE)
|
|
/// memset(state, 0, STATE_SIZE)
|
|
/// state[0] = allocator ← capture for -dealloc
|
|
/// object_setIvar(instance, __sx_state_ivar, state)
|
|
/// ret instance
|
|
///
|
|
/// Sx-side `Cls.alloc()` is intercepted at the call site (see
|
|
/// `lowerObjcStaticCall`) and emits the same sequence inline with
|
|
/// `current_ctx_ref` as the ctx — so `push Context.{ allocator = ... }`
|
|
/// flows through to per-instance allocator capture without going via
|
|
/// the IMP.
|
|
fn emitObjcDefinedClassAllocImp(self: *Lowering, fcd: *const ast.ForeignClassDecl) void {
|
|
const saved_func = self.builder.func;
|
|
const saved_block = self.builder.current_block;
|
|
const saved_counter = self.builder.inst_counter;
|
|
defer {
|
|
self.builder.func = saved_func;
|
|
self.builder.current_block = saved_block;
|
|
self.builder.inst_counter = saved_counter;
|
|
}
|
|
|
|
const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_alloc_imp", .{fcd.name}) catch return;
|
|
const name_id = self.module.types.internString(imp_name);
|
|
const ptr_void = self.module.types.ptrTo(.void);
|
|
|
|
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
|
params.append(self.alloc, .{ .name = self.module.types.internString("cls"), .ty = ptr_void }) catch return;
|
|
params.append(self.alloc, .{ .name = self.module.types.internString("_cmd"), .ty = ptr_void }) catch return;
|
|
const params_slice = params.toOwnedSlice(self.alloc) catch return;
|
|
|
|
_ = self.builder.beginFunction(name_id, params_slice, ptr_void);
|
|
const func = self.builder.currentFunc();
|
|
func.linkage = .external;
|
|
func.call_conv = .c;
|
|
func.has_implicit_ctx = false;
|
|
|
|
const entry_name = self.module.types.internString("entry");
|
|
const entry = self.builder.appendBlock(entry_name, &.{});
|
|
self.builder.switchToBlock(entry);
|
|
|
|
// ctx_addr = &__sx_default_context — IMP runs in Apple's runtime
|
|
// context, no implicit sx ctx to inherit, so use the process-wide
|
|
// default allocator. Sx-side callers bypass this IMP entirely
|
|
// (compiler intercepts Cls.alloc()) and use their own
|
|
// `context.allocator`.
|
|
const default_ctx_gi = self.program_index.global_names.get("__sx_default_context") orelse {
|
|
if (self.diagnostics) |d| {
|
|
d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedClassAllocImp: __sx_default_context global missing for class '{s}' (compiler bug — scan pass did not register the default context)", .{fcd.name});
|
|
}
|
|
return;
|
|
};
|
|
const ctx_addr = self.builder.emit(.{ .global_addr = default_ctx_gi.id }, ptr_void);
|
|
|
|
const cls_ref = Ref.fromIndex(0);
|
|
const instance = self.emitObjcDefinedAllocAndInit(fcd, cls_ref, ctx_addr) orelse return;
|
|
|
|
self.builder.ret(instance, ptr_void);
|
|
self.builder.finalize();
|
|
}
|
|
|
|
/// Shared inline sequence: allocate Obj-C instance + sx state struct,
|
|
/// capture the allocator, bind to the `__sx_state` ivar. Used by both
|
|
/// the `+alloc` IMP (ctx_addr = &__sx_default_context) and the sx-side
|
|
/// `Cls.alloc()` interception (ctx_addr = current_ctx_ref).
|
|
///
|
|
/// Returns the new instance pointer, or `null` if a required global is
|
|
/// missing (compiler bug — should be impossible after scan pass).
|
|
fn emitObjcDefinedAllocAndInit(
|
|
self: *Lowering,
|
|
fcd: *const ast.ForeignClassDecl,
|
|
cls_ref: Ref,
|
|
ctx_addr: Ref,
|
|
) ?Ref {
|
|
const ptr_void = self.module.types.ptrTo(.void);
|
|
|
|
// (1) instance = class_createInstance(cls, 0)
|
|
const create_fid = self.ensureCRuntimeDecl("class_createInstance", &.{ ptr_void, .u64 }, ptr_void);
|
|
const create_args = self.alloc.alloc(Ref, 2) catch return null;
|
|
create_args[0] = cls_ref;
|
|
create_args[1] = self.builder.constInt(0, .u64);
|
|
const instance = self.builder.emit(.{ .call = .{ .callee = create_fid, .args = create_args } }, ptr_void);
|
|
|
|
// STATE_SIZE = max(typeSizeBytes(__<Cls>State), 1).
|
|
const state_struct_ty = self.objc().objcDefinedStateStructType(fcd);
|
|
const raw_size = self.module.types.typeSizeBytes(state_struct_ty);
|
|
const state_size: u64 = if (raw_size == 0) 1 else @intCast(raw_size);
|
|
const size_const = self.builder.constInt(@intCast(state_size), .u64);
|
|
|
|
// (2) Dispatch through Context.allocator at ctx_addr:
|
|
// allocator = (*ctx_addr).field[0]
|
|
// state = allocator.alloc(size) (via inline-protocol fn-ptr)
|
|
const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse {
|
|
if (self.diagnostics) |d| {
|
|
d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedAllocAndInit: Context type not found in module for class '{s}' (compiler bug)", .{fcd.name});
|
|
}
|
|
return null;
|
|
};
|
|
const ctx_info = self.module.types.get(ctx_ty);
|
|
if (ctx_info != .@"struct" or ctx_info.@"struct".fields.len < 1) {
|
|
if (self.diagnostics) |d| {
|
|
d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedAllocAndInit: Context has unexpected shape for class '{s}' (compiler bug)", .{fcd.name});
|
|
}
|
|
return null;
|
|
}
|
|
const allocator_ty = ctx_info.@"struct".fields[0].ty;
|
|
const ctx_val = self.builder.load(ctx_addr, ctx_ty);
|
|
const allocator = self.builder.structGet(ctx_val, 0, allocator_ty);
|
|
const alloc_ctx = self.builder.structGet(allocator, 0, ptr_void);
|
|
const alloc_fn_ptr = self.builder.structGet(allocator, 1, ptr_void);
|
|
const call_args = self.alloc.dupe(Ref, &.{ ctx_addr, alloc_ctx, size_const }) catch return null;
|
|
const state = self.builder.emit(.{ .call_indirect = .{
|
|
.callee = alloc_fn_ptr,
|
|
.args = call_args,
|
|
} }, ptr_void);
|
|
|
|
// (3) memset(state, 0, STATE_SIZE) — zero everything including the
|
|
// allocator slot; the next store re-writes the allocator slot.
|
|
const memset_fid = self.ensureCRuntimeDecl("memset", &.{ ptr_void, .s32, .u64 }, ptr_void);
|
|
const memset_args = self.alloc.alloc(Ref, 3) catch return null;
|
|
memset_args[0] = state;
|
|
memset_args[1] = self.builder.constInt(0, .s32);
|
|
memset_args[2] = size_const;
|
|
_ = self.builder.emit(.{ .call = .{ .callee = memset_fid, .args = memset_args } }, ptr_void);
|
|
|
|
// (4) Capture allocator at state[0] — `-dealloc` reads it back.
|
|
const state_alloc_addr = self.builder.emit(.{ .struct_gep = .{
|
|
.base = state,
|
|
.field_index = 0,
|
|
.base_type = state_struct_ty,
|
|
} }, ptr_void);
|
|
self.builder.store(state_alloc_addr, allocator);
|
|
|
|
// (5) object_setIvar(instance, load(@__<Cls>_state_ivar), state)
|
|
const ivar_global_name = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{fcd.name}) catch return null;
|
|
defer self.alloc.free(ivar_global_name);
|
|
const ivar_global_id = self.lookupGlobalIdByName(ivar_global_name) orelse {
|
|
if (self.diagnostics) |d| {
|
|
d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedAllocAndInit: ivar global '{s}' missing (scan-pass bug)", .{ivar_global_name});
|
|
}
|
|
return null;
|
|
};
|
|
const ivar_addr_v = self.builder.emit(.{ .global_addr = ivar_global_id }, ptr_void);
|
|
const ivar_handle = self.builder.load(ivar_addr_v, ptr_void);
|
|
const set_ivar_fid = self.ensureCRuntimeDecl("object_setIvar", &.{ ptr_void, ptr_void, ptr_void }, .void);
|
|
const set_args = self.alloc.alloc(Ref, 3) catch return null;
|
|
set_args[0] = instance;
|
|
set_args[1] = ivar_handle;
|
|
set_args[2] = state;
|
|
_ = self.builder.emit(.{ .call = .{ .callee = set_ivar_fid, .args = set_args } }, .void);
|
|
|
|
return instance;
|
|
}
|
|
|
|
/// Emit a C-ABI IMP trampoline for a CLASS method (no `*Self`
|
|
/// first param) on a sx-defined `#objc_class`. M2.1(b).
|
|
/// Registered on the metaclass by emit_llvm.
|
|
///
|
|
/// C-ABI: `(cls: Class, _cmd: SEL, ...user_args) -> ret`
|
|
///
|
|
/// Body:
|
|
/// call @<Cls>.<method>(__sx_default_context, ...user_args)
|
|
/// ret <result>
|
|
///
|
|
/// No ivar read — class methods have no per-instance state.
|
|
fn emitObjcDefinedClassStaticImp(self: *Lowering, fcd: *const ast.ForeignClassDecl, md: ast.ForeignMethodDecl) void {
|
|
const saved_func = self.builder.func;
|
|
const saved_block = self.builder.current_block;
|
|
const saved_counter = self.builder.inst_counter;
|
|
defer {
|
|
self.builder.func = saved_func;
|
|
self.builder.current_block = saved_block;
|
|
self.builder.inst_counter = saved_counter;
|
|
}
|
|
|
|
const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, md.name }) catch return;
|
|
const name_id = self.module.types.internString(imp_name);
|
|
const ptr_void = self.module.types.ptrTo(.void);
|
|
|
|
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
|
params.append(self.alloc, .{ .name = self.module.types.internString("cls"), .ty = ptr_void }) catch return;
|
|
params.append(self.alloc, .{ .name = self.module.types.internString("_cmd"), .ty = ptr_void }) catch return;
|
|
|
|
// current_foreign_class lets `*Self` (if it appears in
|
|
// user-arg types — rare for class methods) resolve to the
|
|
// state-struct type. Save+restore.
|
|
const saved_fc = self.current_foreign_class;
|
|
self.current_foreign_class = fcd;
|
|
defer self.current_foreign_class = saved_fc;
|
|
|
|
for (md.params, 0..) |p_node, i| {
|
|
const pty = self.resolveType(p_node);
|
|
params.append(self.alloc, .{
|
|
.name = self.module.types.internString(md.param_names[i]),
|
|
.ty = pty,
|
|
}) catch return;
|
|
}
|
|
|
|
const ret_ty: TypeId = if (md.return_type) |rt| self.resolveType(rt) else .void;
|
|
const params_slice = params.toOwnedSlice(self.alloc) catch return;
|
|
|
|
_ = self.builder.beginFunction(name_id, params_slice, ret_ty);
|
|
const func = self.builder.currentFunc();
|
|
func.linkage = .external;
|
|
func.call_conv = .c;
|
|
func.has_implicit_ctx = false;
|
|
|
|
const entry_name = self.module.types.internString("entry");
|
|
const entry = self.builder.appendBlock(entry_name, &.{});
|
|
self.builder.switchToBlock(entry);
|
|
|
|
// Call @<Cls>.<method>(default_ctx, ...user_args).
|
|
const body_name = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fcd.name, md.name }) catch return;
|
|
defer self.alloc.free(body_name);
|
|
const body_fid = self.resolveFuncByName(body_name) orelse return;
|
|
|
|
const ctx_ref: ?Ref = blk: {
|
|
if (!self.implicit_ctx_enabled) break :blk null;
|
|
const dctx_gi = self.program_index.global_names.get("__sx_default_context") orelse break :blk null;
|
|
break :blk self.builder.emit(.{ .global_addr = dctx_gi.id }, ptr_void);
|
|
};
|
|
|
|
const num_user_args = params_slice.len - 2; // minus cls + _cmd
|
|
const num_call_args = (if (ctx_ref != null) @as(usize, 1) else 0) + num_user_args;
|
|
const call_args = self.alloc.alloc(Ref, num_call_args) catch return;
|
|
var idx: usize = 0;
|
|
if (ctx_ref) |c_ref| {
|
|
call_args[idx] = c_ref;
|
|
idx += 1;
|
|
}
|
|
var ip: usize = 2;
|
|
while (ip < params_slice.len) : (ip += 1) {
|
|
call_args[idx] = Ref.fromIndex(@intCast(ip));
|
|
idx += 1;
|
|
}
|
|
|
|
const call_ref = self.builder.emit(.{ .call = .{
|
|
.callee = body_fid,
|
|
.args = call_args,
|
|
} }, ret_ty);
|
|
|
|
if (ret_ty == .void) self.builder.retVoid() else self.builder.ret(call_ref, ret_ty);
|
|
self.builder.finalize();
|
|
}
|
|
|
|
/// Synthesize the `-dealloc` IMP for an sx-defined `#objc_class`.
|
|
/// Runs when the Obj-C runtime drops the last retain on an instance.
|
|
///
|
|
/// C-ABI: `(self: id, _cmd: SEL) -> void`. No implicit sx ctx.
|
|
///
|
|
/// Body (M4.0c):
|
|
/// %state = object_getIvar(self, load @__<Cls>_state_ivar)
|
|
/// %allocator = load struct_gep(state, 0) ← __sx_allocator (M4.0a)
|
|
/// allocator.dealloc(state) ← via inline-protocol fn-ptr
|
|
/// object_setIvar(self, ivar, null)
|
|
/// [super dealloc] // objc_msgSendSuper2(&super, sel_dealloc)
|
|
/// ret void
|
|
///
|
|
/// The state struct's first field is the allocator captured at
|
|
/// +alloc time (M4.0a + M4.0b). Reading it back lets -dealloc free
|
|
/// through the same allocator the instance was constructed with —
|
|
/// the per-instance allocator design from M1.2 A.5, now realised.
|
|
fn emitObjcDefinedClassDeallocImp(self: *Lowering, fcd: *const ast.ForeignClassDecl) void {
|
|
const saved_func = self.builder.func;
|
|
const saved_block = self.builder.current_block;
|
|
const saved_counter = self.builder.inst_counter;
|
|
defer {
|
|
self.builder.func = saved_func;
|
|
self.builder.current_block = saved_block;
|
|
self.builder.inst_counter = saved_counter;
|
|
}
|
|
|
|
const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_dealloc_imp", .{fcd.name}) catch return;
|
|
const name_id = self.module.types.internString(imp_name);
|
|
const ptr_void = self.module.types.ptrTo(.void);
|
|
|
|
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
|
params.append(self.alloc, .{ .name = self.module.types.internString("self"), .ty = ptr_void }) catch return;
|
|
params.append(self.alloc, .{ .name = self.module.types.internString("_cmd"), .ty = ptr_void }) catch return;
|
|
const params_slice = params.toOwnedSlice(self.alloc) catch return;
|
|
|
|
_ = self.builder.beginFunction(name_id, params_slice, .void);
|
|
const func = self.builder.currentFunc();
|
|
func.linkage = .external;
|
|
func.call_conv = .c;
|
|
func.has_implicit_ctx = false;
|
|
|
|
const entry_name = self.module.types.internString("entry");
|
|
const entry = self.builder.appendBlock(entry_name, &.{});
|
|
self.builder.switchToBlock(entry);
|
|
|
|
const self_ref = Ref.fromIndex(0);
|
|
|
|
// (1) state = object_getIvar(self, load @__<Cls>_state_ivar)
|
|
const ivar_global_name = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{fcd.name}) catch return;
|
|
defer self.alloc.free(ivar_global_name);
|
|
const ivar_global_id = self.lookupGlobalIdByName(ivar_global_name) orelse return;
|
|
const ivar_addr = self.builder.emit(.{ .global_addr = ivar_global_id }, ptr_void);
|
|
const ivar_handle = self.builder.load(ivar_addr, ptr_void);
|
|
|
|
const get_ivar_fid = self.ensureCRuntimeDecl("object_getIvar", &.{ ptr_void, ptr_void }, ptr_void);
|
|
const get_args = self.alloc.alloc(Ref, 2) catch return;
|
|
get_args[0] = self_ref;
|
|
get_args[1] = ivar_handle;
|
|
const state = self.builder.emit(.{ .call = .{ .callee = get_ivar_fid, .args = get_args } }, ptr_void);
|
|
|
|
// (2) M4.B dealloc — release strong/copy property ivars and
|
|
// destroyWeak weak property ivars BEFORE freeing the state struct
|
|
// (which would invalidate the pointers we need to read). Property
|
|
// metadata is re-derived from `fcd.members`; the state struct is
|
|
// already interned via objcDefinedStateStructType.
|
|
const state_struct_ty = self.objc().objcDefinedStateStructType(fcd);
|
|
const state_info_check = self.module.types.get(state_struct_ty);
|
|
if (state_info_check == .@"struct") {
|
|
const state_fields = state_info_check.@"struct".fields;
|
|
for (fcd.members) |m| switch (m) {
|
|
.field => |f| {
|
|
if (!f.is_property) continue;
|
|
// Find the field index in the state struct (by name —
|
|
// M4.0a's prepended __sx_allocator shifted user fields).
|
|
const field_name_id = self.module.types.internString(f.name);
|
|
var pfidx: ?u32 = null;
|
|
for (state_fields, 0..) |sf, i| {
|
|
if (sf.name == field_name_id) {
|
|
pfidx = @intCast(i);
|
|
break;
|
|
}
|
|
}
|
|
const fidx = pfidx orelse continue;
|
|
const field_ty = self.resolveType(f.field_type);
|
|
const kind = self.objc().objcPropertyKind(f);
|
|
|
|
switch (kind) {
|
|
.assign => {}, // no ARC ops
|
|
.strong, .copy => {
|
|
// val = load field; objc_release(val) — release(NULL) is a no-op.
|
|
self.ensureArcRuntimeDecls();
|
|
const release_fid = self.ensureCRuntimeDecl("objc_release", &.{ptr_void}, .void);
|
|
const field_addr = self.builder.emit(.{ .struct_gep = .{
|
|
.base = state,
|
|
.field_index = fidx,
|
|
.base_type = state_struct_ty,
|
|
} }, ptr_void);
|
|
const val = self.builder.load(field_addr, field_ty);
|
|
const args = self.alloc.alloc(Ref, 1) catch continue;
|
|
args[0] = val;
|
|
_ = self.builder.emit(.{ .call = .{ .callee = release_fid, .args = args } }, .void);
|
|
},
|
|
.weak => {
|
|
// objc_destroyWeak(&field) — unregisters the slot
|
|
// from libobjc's side-table.
|
|
self.ensureArcRuntimeDecls();
|
|
const destroy_weak_fid = self.ensureCRuntimeDecl("objc_destroyWeak", &.{ptr_void}, .void);
|
|
const field_addr = self.builder.emit(.{ .struct_gep = .{
|
|
.base = state,
|
|
.field_index = fidx,
|
|
.base_type = state_struct_ty,
|
|
} }, ptr_void);
|
|
const args = self.alloc.alloc(Ref, 1) catch continue;
|
|
args[0] = field_addr;
|
|
_ = self.builder.emit(.{ .call = .{ .callee = destroy_weak_fid, .args = args } }, .void);
|
|
},
|
|
}
|
|
},
|
|
else => {},
|
|
};
|
|
}
|
|
|
|
// (3) Free state through the captured allocator (M4.0a + M4.0b):
|
|
// allocator = load struct_gep(state, 0) ← __sx_allocator field
|
|
// allocator.dealloc(state) ← inline-protocol fn-ptr at field 2
|
|
// Compare to the old `free(state)` — that ignored the per-instance
|
|
// allocator and went straight to libc. Now `push Context.{ allocator = arena }`
|
|
// round-trips correctly: arena.alloc on construction, arena.dealloc here.
|
|
const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse {
|
|
if (self.diagnostics) |d| {
|
|
d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedClassDeallocImp: Context type not found for class '{s}' (compiler bug)", .{fcd.name});
|
|
}
|
|
return;
|
|
};
|
|
const ctx_info = self.module.types.get(ctx_ty);
|
|
if (ctx_info != .@"struct" or ctx_info.@"struct".fields.len < 1) {
|
|
if (self.diagnostics) |d| {
|
|
d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedClassDeallocImp: Context has unexpected shape for class '{s}'", .{fcd.name});
|
|
}
|
|
return;
|
|
}
|
|
const allocator_ty = ctx_info.@"struct".fields[0].ty;
|
|
|
|
const state_alloc_addr = self.builder.emit(.{ .struct_gep = .{
|
|
.base = state,
|
|
.field_index = 0,
|
|
.base_type = state_struct_ty,
|
|
} }, ptr_void);
|
|
const allocator = self.builder.load(state_alloc_addr, allocator_ty);
|
|
|
|
// Default-context address for the implicit __sx_ctx the dealloc
|
|
// fn-ptr takes as its first arg (the dealloc body might allocate
|
|
// internally; default GPA is the safe baseline).
|
|
const default_ctx_gi = self.program_index.global_names.get("__sx_default_context") orelse {
|
|
if (self.diagnostics) |d| {
|
|
d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedClassDeallocImp: __sx_default_context global missing for class '{s}'", .{fcd.name});
|
|
}
|
|
return;
|
|
};
|
|
const default_ctx_addr = self.builder.emit(.{ .global_addr = default_ctx_gi.id }, ptr_void);
|
|
const alloc_ctx = self.builder.structGet(allocator, 0, ptr_void);
|
|
const dealloc_fn_ptr = self.builder.structGet(allocator, 2, ptr_void);
|
|
const dealloc_args = self.alloc.dupe(Ref, &.{ default_ctx_addr, alloc_ctx, state }) catch return;
|
|
_ = self.builder.emit(.{ .call_indirect = .{
|
|
.callee = dealloc_fn_ptr,
|
|
.args = dealloc_args,
|
|
} }, .void);
|
|
|
|
// (3) object_setIvar(self, ivar, null)
|
|
const set_ivar_fid = self.ensureCRuntimeDecl("object_setIvar", &.{ ptr_void, ptr_void, ptr_void }, .void);
|
|
const null_ptr = self.builder.constInt(0, ptr_void);
|
|
const set_args = self.alloc.alloc(Ref, 3) catch return;
|
|
set_args[0] = self_ref;
|
|
set_args[1] = ivar_handle;
|
|
set_args[2] = null_ptr;
|
|
_ = self.builder.emit(.{ .call = .{ .callee = set_ivar_fid, .args = set_args } }, .void);
|
|
|
|
// (4) [super dealloc]
|
|
//
|
|
// objc_super = struct { receiver: id, super_class: Class }
|
|
const super_struct_ty = self.module.types.intern(.{ .@"struct" = .{
|
|
.name = self.module.types.internString("__sx_objc_super"),
|
|
.fields = blk: {
|
|
var f = std.ArrayList(types.TypeInfo.StructInfo.Field).empty;
|
|
f.append(self.alloc, .{ .name = self.module.types.internString("receiver"), .ty = ptr_void }) catch unreachable;
|
|
f.append(self.alloc, .{ .name = self.module.types.internString("super_class"), .ty = ptr_void }) catch unreachable;
|
|
break :blk f.toOwnedSlice(self.alloc) catch unreachable;
|
|
},
|
|
} });
|
|
const super_alloca = self.builder.alloca(super_struct_ty);
|
|
|
|
// store receiver
|
|
const recv_gep = self.builder.emit(.{ .struct_gep = .{ .base = super_alloca, .field_index = 0, .base_type = super_struct_ty } }, ptr_void);
|
|
self.builder.store(recv_gep, self_ref);
|
|
|
|
// store super_class = load @__<Cls>_class
|
|
const class_global_name = std.fmt.allocPrint(self.alloc, "__{s}_class", .{fcd.name}) catch return;
|
|
defer self.alloc.free(class_global_name);
|
|
const class_global_id = self.lookupGlobalIdByName(class_global_name) orelse return;
|
|
const class_addr = self.builder.emit(.{ .global_addr = class_global_id }, ptr_void);
|
|
const class_val = self.builder.load(class_addr, ptr_void);
|
|
const cls_gep = self.builder.emit(.{ .struct_gep = .{ .base = super_alloca, .field_index = 1, .base_type = super_struct_ty } }, ptr_void);
|
|
self.builder.store(cls_gep, class_val);
|
|
|
|
// sel_dealloc = sel_registerName("dealloc")
|
|
const sel_reg_fid = self.ensureCRuntimeDecl("sel_registerName", &.{ptr_void}, ptr_void);
|
|
const sel_str_gid = self.internStringConstantGlobal("dealloc");
|
|
const sel_str_addr = self.builder.emit(.{ .global_addr = sel_str_gid }, ptr_void);
|
|
const sel_args = self.alloc.alloc(Ref, 1) catch return;
|
|
sel_args[0] = sel_str_addr;
|
|
const sel_dealloc = self.builder.emit(.{ .call = .{ .callee = sel_reg_fid, .args = sel_args } }, ptr_void);
|
|
|
|
// objc_msgSendSuper2(&super, sel_dealloc)
|
|
const send_super_fid = self.ensureCRuntimeDecl("objc_msgSendSuper2", &.{ ptr_void, ptr_void }, .void);
|
|
const send_args = self.alloc.alloc(Ref, 2) catch return;
|
|
send_args[0] = super_alloca;
|
|
send_args[1] = sel_dealloc;
|
|
_ = self.builder.emit(.{ .call = .{ .callee = send_super_fid, .args = send_args } }, .void);
|
|
|
|
self.builder.retVoid();
|
|
self.builder.finalize();
|
|
}
|
|
|
|
/// Intern a C-string constant as a `[N:0]u8` global and return
|
|
/// its GlobalId. Used by IMP trampolines that need to pass a
|
|
/// literal string to runtime helpers (e.g. selector names).
|
|
fn internStringConstantGlobal(self: *Lowering, s: []const u8) inst_mod.GlobalId {
|
|
const z = self.alloc.allocSentinel(u8, s.len, 0) catch unreachable;
|
|
@memcpy(z[0..s.len], s);
|
|
const arr_ty = self.module.types.arrayOf(.u8, @intCast(s.len + 1));
|
|
const slot_name = std.fmt.allocPrint(self.alloc, "__sx_objc_cstr_{s}", .{s}) catch unreachable;
|
|
const name_id = self.module.types.internString(slot_name);
|
|
if (self.lookupGlobalIdByName(slot_name)) |existing| {
|
|
self.alloc.free(z);
|
|
return existing;
|
|
}
|
|
var bytes_vec = std.ArrayList(inst_mod.ConstantValue).empty;
|
|
for (z[0 .. s.len + 1]) |b| {
|
|
bytes_vec.append(self.alloc, .{ .int = b }) catch unreachable;
|
|
}
|
|
const init_val: inst_mod.ConstantValue = .{ .aggregate = bytes_vec.toOwnedSlice(self.alloc) catch unreachable };
|
|
return self.module.addGlobal(.{
|
|
.name = name_id,
|
|
.ty = arr_ty,
|
|
.init_val = init_val,
|
|
.is_extern = false,
|
|
.is_const = true,
|
|
});
|
|
}
|
|
|
|
/// Linear scan over module globals for a given name. Used for
|
|
/// looking up the per-class ivar handle global from inside IMP
|
|
/// trampoline emission.
|
|
fn lookupGlobalIdByName(self: *Lowering, name: []const u8) ?inst_mod.GlobalId {
|
|
const name_id = self.module.types.internString(name);
|
|
for (self.module.globals.items, 0..) |g, i| {
|
|
if (g.name == name_id) return inst_mod.GlobalId.fromIndex(@intCast(i));
|
|
}
|
|
return null;
|
|
}
|
|
|
|
fn synthesizeJniMainStubs(self: *Lowering) void {
|
|
var seen = std.StringHashMap(void).init(self.alloc);
|
|
defer seen.deinit();
|
|
|
|
var it = self.program_index.foreign_class_map.iterator();
|
|
while (it.next()) |entry| {
|
|
const fcd = entry.value_ptr.*;
|
|
if (!fcd.is_main) continue;
|
|
if (fcd.is_foreign) continue;
|
|
if (fcd.runtime != .jni_class) continue;
|
|
if (seen.contains(fcd.foreign_path)) continue;
|
|
seen.put(fcd.foreign_path, {}) catch continue;
|
|
|
|
for (fcd.members) |m| switch (m) {
|
|
.method => |md| {
|
|
if (md.body == null) continue;
|
|
if (md.is_static) continue; // future: emit static native ABI without `self`
|
|
self.synthesizeJniMainStub(fcd, md);
|
|
},
|
|
else => {},
|
|
};
|
|
}
|
|
}
|
|
|
|
fn synthesizeJniMainStub(self: *Lowering, fcd: *const ast.ForeignClassDecl, md: ast.ForeignMethodDecl) void {
|
|
const mangled = jni_descriptor.jniMangleNativeName(self.alloc, fcd.foreign_path, md.name) catch return;
|
|
const name_id = self.module.types.internString(mangled);
|
|
|
|
const ptr_void = self.module.types.ptrTo(.void);
|
|
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
|
params.append(self.alloc, .{
|
|
.name = self.module.types.internString("env"),
|
|
.ty = ptr_void,
|
|
}) catch return;
|
|
params.append(self.alloc, .{
|
|
.name = self.module.types.internString("self"),
|
|
.ty = ptr_void,
|
|
}) catch return;
|
|
|
|
// User's declared params (skip the implicit `*Self` at index 0 for
|
|
// instance methods — we synthesized `self` above as the jobject).
|
|
const param_start: usize = 1;
|
|
for (md.params[param_start..], 0..) |p_node, i| {
|
|
const pty = jniMapParamType(self, p_node);
|
|
params.append(self.alloc, .{
|
|
.name = self.module.types.internString(md.param_names[param_start + i]),
|
|
.ty = pty,
|
|
}) catch return;
|
|
}
|
|
|
|
const ret_ty = if (md.return_type) |rt| jniMapParamType(self, rt) else .void;
|
|
const params_slice = params.toOwnedSlice(self.alloc) catch return;
|
|
|
|
_ = self.builder.beginFunction(name_id, params_slice, ret_ty);
|
|
self.builder.currentFunc().linkage = .external;
|
|
self.builder.currentFunc().call_conv = .c;
|
|
|
|
const entry_name = self.module.types.internString("entry");
|
|
const entry = self.builder.appendBlock(entry_name, &.{});
|
|
self.builder.switchToBlock(entry);
|
|
|
|
var scope = Scope.init(self.alloc, self.scope);
|
|
defer scope.deinit();
|
|
const saved_scope = self.scope;
|
|
self.scope = &scope;
|
|
defer self.scope = saved_scope;
|
|
|
|
for (params_slice, 0..) |p, i| {
|
|
const slot = self.builder.alloca(p.ty);
|
|
const param_ref = Ref.fromIndex(@intCast(i));
|
|
self.builder.store(slot, param_ref);
|
|
scope.put(self.module.types.getString(p.name), .{ .ref = slot, .ty = p.ty, .is_alloca = true });
|
|
}
|
|
|
|
// Push the JNIEnv* arg onto the lexical `#jni_env` stack so the
|
|
// method body's `#jni_call(...)` / `super.method(...)` sites pick
|
|
// it up without an explicit `#jni_env(env) { ... }` wrapper. The
|
|
// JNI runtime guarantees the env passed to a native method is
|
|
// valid for the calling thread.
|
|
const env_slot = scope.lookup("env").?.ref;
|
|
const env_loaded = self.builder.load(env_slot, ptr_void);
|
|
const env_stack_base = self.jni_env_stack_base;
|
|
self.jni_env_stack_base = self.jni_env_stack.items.len;
|
|
self.jni_env_stack.append(self.alloc, env_loaded) catch {};
|
|
defer {
|
|
_ = self.jni_env_stack.pop();
|
|
self.jni_env_stack_base = env_stack_base;
|
|
}
|
|
|
|
// Record method context so `super.method(args)` inside the body
|
|
// can find the parent class (via `#extends`) and the method's
|
|
// signature.
|
|
const saved_fcd = self.current_foreign_class;
|
|
const saved_method = self.current_foreign_method;
|
|
self.current_foreign_class = fcd;
|
|
self.current_foreign_method = md;
|
|
defer {
|
|
self.current_foreign_class = saved_fcd;
|
|
self.current_foreign_method = saved_method;
|
|
}
|
|
|
|
// JNI native methods are C-callable entry points — install the
|
|
// static default Context so `context.X` reads in the method body
|
|
// resolve through `current_ctx_ref`. Mirror the same binding
|
|
// `lowerFunction` does for callconv(.c) / isExportedEntryName.
|
|
const saved_ctx_ref_jni = self.current_ctx_ref;
|
|
defer self.current_ctx_ref = saved_ctx_ref_jni;
|
|
if (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 }, ptr_void);
|
|
}
|
|
}
|
|
|
|
const saved_target = self.target_type;
|
|
self.target_type = if (ret_ty != .void) ret_ty else null;
|
|
if (ret_ty != .void) {
|
|
const body_val = self.lowerBlockValue(md.body.?);
|
|
if (!self.currentBlockHasTerminator()) {
|
|
if (body_val) |val| {
|
|
const val_ty = self.builder.getRefType(val);
|
|
if (val_ty == .void) {
|
|
self.ensureTerminator(ret_ty);
|
|
} else {
|
|
const coerced = self.coerceToType(val, val_ty, ret_ty);
|
|
self.builder.ret(coerced, ret_ty);
|
|
}
|
|
} else {
|
|
self.ensureTerminator(ret_ty);
|
|
}
|
|
}
|
|
} else {
|
|
self.lowerBlock(md.body.?);
|
|
self.ensureTerminator(ret_ty);
|
|
}
|
|
self.target_type = saved_target;
|
|
|
|
self.builder.finalize();
|
|
}
|
|
};
|
|
|
|
/// JNI param/return type resolution: user-declared types pass through
|
|
/// `resolveType` so the method body can dispatch on richer foreign-class
|
|
/// types (`holder.getSurface()` etc.). At LLVM level both `*SurfaceHolder`
|
|
/// and `*void` lower to the same `ptr`, so the C ABI shape Java sees is
|
|
/// unchanged — only sx-side method resolution benefits.
|
|
fn jniMapParamType(self: *Lowering, type_node: *ast.Node) TypeId {
|
|
return self.resolveType(type_node);
|
|
}
|