Verbatim relocation of the 13-method protocol cluster (protocol decl registration, param-protocol instantiation, thunk creation, vtable globals, protocol-value construction, dispatch emission, impl lookup) into src/ir/lower/protocol.zig. 13 fn aliases on Lowering keep all call sites unchanged. Two pub nested types travelled with the run (ProjectionPosition, PackProjection) and are re-exposed via Lowering type aliases; they are pack-domain types and may relocate to lower/pack.zig in B7.2. Method pub-flips: allocViaContext, callForeign, genericInstanceMethod, monomorphizeFunction. Gate: zig build OK; zig build test 426/426; run_examples 541/0; zero expected/ snapshot churn.
12592 lines
654 KiB
Zig
12592 lines
654 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 lower_error = @import("lower/error.zig");
|
|
const lower_comptime = @import("lower/comptime.zig");
|
|
const lower_stmt = @import("lower/stmt.zig");
|
|
const lower_control_flow = @import("lower/control_flow.zig");
|
|
const lower_decl = @import("lower/decl.zig");
|
|
const lower_nominal = @import("lower/nominal.zig");
|
|
const lower_protocol = @import("lower/protocol.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;
|
|
|
|
|
|
/// One frame in the chain of module-const names currently being folded by the
|
|
/// SOURCE-AWARE const evaluator (`Lowering.foldSourceConstInt` and its float
|
|
/// twins). Stack-allocated per recursive frame, so cycle detection needs no
|
|
/// allocation — the source-aware analogue of `program_index.ModuleConstFrame`,
|
|
/// which guards the GLOBAL-map fold (`moduleConstInt`). The frame keys on the
|
|
/// const's (name, author-source) pair, NOT name alone: same-name nested consts
|
|
/// across modules (`a.M` ≠ `b.M`) must NOT trip a false cycle (F3). A pair
|
|
/// already on the chain is a cyclic definition (`N :: N`; `N :: M + 1; M :: N`)
|
|
/// with no compile-time value → folds to null.
|
|
pub const ConstFoldFrame = struct {
|
|
name: []const u8,
|
|
source: ?[]const u8,
|
|
parent: ?*const ConstFoldFrame,
|
|
};
|
|
|
|
pub fn constFoldFrameContains(frame: ?*const ConstFoldFrame, name: []const u8, source: ?[]const u8) bool {
|
|
var cur = frame;
|
|
while (cur) |c| : (cur = c.parent) {
|
|
if (std.mem.eql(u8, c.name, name) and sourcesEql(c.source, source)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
fn sourcesEql(a: ?[]const u8, b: ?[]const u8) bool {
|
|
if (a == null and b == null) return true;
|
|
if (a == null or b == null) return false;
|
|
return std.mem.eql(u8, a.?, b.?);
|
|
}
|
|
|
|
/// Folding context for a SOURCE-AWARE module-const EXPRESSION RHS (E2/F2/R1).
|
|
/// The leaf-resolution twin of `program_index.ModuleConstCtx`, but every leaf
|
|
/// name resolves through the querying source's OWN const author
|
|
/// (`selectModuleConst`, own-wins / ambiguous) instead of the GLOBAL last-wins
|
|
/// `module_const_map`. This is what makes a same-name shadow's RHS chain
|
|
/// (`K :: M + 1`, with `M` a same-name shadow too) fold `M` to the SELECTED
|
|
/// author's `M` — coherently for a const used as a value AND as an array
|
|
/// dimension / count. `frame` is the cyclic-definition guard.
|
|
pub const SourceConstCtx = struct {
|
|
lowering: *Lowering,
|
|
frame: ?*const ConstFoldFrame,
|
|
pub fn lookupDimName(self: SourceConstCtx, name: []const u8) ?i64 {
|
|
return self.lowering.foldSourceConstInt(name, self.frame);
|
|
}
|
|
pub fn lookupPackLen(self: SourceConstCtx, name: []const u8) ?i64 {
|
|
return self.lowering.lookupPackLen(name);
|
|
}
|
|
pub fn lookupFloatName(self: SourceConstCtx, name: []const u8) ?f64 {
|
|
return self.lowering.foldSourceConstFloat(name, self.frame);
|
|
}
|
|
pub fn nameIsFloatTyped(self: SourceConstCtx, name: []const u8) bool {
|
|
return self.lowering.sourceConstIsFloatTyped(name, self.frame);
|
|
}
|
|
};
|
|
|
|
// ── 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
|
|
struct_instance_author: std.StringHashMap(*const ast.StructDecl) = std.StringHashMap(*const ast.StructDecl).init(std.heap.page_allocator), // mangled struct name → authoring StructDecl (CP-2: body-author ≡ layout-author)
|
|
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).
|
|
pub 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,
|
|
|
|
pub 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;
|
|
}
|
|
|
|
pub 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 ──────────────────────────────────────────
|
|
|
|
pub fn lowerExpr(self: *Lowering, node: *const Node) Ref {
|
|
// Stamp this node's source span onto the instructions it emits (ERR
|
|
// E3.0 — feeds DWARF line-info + comptime frame resolution). Save/
|
|
// 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_global| {
|
|
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);
|
|
}
|
|
// F2: emit the SOURCE-AWARE author's value (own-wins), not the
|
|
// global last-wins `ci_global`. ≥2 flat-visible same-name const
|
|
// authors → a loud ambiguity (issue 0105 / 0760), never a silent
|
|
// pick. `.none` after a visible name is the registration-only
|
|
// author (no per-source partition) — emit its global value.
|
|
switch (self.selectModuleConst(id.name)) {
|
|
.resolved => |sel| break :blk self.emitModuleConst(sel.info, sel.source),
|
|
.ambiguous => {
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, node.span, "'{s}' is ambiguous: it is declared in multiple flat-imported modules; qualify the reference or remove the duplicate import", .{id.name});
|
|
break :blk self.emitPlaceholder(id.name);
|
|
},
|
|
.none => break :blk self.emitModuleConst(ci_global, null),
|
|
}
|
|
}
|
|
// 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).
|
|
// E4 single-hop visibility + ambiguity gate: a bare type name used
|
|
// as a VALUE (`x: Type = COnly`, `x == COnly`) reachable only over
|
|
// 2+ flat hops is not bare-visible (consistent with annotations /
|
|
// 0763); ≥2 direct flat same-name authors are ambiguous (loud
|
|
// diagnostic, 0755/0767). A single source-keyed author — including
|
|
// the querying source's OWN author over a same-name flat import
|
|
// (own-wins, 0754) — resolves to ITS TypeId, NOT whichever same-name
|
|
// author a global `findByName` would pick. A value name / generic
|
|
// param / undeclared name → `.proceed`, falling through below.
|
|
const ty = blk_ty: {
|
|
switch (self.headTypeGate(id.name, node.span)) {
|
|
.ambiguous, .not_visible => break :blk self.emitPlaceholder(id.name),
|
|
.resolved => |tid| break :blk_ty tid,
|
|
.proceed => {},
|
|
}
|
|
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 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);
|
|
}
|
|
}
|
|
// Generic-struct typed-literal head (`Box(s64).[...]`): route
|
|
// through the single layout choke-point (CP-1). A qualified head
|
|
// `a.Box(s64).[...]` selects a's OWN template via the namespace edge
|
|
// (Counter-1: was the global last-wins map); a bare head selects the
|
|
// single bare-VISIBLE author.
|
|
if (headNameOfCallee(cl.callee)) |hn| {
|
|
switch (self.selectGenericStructHead(hn.name, hn.alias, hn.is_qualified, cl.callee.span)) {
|
|
.template => |t| return self.instantiateGenericStruct(&t, cl.args),
|
|
.poisoned => return .unresolved,
|
|
.not_generic => {},
|
|
}
|
|
}
|
|
return .unresolved;
|
|
},
|
|
.parameterized_type_expr => |pt| return self.resolveParameterizedWithBindings(&pt, te.span),
|
|
.identifier => |id| {
|
|
// E4 single-hop visibility + ambiguity gate: a 2-flat-hop bare type
|
|
// name in a typed array/vector-literal annotation (`Nums.[1, 2]`) is
|
|
// not bare-visible (consistent with annotations / 0763); ≥2 direct
|
|
// flat same-name authors are ambiguous (loud diagnostic, consistent
|
|
// with the leaf / 0755); a single source-keyed author resolves to
|
|
// ITS TypeId instead of a global `findByName` first-/last-wins pick.
|
|
switch (self.headTypeGate(id.name, te.span)) {
|
|
.ambiguous, .not_visible => return .unresolved,
|
|
.resolved => |tid| return tid,
|
|
.proceed => {},
|
|
}
|
|
const name_id = self.module.types.internString(id.name);
|
|
return self.module.types.findByName(name_id) orelse .unresolved;
|
|
},
|
|
.type_expr => |inner| {
|
|
if (self.headTypeLeak(inner.name, te.span)) return .unresolved;
|
|
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.
|
|
pub 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.
|
|
pub 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;
|
|
// Generic struct STATIC-METHOD head (`Box(s64).make(..)` or the
|
|
// qualified `a.Box(s64).make(..)`): the layout author is chosen
|
|
// by the single head choke-point (CP-1) and the method body by
|
|
// the instance's STAMPED author (CP-4), so layout-author ≡
|
|
// body-author for BOTH bare and qualified heads (E4 #1 / #2).
|
|
if (headNameOfCallee(inner_call.callee)) |hn| {
|
|
switch (self.selectGenericStructHead(hn.name, hn.alias, hn.is_qualified, inner_call.callee.span)) {
|
|
.poisoned => return Ref.none,
|
|
.template => |t| {
|
|
const inst_ty = self.instantiateGenericStruct(&t, inner_call.args);
|
|
const inst_name = self.formatTypeName(inst_ty);
|
|
if (self.genericInstanceMethod(inst_name, fa.field)) |gm| {
|
|
if (self.ensureGenericInstanceMethodLowered(gm)) |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);
|
|
}
|
|
}
|
|
},
|
|
.not_generic => {},
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
if (self.program_index.fn_ast_map.get(resolved)) |fd| {
|
|
if (fd.type_params.len > 0) {
|
|
if (self.headFnLeak(inner_name, inner_call.callee.span)) return Ref.none;
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
// Generic-struct instance method: select the body via the
|
|
// instance's STAMPED author (CP-4), so the dispatched method is
|
|
// the one authored alongside this instance's layout — never the
|
|
// global last-wins `fn_ast_map["Template.method"]`.
|
|
if (self.genericInstanceMethod(sname, fa.field)) |gm| {
|
|
if (self.ensureGenericInstanceMethodLowered(gm)) |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(gm.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.
|
|
pub 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).
|
|
pub 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.
|
|
pub 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 ──────────────────────────────────────
|
|
|
|
/// 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.
|
|
pub 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();
|
|
}
|
|
|
|
pub 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;
|
|
}
|
|
// E4 single-hop visibility gate: each element leaf is resolved through
|
|
// the source-aware resolver, so a 2-flat-hop inner leaf (`(COnly, s64)`)
|
|
// emits "not visible" + poisons rather than leaking through
|
|
// `type_bridge`'s ungated global lookup. A valid element resolves to the
|
|
// same TypeId the delegated build produces below (no diagnostic, no
|
|
// drift); only the poison short-circuits.
|
|
if (self.resolveTypeWithBindings(el.value) == .unresolved) 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;
|
|
}
|
|
// E4 single-hop visibility + ambiguity gate: a bare type name
|
|
// reachable only over 2+ flat hops is not bare-visible in a
|
|
// reflection / type-arg slot (consistent with normal annotations /
|
|
// 0763); ≥2 direct flat same-name authors are ambiguous (loud
|
|
// diagnostic, consistent with the leaf / 0755) instead of a global
|
|
// first-/last-wins pick; a single source-keyed author resolves to
|
|
// ITS TypeId. A genuinely-undeclared name is NOT authored as a type
|
|
// anywhere → `.proceed`, falling to the "unresolved type"
|
|
// diagnostic below.
|
|
switch (self.headTypeGate(id.name, node.span)) {
|
|
.ambiguous, .not_visible => return .unresolved,
|
|
.resolved => |tid| return tid,
|
|
.proceed => {},
|
|
}
|
|
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.headTypeLeak(te.name, node.span)) return .unresolved;
|
|
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);
|
|
},
|
|
// Wrapped / structural forms (`*T`, `[N]T`, `[]T`, `?T`, fn-ptr, tuple)
|
|
// route through the gated `resolveTypeWithBindings`, whose
|
|
// `resolveCompound` recurses each element through the source-aware leaf
|
|
// (`resolveNominalLeaf`) — so a 2-hop inner leaf (`*COnly`, `[2]COnly`,
|
|
// `(COnly, s64)`) is rejected exactly as in a normal annotation, instead
|
|
// of `type_bridge.resolveAstType`'s ungated global lookup (E4).
|
|
.tuple_literal,
|
|
.pointer_type_expr,
|
|
.many_pointer_type_expr,
|
|
.array_type_expr,
|
|
.slice_type_expr,
|
|
.optional_type_expr,
|
|
.function_type_expr,
|
|
=> return self.resolveTypeWithBindings(node),
|
|
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.
|
|
pub 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).
|
|
pub 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;
|
|
}
|
|
|
|
pub 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;
|
|
}
|
|
}
|
|
// Generic-struct instance method param types: select the method
|
|
// body via the instance's STAMPED author (CP-4), substituting the
|
|
// instance's bindings so `T → concrete`. The param source-pin
|
|
// follows the selected `fd` (its own `body.source_file`).
|
|
if (self.genericInstanceMethod(sname, fa.field)) |gm| {
|
|
if (gm.fd.params.len > 0) {
|
|
const saved_bindings = self.type_bindings;
|
|
self.type_bindings = gm.bindings.*;
|
|
var types_list = std.ArrayList(TypeId).empty;
|
|
for (gm.fd.params[1..]) |p| {
|
|
types_list.append(self.alloc, self.resolveParamTypeInSource(gm.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.
|
|
pub 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.
|
|
pub fn isPlainFreeFn(fd: *const ast.FnDecl) bool {
|
|
if (fd.type_params.len > 0) return false;
|
|
return switch (fd.body.data) {
|
|
.foreign_expr, .builtin_expr, .compiler_expr => false,
|
|
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).
|
|
pub 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);
|
|
}
|
|
|
|
pub 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,
|
|
};
|
|
}
|
|
|
|
pub 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`).
|
|
pub 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 {
|
|
return self.foldSourceConstFloat(name, null);
|
|
}
|
|
|
|
/// 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 {
|
|
return self.sourceConstIsFloatTyped(name, null);
|
|
}
|
|
|
|
/// 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 });
|
|
}
|
|
|
|
/// The poison-vs-proceed projection of `headTypeGate` for an UNQUALIFIED
|
|
/// parameterized type HEAD that names a generic STRUCT, a parameterized
|
|
/// PROTOCOL, or a type-returning function used as a head (`Box(s64)`,
|
|
/// `VL(s64)`) — and the alias-registration / type-match sites that likewise
|
|
/// only need "poison or proceed". Returns TRUE (the gate's loud diagnostic is
|
|
/// already emitted) when the head is `.not_visible` (a 2-flat-hop leak) or
|
|
/// `.ambiguous` (≥2 direct flat same-name authors — consistent with the leaf /
|
|
/// 0755); FALSE when it resolves or falls open. See `headTypeGate` for the full
|
|
/// non-transitive visibility + ambiguity model and the fall-open conditions.
|
|
fn headTypeLeak(self: *Lowering, name: []const u8, span: ?ast.Span) bool {
|
|
// A head site INSTANTIATES (template / type-fn) rather than substituting a
|
|
// nominal TypeId, so it consumes only the poison-vs-proceed bit of the
|
|
// full author outcome: `.ambiguous` / `.not_visible` (loud diagnostic
|
|
// already emitted by `headTypeGate`) poison; `.resolved` / `.proceed`
|
|
// proceed to instantiation.
|
|
return switch (self.headTypeGate(name, span)) {
|
|
.ambiguous, .not_visible => true,
|
|
.proceed, .resolved => false,
|
|
};
|
|
}
|
|
|
|
/// Control-flow outcome of the generic-struct LAYOUT-head selector. Carries no
|
|
/// diagnostic for the caller to emit — `selectGenericStructHead` emits inline.
|
|
const HeadTemplate = union(enum) {
|
|
template: StructTemplate, // visible bare author OR qualified author → instantiate
|
|
poisoned, // gate already diagnosed → caller returns .unresolved / Ref.none
|
|
not_generic, // name is not a generic struct head → caller's non-struct path
|
|
};
|
|
|
|
/// THE single selector every generic-struct LAYOUT-head site funnels through —
|
|
/// no head site reads `struct_template_map` for selection directly. Decides the
|
|
/// authoring template for a head named `name`, qualified by namespace `alias`
|
|
/// (non-null only for `ns.Box(..)` with an identifier object) and flagged
|
|
/// `is_qualified` (any `.field_access` callee, including a non-identifier
|
|
/// object). Emits the visibility / missing-member diagnostics INLINE at `span`,
|
|
/// at the same program point and ordering the sites used before (0767/0769/0775),
|
|
/// and returns a control-flow-only outcome:
|
|
/// - qualified, namespace authors `name` as a generic struct → that author.
|
|
/// - qualified, namespace exists but lacks `name` → diagnose missing member,
|
|
/// `.poisoned` (never the bare global map, E4 #2).
|
|
/// - qualified, namespace authors `name` but NOT as a generic struct (a
|
|
/// type-fn / named type) → `.not_generic` (caller's non-struct path).
|
|
/// - qualified with no usable alias (nested-ns object) → the global template
|
|
/// if one exists (pre-existing behavior; no namespace edge to consult).
|
|
/// - bare, ≥2 visible authors / 2-flat-hop only → `headTypeLeak` diagnosed →
|
|
/// `.poisoned`.
|
|
/// - bare, single visible author → that author (own / 1-hop flat), source-keyed.
|
|
/// - bare, visible author IS the canonical map author → the global template
|
|
/// (byte-identical single-author path).
|
|
/// - not in `struct_template_map` at all → `.not_generic`.
|
|
pub fn selectGenericStructHead(self: *Lowering, name: []const u8, alias: ?[]const u8, is_qualified: bool, span: ?ast.Span) HeadTemplate {
|
|
if (is_qualified) {
|
|
if (alias) |a| {
|
|
if (self.qualifiedStructTemplate(a, name)) |tmpl| return .{ .template = tmpl };
|
|
if (self.qualifiedMemberMissing(a, name)) {
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, span, "namespace '{s}' has no member '{s}'", .{ a, name });
|
|
return .poisoned;
|
|
}
|
|
return .not_generic;
|
|
}
|
|
// Qualified but un-aliasable object (nested namespace / non-identifier):
|
|
// no namespace edge to select from — use the global template if present.
|
|
if (self.program_index.struct_template_map.getPtr(name)) |tmpl| return .{ .template = tmpl.* };
|
|
return .not_generic;
|
|
}
|
|
if (self.program_index.struct_template_map.getPtr(name)) |tmpl| {
|
|
if (self.headTypeLeak(name, span)) return .poisoned;
|
|
if (self.bareVisibleStructTemplate(name)) |vt| return .{ .template = vt };
|
|
return .{ .template = tmpl.* };
|
|
}
|
|
return .not_generic;
|
|
}
|
|
|
|
/// Decompose a head callee NODE (`.identifier Box` or `.field_access ns.Box`)
|
|
/// into the `(name, alias, is_qualified)` triple `selectGenericStructHead`
|
|
/// consumes. `alias` is the namespace identifier only for a `.field_access`
|
|
/// whose object is a plain identifier; a nested / non-identifier object is
|
|
/// qualified-but-unaliased.
|
|
const HeadName = struct { name: []const u8, alias: ?[]const u8, is_qualified: bool };
|
|
fn headNameOfCallee(callee: *const Node) ?HeadName {
|
|
return switch (callee.data) {
|
|
.identifier => |id| .{ .name = id.name, .alias = null, .is_qualified = false },
|
|
.field_access => |fa| .{
|
|
.name = fa.field,
|
|
.alias = if (fa.object.data == .identifier) fa.object.data.identifier.name else null,
|
|
.is_qualified = true,
|
|
},
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
/// The complete source-aware author outcome of an UNQUALIFIED bare TYPE head —
|
|
/// the unified non-transitive visibility + ambiguity gate every bare-type-
|
|
/// reference site OUTSIDE the nominal leaf routes through (E4 attempt-5):
|
|
/// reflection / type-arg slots, typed array/vector-literal heads, parameterized
|
|
/// generic / protocol / type-fn heads, type-as-value, and type-category match
|
|
/// arms. Mirrors `selectNominalLeaf`'s author model so a 2-flat-hop type is
|
|
/// `.not_visible`, ≥2 direct flat same-name authors are `.ambiguous` (the LOUD
|
|
/// diagnostic, consistent with the leaf / 0755 — never a silent global
|
|
/// `findByName` / `struct_template_map` first-/last-wins pick), and a single
|
|
/// direct flat author resolves to ITS source-keyed TypeId. Falls open
|
|
/// (`.proceed`) when import facts are unwired, the source context is absent,
|
|
/// the default-Context emitter is running (built-in infrastructure resolves
|
|
/// independent of the user's import style, F1), the querying source is the OWN
|
|
/// author, a single flat author is not registered yet (a forward / foreign /
|
|
/// generic template — the caller instantiates it), or `name` is a block-local
|
|
/// of this source / no type author at all. Library-internal heads stay visible
|
|
/// because every instantiation kind is source-pinned to the template's defining
|
|
/// module (E3/E4 #1): the query originates THERE, where the head is a direct
|
|
/// flat import. A namespaced `ns.Box(..)` head is an explicit qualified reach
|
|
/// and is exempt (the caller skips this gate).
|
|
const HeadTypeGate = union(enum) {
|
|
proceed,
|
|
resolved: TypeId,
|
|
ambiguous,
|
|
not_visible,
|
|
};
|
|
pub fn headTypeGate(self: *Lowering, name: []const u8, span: ?ast.Span) HeadTypeGate {
|
|
if (self.emitting_default_context) return .proceed;
|
|
if (self.program_index.module_decls == null or self.program_index.flat_import_graph == null) return .proceed;
|
|
const from = self.current_source_file orelse return .proceed;
|
|
|
|
var res_walk = self.resolver();
|
|
const author_set = res_walk.collectVisibleAuthors(name, from, .user_bare_flat);
|
|
defer if (author_set.flat.len > 0) self.alloc.free(author_set.flat);
|
|
|
|
// Own author wins outright (own-wins, 0754). Pending / unregistered → .proceed.
|
|
if (author_set.own) |own| switch (own.raw) {
|
|
.const_decl => {
|
|
if (self.program_index.type_aliases_by_source.get(own.source)) |inner| {
|
|
if (inner.get(name)) |tid| return .{ .resolved = tid };
|
|
}
|
|
return .proceed;
|
|
},
|
|
else => if (isNamedTypeKind(own.raw)) {
|
|
if (self.namedRefTid(own.raw, name)) |tid| return .{ .resolved = tid };
|
|
return .proceed;
|
|
},
|
|
};
|
|
|
|
// Flat type authors
|
|
var flat_type_count: usize = 0;
|
|
var found_tid: ?TypeId = null;
|
|
var flat_tid_count: usize = 0;
|
|
for (author_set.flat) |fa| {
|
|
const is_type = switch (fa.raw) {
|
|
.const_decl => blk: {
|
|
if (self.program_index.type_aliases_by_source.get(fa.source)) |inner|
|
|
break :blk inner.contains(name);
|
|
break :blk false;
|
|
},
|
|
else => isNamedTypeKind(fa.raw),
|
|
};
|
|
if (!is_type) continue;
|
|
flat_type_count += 1;
|
|
const fa_tid: ?TypeId = switch (fa.raw) {
|
|
.const_decl => blk: {
|
|
if (self.program_index.type_aliases_by_source.get(fa.source)) |inner|
|
|
break :blk inner.get(name);
|
|
break :blk null;
|
|
},
|
|
else => self.namedRefTid(fa.raw, name),
|
|
};
|
|
if (fa_tid) |t| {
|
|
flat_tid_count += 1;
|
|
if (found_tid) |f| { if (t != f) {
|
|
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 .ambiguous;
|
|
} } else found_tid = t;
|
|
}
|
|
}
|
|
if (flat_type_count > 0) {
|
|
// ≥2 authors but not all resolved to one TypeId → ambiguous
|
|
if (flat_type_count >= 2 and !(flat_tid_count == flat_type_count and found_tid != null)) {
|
|
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 .ambiguous;
|
|
}
|
|
if (found_tid) |t| return .{ .resolved = t };
|
|
return .proceed; // single author exists but TypeId not registered
|
|
}
|
|
|
|
if (self.localTypeInSource(from, name)) return .proceed;
|
|
if (!self.nameAuthoredAsTypeAnywhere(name)) return .proceed;
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, span, "type '{s}' is not visible; #import the module that declares it", .{name});
|
|
return .not_visible;
|
|
}
|
|
|
|
/// Single-hop non-transitive visibility + ambiguity gate for an UNQUALIFIED
|
|
/// type-returning FUNCTION head used as a type (`Make(N, T)` where
|
|
/// `Make :: ($K, $T) -> Type`). A type-fn is a `fn_decl`, so visibility is
|
|
/// decided from the ELIGIBLE FUNCTION authors directly reachable from the use
|
|
/// site (`flatFnAuthorVisible`) — NOT the module-scope NAME predicate
|
|
/// (`isNameVisible`), which a same-name NON-function (a value const, a named
|
|
/// type) would wrongly vouch for. Returns TRUE (loud diagnostic already
|
|
/// emitted) when the head is AMBIGUOUS (≥2 distinct direct flat same-name
|
|
/// type-fn authors, no own author — consistent with the parameterized struct /
|
|
/// protocol heads and the leaf, 0755/0767, never a silent `fn_ast_map`
|
|
/// first-/last-wins pick) or NOT-VISIBLE (its only directly-visible same-name
|
|
/// author is a non-function and the real type-fn author is ≥2 flat hops away).
|
|
/// A scope-local (mangled) type-fn or the querying source's OWN function author
|
|
/// wins outright (own-wins) and is exempt; falls open when unwired /
|
|
/// default-context. Diagnostic mirrors the type form (the head IS used as a type
|
|
/// here).
|
|
pub fn headFnLeak(self: *Lowering, name: []const u8, span: ?ast.Span) bool {
|
|
if (self.emitting_default_context) return false;
|
|
const from = self.current_source_file orelse return false;
|
|
if (self.scope) |s| if (s.lookupFn(name) != null) return false;
|
|
// Fall open when the import facts aren't wired (comptime callers,
|
|
// directory imports without a main file): the author collector would
|
|
// otherwise return an empty set and wrongly report a genuinely-visible
|
|
// type-fn as not-visible. Mirrors `headTypeGate`'s guard.
|
|
if (self.program_index.module_decls == null or self.program_index.flat_import_graph == null) return false;
|
|
// ≥2 distinct direct flat type-fn authors with no own author — a genuine
|
|
// collision the source cannot disambiguate. Diagnose loudly BEFORE the
|
|
// visibility short-circuit, which would otherwise let the single
|
|
// `fn_ast_map[name]` author silently win.
|
|
if (self.flatFnAuthorAmbiguous(name, from)) {
|
|
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 true;
|
|
}
|
|
// KIND-AWARE: visible iff a directly-reachable (own or 1-hop flat) author
|
|
// is itself a TYPE-FUNCTION. A same-name 1-hop non-function (attempt-7) OR
|
|
// ordinary non-type function (attempt-8) does NOT vouch for a type-fn head
|
|
// whose real author is 2 flat hops away.
|
|
if (self.flatFnAuthorVisible(name, from)) return false;
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, span, "type '{s}' is not visible; #import the module that declares it", .{name});
|
|
return true;
|
|
}
|
|
|
|
/// TRUE iff bare `name` has ≥2 DISTINCT direct flat-import authors that are
|
|
/// TYPE-FUNCTIONS (`typeFnAuthor`: a `fn_decl` with ≥1 `$`-param — an ordinary
|
|
/// same-name function does not count) and the querying source authors NONE
|
|
/// itself. The querying source's OWN
|
|
/// author wins outright (own-wins), so an own author short-circuits to "not
|
|
/// ambiguous" — the existing single-author path instantiates it. Diamond
|
|
/// imports of the SAME author collapse in `collectVisibleAuthors`'s
|
|
/// author-identity de-dup, so two edges onto one type-fn are NOT ambiguous. The
|
|
/// type-fn ambiguity analogue of `flatTypeAuthorCount`'s `.ambiguous` for named
|
|
/// type / template heads.
|
|
fn flatFnAuthorAmbiguous(self: *Lowering, name: []const u8, from: []const u8) bool {
|
|
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 != null) return false; // own-wins
|
|
var fn_authors: usize = 0;
|
|
for (set.flat) |fa| {
|
|
if (typeFnAuthor(fa.raw)) fn_authors += 1;
|
|
}
|
|
return fn_authors >= 2;
|
|
}
|
|
|
|
/// TRUE iff bare `name` has at least one DIRECTLY-visible author — the
|
|
/// querying source's OWN author or a 1-hop flat-import author — that is a
|
|
/// TYPE-FUNCTION (`typeFnAuthor`: a `fn_decl` with ≥1 `$`-param). The KIND-AWARE
|
|
/// analogue of `isNameVisible` for a type-fn head: a same-name 1-hop
|
|
/// NON-function (a value const `Make :: 123`, a named type) does NOT vouch
|
|
/// (attempt-7), and — crucially — neither does a same-name 1-hop ORDINARY
|
|
/// function (`Make :: () -> s32`, zero `$`-params), which cannot be the type
|
|
/// head being instantiated (attempt-8). So a type-fn whose only directly-
|
|
/// visible same-name author is a non-fn OR a non-type-fn — its real author 2
|
|
/// flat hops away — is correctly invisible. Mirrors `flatFnAuthorAmbiguous`'s
|
|
/// type-fn-only author view.
|
|
fn flatFnAuthorVisible(self: *Lowering, name: []const u8, from: []const u8) bool {
|
|
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) |own| {
|
|
if (typeFnAuthor(own.raw)) return true;
|
|
}
|
|
for (set.flat) |fa| {
|
|
if (typeFnAuthor(fa.raw)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Resolve a .call node that represents a type constructor (e.g., List(T), Vector(N, T)).
|
|
pub fn resolveTypeCallWithBindings(self: *Lowering, cl: *const ast.Call) TypeId {
|
|
// A namespaced callee (`ns.Box(..)`) is an explicit qualified reach and is
|
|
// exempt from the bare-head visibility gate; only a plain identifier head
|
|
// is policed (E4).
|
|
const is_qualified = cl.callee.data == .field_access;
|
|
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);
|
|
}
|
|
// Generic-struct head: route through the single layout choke-point (CP-1).
|
|
// Bare → the single bare-VISIBLE author (own / 1-hop flat), source-keyed;
|
|
// qualified `ns.Box(..)` → ns's OWN template (or a missing-member diagnostic);
|
|
// never the global last-wins map for a visible-shadowed or qualified head.
|
|
if (headNameOfCallee(cl.callee)) |hn| {
|
|
switch (self.selectGenericStructHead(hn.name, hn.alias, hn.is_qualified, cl.callee.span)) {
|
|
.template => |t| return self.instantiateGenericStruct(&t, cl.args),
|
|
.poisoned => return .unresolved,
|
|
.not_generic => {},
|
|
}
|
|
}
|
|
// 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 (!is_qualified and self.headFnLeak(callee_name, cl.callee.span)) return .unresolved;
|
|
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;
|
|
// A namespaced base (`ns.Box(..)`) is an explicit qualified reach and is
|
|
// exempt from the bare-head visibility gate; only a dotless head is
|
|
// policed (E4).
|
|
const is_qualified = std.mem.indexOfScalar(u8, pt.name, '.') != null;
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
// Generic-struct base: route through the single layout choke-point (CP-1).
|
|
// Bare → the single bare-VISIBLE author (own / 1-hop flat), source-keyed;
|
|
// qualified `ns.Box(..)` → ns's OWN template (or a missing-member diagnostic);
|
|
// never the global last-wins map for a visible-shadowed or qualified head.
|
|
{
|
|
const alias: ?[]const u8 = if (std.mem.indexOfScalar(u8, pt.name, '.')) |dot| pt.name[0..dot] else null;
|
|
switch (self.selectGenericStructHead(base_name, alias, is_qualified, span)) {
|
|
.template => |t| return self.instantiateGenericStruct(&t, pt.args),
|
|
.poisoned => return .unresolved,
|
|
.not_generic => {},
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
if (!is_qualified and self.headTypeLeak(base_name, span)) return .unresolved;
|
|
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 (!is_qualified and self.headFnLeak(base_name, span)) return .unresolved;
|
|
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) }
|
|
/// A generic-struct instance method selected via the STAMPED authoring decl:
|
|
/// the `fn_decl` to monomorphize, the instance's stored type bindings, and the
|
|
/// instance (mangled / alias) name the monomorphized function is keyed under.
|
|
const GenericStructMethod = struct {
|
|
fd: *const ast.FnDecl,
|
|
bindings: *std.StringHashMap(TypeId),
|
|
inst_name: []const u8,
|
|
};
|
|
|
|
/// THE single body-axis reader: select `method` of generic-struct instance
|
|
/// `inst_name` via the instance's STAMPED author (`struct_instance_author`),
|
|
/// so body-author ≡ layout-author by construction — never the global last-wins
|
|
/// `fn_ast_map["Template.method"]` a 2-flat-hop same-name template's method
|
|
/// could win. Null when `inst_name` is NOT a generic instance (no author stamp)
|
|
/// — the caller's existing non-generic `fn_ast_map` path then handles it
|
|
/// (non-generic structs, free fns, FFI), or when the confirmed author declares
|
|
/// no such `method` (a normal unresolved-method, handled downstream). A
|
|
/// confirmed instance whose author is present but whose bindings are missing is
|
|
/// a LOUD invariant failure — instantiation writes both together (CP-2).
|
|
pub fn genericInstanceMethod(self: *Lowering, inst_name: []const u8, method: []const u8) ?GenericStructMethod {
|
|
const author = self.struct_instance_author.get(inst_name) orelse return null;
|
|
const bindings = self.struct_instance_bindings.getPtr(inst_name) orelse
|
|
std.debug.panic("generic struct instance '{s}' has an author but no bindings", .{inst_name});
|
|
// INLINE struct method (`Box :: struct { make :: ... }`): selected via the
|
|
// instance's STAMPED author, so the body is the one authored alongside the
|
|
// layout — never the global last-wins `fn_ast_map["Template.method"]` a
|
|
// 2-flat-hop same-name template's method could win (finding #1).
|
|
if (structMethodFn(author, method)) |fd|
|
|
return .{ .fd = fd, .bindings = bindings, .inst_name = inst_name };
|
|
// IMPL-block method (`impl P for Box { ... }`): registered under the
|
|
// template name in `fn_ast_map`, not on the struct decl, so it is keyed by
|
|
// template name (protocol dispatch). The author confirms this IS a generic
|
|
// instance; the method body is the template's registered impl method.
|
|
const tmpl_name = self.struct_instance_template.get(inst_name) orelse return null;
|
|
const tmpl_qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ tmpl_name, method }) catch return null;
|
|
if (self.program_index.fn_ast_map.get(tmpl_qualified)) |fd|
|
|
return .{ .fd = fd, .bindings = bindings, .inst_name = inst_name };
|
|
return null;
|
|
}
|
|
|
|
/// Monomorphize (once) the selected generic-instance method under
|
|
/// `<inst_name>.<method>` and return its FuncId. The source-pin follows the
|
|
/// selected `fd` for free: `monomorphizeFunction` pins to `fd.body.source_file`,
|
|
/// which is the template's defining module (the author's own method node).
|
|
/// Null when the function fails to resolve post-monomorphization.
|
|
fn ensureGenericInstanceMethodLowered(self: *Lowering, m: GenericStructMethod) ?FuncId {
|
|
const mangled = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ m.inst_name, m.fd.name }) catch return null;
|
|
if (!self.lowered_functions.contains(mangled)) {
|
|
self.monomorphizeFunction(m.fd, mangled, m.bindings);
|
|
}
|
|
return self.resolveFuncByName(mangled);
|
|
}
|
|
|
|
/// Debug invariant (CP coverage lock): the two generic-instance maps written
|
|
/// in lockstep at the SAME two writers (instantiation + alias copy) —
|
|
/// `struct_instance_template` and `struct_instance_author` — must have
|
|
/// coincident keysets. A future writer that registers an instance's layout
|
|
/// without stamping its author (a silent body-axis reopen) trips this in a
|
|
/// debug `zig build test`, not in production.
|
|
pub fn assertInstanceMapsCoincide(self: *Lowering) void {
|
|
if (!std.debug.runtime_safety) return;
|
|
var it = self.struct_instance_template.keyIterator();
|
|
while (it.next()) |k| {
|
|
if (!self.struct_instance_author.contains(k.*))
|
|
std.debug.panic("generic instance '{s}' has a template but no author stamp", .{k.*});
|
|
}
|
|
var it2 = self.struct_instance_author.keyIterator();
|
|
while (it2.next()) |k| {
|
|
if (!self.struct_instance_template.contains(k.*))
|
|
std.debug.panic("generic instance '{s}' has an author but no template stamp", .{k.*});
|
|
}
|
|
}
|
|
|
|
pub 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 {};
|
|
|
|
// A qualified `ns.Box(..)` head can select a generic template whose bare
|
|
// name also belongs to a DIFFERENT module's same-name template (the one
|
|
// that won the last-wins `struct_template_map`). Both would mangle to
|
|
// `Box__s64` and the second instantiation would alias the first's layout.
|
|
// Tag the NON-canonical author's mangled name with its source so each
|
|
// author's instantiation is a distinct type. The canonical (bare-map)
|
|
// author keeps the untagged name — no churn for single-author generics.
|
|
if (self.program_index.struct_template_map.get(tmpl.name)) |canon| {
|
|
const canon_src = canon.source_file orelse "";
|
|
const this_src = tmpl.source_file orelse "";
|
|
if (!std.mem.eql(u8, canon_src, this_src)) {
|
|
var tag_buf: [24]u8 = undefined;
|
|
const tag = std.fmt.bufPrint(&tag_buf, "$m{x}", .{std.hash.Wyhash.hash(0, this_src)}) catch "";
|
|
name_parts.appendSlice(self.alloc, tag) 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) {
|
|
// A confirmed generic instance must never be returned without an
|
|
// author stamp — the body axis (CP-4) keys method selection off
|
|
// it. The template/bindings were written at first instantiation;
|
|
// re-stamp the author from THIS `tmpl` if the dedup fast-path is
|
|
// the first to reach this mangled name (e.g. a layout interned by
|
|
// a forward reference before any method dispatch).
|
|
if (!self.struct_instance_author.contains(mangled_name)) {
|
|
const owned = self.alloc.dupe(u8, mangled_name) catch return existing;
|
|
self.struct_instance_author.put(owned, tmpl.decl) catch {};
|
|
}
|
|
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, template name, and authoring decl for method
|
|
// resolution. The author is stamped from the SAME `tmpl` that built the
|
|
// layout above, so the body axis (CP-4) selects this instance's methods
|
|
// via the layout author — never the global last-wins `fn_ast_map`.
|
|
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 {};
|
|
self.struct_instance_author.put(owned_mangled, tmpl.decl) 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.
|
|
pub fn instantiateTypeFunction(self: *Lowering, alias_name: []const u8, template_name: []const u8, fd: *const ast.FnDecl, args: []const *const Node) ?TypeId {
|
|
const table = &self.module.types;
|
|
|
|
// Build type bindings from params + args
|
|
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 ───────────────────────────────────────────
|
|
|
|
/// 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).
|
|
pub fn registerForeignClassDecl(self: *Lowering, fcd: *const ast.ForeignClassDecl) void {
|
|
self.program_index.foreign_class_map.put(fcd.name, fcd) catch {};
|
|
if (!fcd.is_foreign and fcd.runtime == .objc_class) {
|
|
if (self.module.lookupObjcDefinedClass(fcd.name) == null) {
|
|
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.
|
|
pub fn lookupObjcDefinedClassForMethod(self: *Lowering, name: []const u8) ?*const ast.ForeignClassDecl {
|
|
const dot = std.mem.indexOf(u8, name, ".") orelse return null;
|
|
return self.module.lookupObjcDefinedClass(name[0..dot]);
|
|
}
|
|
|
|
/// 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.
|
|
pub 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.? };
|
|
}
|
|
|
|
/// 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.
|
|
pub fn registerNamespacedForeignClasses(self: *Lowering, ns: ast.NamespaceDecl) void {
|
|
for (ns.decls) |inner| {
|
|
if (inner.data == .foreign_class_decl) {
|
|
const fcd = &inner.data.foreign_class_decl;
|
|
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 ──────────────────────────────────────────
|
|
|
|
/// 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).
|
|
pub 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);
|
|
}
|
|
|
|
pub 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.
|
|
pub 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.
|
|
pub 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),
|
|
};
|
|
}
|
|
|
|
/// 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.
|
|
pub 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.
|
|
pub fn stampCallerSource(self: *Lowering, node: *Node) void {
|
|
if (node.source_file != null) return;
|
|
if (self.current_source_file) |src| node.source_file = src;
|
|
}
|
|
|
|
pub 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);
|
|
}
|
|
|
|
pub 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.
|
|
pub 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) });
|
|
}
|
|
|
|
/// 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.
|
|
pub 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.)
|
|
pub 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,
|
|
};
|
|
}
|
|
|
|
/// 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").
|
|
pub 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);
|
|
}
|
|
}
|
|
|
|
/// 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).
|
|
pub 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).
|
|
pub 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:`.
|
|
pub 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;
|
|
}
|
|
|
|
pub 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();
|
|
}
|
|
|
|
// --- moved to lower/error.zig (lower_error) ---
|
|
pub const getTraceFids = lower_error.getTraceFids;
|
|
pub const tracesEnabled = lower_error.tracesEnabled;
|
|
pub const emitTracePush = lower_error.emitTracePush;
|
|
pub const emitTraceClear = lower_error.emitTraceClear;
|
|
pub const placeholderTraceFrame = lower_error.placeholderTraceFrame;
|
|
pub const errorSetTypeOf = lower_error.errorSetTypeOf;
|
|
pub const isErrorTagLiteralNode = lower_error.isErrorTagLiteralNode;
|
|
pub const tryLowerErrorSetEquality = lower_error.tryLowerErrorSetEquality;
|
|
pub const effectiveReturnType = lower_error.effectiveReturnType;
|
|
pub const errorChannelOf = lower_error.errorChannelOf;
|
|
pub const isInferredErrorSet = lower_error.isInferredErrorSet;
|
|
pub const checkErrorSetSubset = lower_error.checkErrorSetSubset;
|
|
pub const diagTagsNotInSet = lower_error.diagTagsNotInSet;
|
|
pub const lowerRaise = lower_error.lowerRaise;
|
|
pub const lowerFailableSuccessReturn = lower_error.lowerFailableSuccessReturn;
|
|
pub const buildFailableTuple = lower_error.buildFailableTuple;
|
|
pub const failableSuccessType = lower_error.failableSuccessType;
|
|
pub const failableReturnTarget = lower_error.failableReturnTarget;
|
|
pub const extractSuccessValue = lower_error.extractSuccessValue;
|
|
pub const extractErrorSlot = lower_error.extractErrorSlot;
|
|
pub const emitTupleRet = lower_error.emitTupleRet;
|
|
pub const diagRaiseNotFailable = lower_error.diagRaiseNotFailable;
|
|
pub const exprIsFailable = lower_error.exprIsFailable;
|
|
pub const lowerCallerLocation = lower_error.lowerCallerLocation;
|
|
pub const sourceForFile = lower_error.sourceForFile;
|
|
pub const currentFunctionName = lower_error.currentFunctionName;
|
|
pub const lowerTry = lower_error.lowerTry;
|
|
pub const emitErrorReturn = lower_error.emitErrorReturn;
|
|
pub const diagTryNotFailable = lower_error.diagTryNotFailable;
|
|
pub const lowerCatch = lower_error.lowerCatch;
|
|
pub const lowerCatchOverChain = lower_error.lowerCatchOverChain;
|
|
pub const finishCatchHandler = lower_error.finishCatchHandler;
|
|
pub const runCatchBody = lower_error.runCatchBody;
|
|
pub const checkEscapeWidening = lower_error.checkEscapeWidening;
|
|
pub const orIsFailableChain = lower_error.orIsFailableChain;
|
|
pub const operandIsFailableLike = lower_error.operandIsFailableLike;
|
|
pub const orChainSuccessType = lower_error.orChainSuccessType;
|
|
pub const unwrapTryNode = lower_error.unwrapTryNode;
|
|
pub const flattenOrChain = lower_error.flattenOrChain;
|
|
pub const lowerFailableOr = lower_error.lowerFailableOr;
|
|
pub const callTargetName = lower_error.callTargetName;
|
|
pub const astIsPureBareInferred = lower_error.astIsPureBareInferred;
|
|
pub const astPureNamedSet = lower_error.astPureNamedSet;
|
|
pub const namedSetTags = lower_error.namedSetTags;
|
|
pub const convergeInferredErrorSets = lower_error.convergeInferredErrorSets;
|
|
pub const containsTag = lower_error.containsTag;
|
|
pub const convergeClosureShapeSets = lower_error.convergeClosureShapeSets;
|
|
pub const recordClosureShape = lower_error.recordClosureShape;
|
|
pub const calleeEscapeTags = lower_error.calleeEscapeTags;
|
|
pub const unionShapeTags = lower_error.unionShapeTags;
|
|
pub const closureShapeKey = lower_error.closureShapeKey;
|
|
pub const returnValuePart = lower_error.returnValuePart;
|
|
pub const shapeKeyOfCallee = lower_error.shapeKeyOfCallee;
|
|
|
|
// --- moved to lower/comptime.zig (lower_comptime) ---
|
|
pub const SelectedConst = lower_comptime.SelectedConst;
|
|
pub const evalComptimeCondition = lower_comptime.evalComptimeCondition;
|
|
pub const evalComptimeMatch = lower_comptime.evalComptimeMatch;
|
|
pub const evalComptimeInt = lower_comptime.evalComptimeInt;
|
|
pub const evalComptimeString = lower_comptime.evalComptimeString;
|
|
pub const lowerComptimeGlobal = lower_comptime.lowerComptimeGlobal;
|
|
pub const lowerComptimeSideEffect = lower_comptime.lowerComptimeSideEffect;
|
|
pub const lowerComptimeCall = lower_comptime.lowerComptimeCall;
|
|
pub const lowerInlineComptime = lower_comptime.lowerInlineComptime;
|
|
pub const lowerInsertExpr = lower_comptime.lowerInsertExpr;
|
|
pub const lowerInsertExprValue = lower_comptime.lowerInsertExprValue;
|
|
pub const lowerComptimeDeps = lower_comptime.lowerComptimeDeps;
|
|
pub const substituteComptimeNodes = lower_comptime.substituteComptimeNodes;
|
|
pub const fnBodyHasReturn = lower_comptime.fnBodyHasReturn;
|
|
pub const createComptimeFunction = lower_comptime.createComptimeFunction;
|
|
pub const constExprValue = lower_comptime.constExprValue;
|
|
pub const constArrayLiteral = lower_comptime.constArrayLiteral;
|
|
pub const constStructLiteral = lower_comptime.constStructLiteral;
|
|
pub const constEnumLiteral = lower_comptime.constEnumLiteral;
|
|
pub const foldSourceConstInt = lower_comptime.foldSourceConstInt;
|
|
pub const foldSourceConstFloat = lower_comptime.foldSourceConstFloat;
|
|
pub const sourceConstIsFloatTyped = lower_comptime.sourceConstIsFloatTyped;
|
|
pub const comptimeIntNamed = lower_comptime.comptimeIntNamed;
|
|
pub const selectModuleConst = lower_comptime.selectModuleConst;
|
|
pub const sourceModuleConst = lower_comptime.sourceModuleConst;
|
|
pub const pinConstAuthorSource = lower_comptime.pinConstAuthorSource;
|
|
pub const foldComptimeFloatInit = lower_comptime.foldComptimeFloatInit;
|
|
|
|
// --- moved to lower/stmt.zig (lower_stmt) ---
|
|
pub const lowerBlock = lower_stmt.lowerBlock;
|
|
pub const lowerInlineBranch = lower_stmt.lowerInlineBranch;
|
|
pub const lowerBlockValue = lower_stmt.lowerBlockValue;
|
|
pub const lowerValueBody = lower_stmt.lowerValueBody;
|
|
pub const tryLowerAsExpr = lower_stmt.tryLowerAsExpr;
|
|
pub const lowerStmt = lower_stmt.lowerStmt;
|
|
pub const lowerVarDecl = lower_stmt.lowerVarDecl;
|
|
pub const lowerLocalFnDecl = lower_stmt.lowerLocalFnDecl;
|
|
pub const lowerConstDecl = lower_stmt.lowerConstDecl;
|
|
pub const lowerReturn = lower_stmt.lowerReturn;
|
|
pub const lowerAssignment = lower_stmt.lowerAssignment;
|
|
pub const fieldLvaluePtr = lower_stmt.fieldLvaluePtr;
|
|
pub const lowerExprAsPtr = lower_stmt.lowerExprAsPtr;
|
|
pub const storeOrCompound = lower_stmt.storeOrCompound;
|
|
pub const emitCompoundOp = lower_stmt.emitCompoundOp;
|
|
pub const lowerMultiAssign = lower_stmt.lowerMultiAssign;
|
|
pub const lowerDestructureDecl = lower_stmt.lowerDestructureDecl;
|
|
pub const lowerPush = lower_stmt.lowerPush;
|
|
pub const lowerDefer = lower_stmt.lowerDefer;
|
|
pub const lowerOnFail = lower_stmt.lowerOnFail;
|
|
pub const diagOnFailNotFailable = lower_stmt.diagOnFailNotFailable;
|
|
pub const emitBlockDefers = lower_stmt.emitBlockDefers;
|
|
pub const lowerCleanupBody = lower_stmt.lowerCleanupBody;
|
|
pub const emitErrorCleanup = lower_stmt.emitErrorCleanup;
|
|
|
|
// --- moved to lower/control_flow.zig (lower_control_flow) ---
|
|
pub const lowerIfExpr = lower_control_flow.lowerIfExpr;
|
|
pub const tryConstBoolCondition = lower_control_flow.tryConstBoolCondition;
|
|
pub const lowerWhile = lower_control_flow.lowerWhile;
|
|
pub const listView = lower_control_flow.listView;
|
|
pub const lowerFor = lower_control_flow.lowerFor;
|
|
pub const lowerRuntimeRangeFor = lower_control_flow.lowerRuntimeRangeFor;
|
|
pub const lowerInlineRangeFor = lower_control_flow.lowerInlineRangeFor;
|
|
pub const lowerMatch = lower_control_flow.lowerMatch;
|
|
pub const lowerBreak = lower_control_flow.lowerBreak;
|
|
pub const lowerContinue = lower_control_flow.lowerContinue;
|
|
pub const freshBlock = lower_control_flow.freshBlock;
|
|
pub const freshBlockWithParams = lower_control_flow.freshBlockWithParams;
|
|
pub const currentBlockHasTerminator = lower_control_flow.currentBlockHasTerminator;
|
|
pub const ensureTerminator = lower_control_flow.ensureTerminator;
|
|
|
|
// --- moved to lower/decl.zig (lower_decl) ---
|
|
pub const SelectedFunc = lower_decl.SelectedFunc;
|
|
pub const BareCallee = lower_decl.BareCallee;
|
|
pub const VisibleStructAuthor = lower_decl.VisibleStructAuthor;
|
|
pub const lowerRoot = lower_decl.lowerRoot;
|
|
pub const validateMainSignature = lower_decl.validateMainSignature;
|
|
pub const checkRequiredEntryPoints = lower_decl.checkRequiredEntryPoints;
|
|
pub const injectComptimeConstants = lower_decl.injectComptimeConstants;
|
|
pub const findVariantIndex = lower_decl.findVariantIndex;
|
|
pub const lowerDeferredTypeFns = lower_decl.lowerDeferredTypeFns;
|
|
pub const lowerDecls = lower_decl.lowerDecls;
|
|
pub const detectContextDecl = lower_decl.detectContextDecl;
|
|
pub const funcWantsImplicitCtx = lower_decl.funcWantsImplicitCtx;
|
|
pub const fnPtrTypeWantsCtx = lower_decl.fnPtrTypeWantsCtx;
|
|
pub const scanDecls = lower_decl.scanDecls;
|
|
pub const registerTypedModuleConst = lower_decl.registerTypedModuleConst;
|
|
pub const typedConstInitFits = lower_decl.typedConstInitFits;
|
|
pub const constExprInitFits = lower_decl.constExprInitFits;
|
|
pub const registerTopLevelGlobal = lower_decl.registerTopLevelGlobal;
|
|
pub const globalInitValue = lower_decl.globalInitValue;
|
|
pub const diagnoseNonConstGlobal = lower_decl.diagnoseNonConstGlobal;
|
|
pub const resolveForwardIdentifierAliases = lower_decl.resolveForwardIdentifierAliases;
|
|
pub const aliasResolvedInSource = lower_decl.aliasResolvedInSource;
|
|
pub const declareFunction = lower_decl.declareFunction;
|
|
pub const registerNamespaceQualifiedFns = lower_decl.registerNamespaceQualifiedFns;
|
|
pub const registerQualifiedFn = lower_decl.registerQualifiedFn;
|
|
pub const isVisible = lower_decl.isVisible;
|
|
pub const visibleOverEdges = lower_decl.visibleOverEdges;
|
|
pub const isCImportVisible = lower_decl.isCImportVisible;
|
|
pub const isNameVisible = lower_decl.isNameVisible;
|
|
pub const lazyLowerFunction = lower_decl.lazyLowerFunction;
|
|
pub const lowerFunctionBodyInto = lower_decl.lowerFunctionBodyInto;
|
|
pub const lowerFunction = lower_decl.lowerFunction;
|
|
pub const lowerMainAndComptime = lower_decl.lowerMainAndComptime;
|
|
pub const lowerRetainedSameNameAuthors = lower_decl.lowerRetainedSameNameAuthors;
|
|
pub const selectPlainCallableAuthor = lower_decl.selectPlainCallableAuthor;
|
|
pub const selectNominalLeaf = lower_decl.selectNominalLeaf;
|
|
pub const isNamedTypeKind = lower_decl.isNamedTypeKind;
|
|
pub const namedRefTid = lower_decl.namedRefTid;
|
|
pub const nameAuthoredAsTypeAnywhere = lower_decl.nameAuthoredAsTypeAnywhere;
|
|
pub const recordLocalTypeName = lower_decl.recordLocalTypeName;
|
|
pub const localTypeInSource = lower_decl.localTypeInSource;
|
|
pub const localTypeInAnySource = lower_decl.localTypeInAnySource;
|
|
pub const resolveNominalLeaf = lower_decl.resolveNominalLeaf;
|
|
pub const fnDeclOfRaw = lower_decl.fnDeclOfRaw;
|
|
pub const structDeclOfRaw = lower_decl.structDeclOfRaw;
|
|
pub const structMethodFn = lower_decl.structMethodFn;
|
|
pub const typeFnAuthor = lower_decl.typeFnAuthor;
|
|
pub const selectedFuncId = lower_decl.selectedFuncId;
|
|
pub const bareAuthorFuncId = lower_decl.bareAuthorFuncId;
|
|
pub const putTypeAlias = lower_decl.putTypeAlias;
|
|
pub const putModuleConst = lower_decl.putModuleConst;
|
|
pub const putGlobal = lower_decl.putGlobal;
|
|
pub const dropModuleConst = lower_decl.dropModuleConst;
|
|
pub const emitModuleConst = lower_decl.emitModuleConst;
|
|
pub const emitPlaceholder = lower_decl.emitPlaceholder;
|
|
|
|
// --- moved to lower/nominal.zig (lower_nominal) ---
|
|
pub const registerErrorSetDecl = lower_nominal.registerErrorSetDecl;
|
|
pub const registerStructDecl = lower_nominal.registerStructDecl;
|
|
pub const registerEnumDecl = lower_nominal.registerEnumDecl;
|
|
pub const registerUnionDecl = lower_nominal.registerUnionDecl;
|
|
pub const qualifyAnonType = lower_nominal.qualifyAnonType;
|
|
pub const nominalIdOf = lower_nominal.nominalIdOf;
|
|
pub const stampNominalId = lower_nominal.stampNominalId;
|
|
pub const reserveShadowStructSlot = lower_nominal.reserveShadowStructSlot;
|
|
pub const reserveShadowEnumSlot = lower_nominal.reserveShadowEnumSlot;
|
|
pub const reserveShadowUnionSlot = lower_nominal.reserveShadowUnionSlot;
|
|
pub const topLevelTypeDecl = lower_nominal.topLevelTypeDecl;
|
|
pub const reserveShadowSlot = lower_nominal.reserveShadowSlot;
|
|
pub const internNamedTypeDecl = lower_nominal.internNamedTypeDecl;
|
|
pub const adoptsForwardStructStub = lower_nominal.adoptsForwardStructStub;
|
|
pub const shadowNominalId = lower_nominal.shadowNominalId;
|
|
pub const nameHasMultipleTypeAuthors = lower_nominal.nameHasMultipleTypeAuthors;
|
|
pub const rawNamedTypePtr = lower_nominal.rawNamedTypePtr;
|
|
pub const buildGenericStructTemplate = lower_nominal.buildGenericStructTemplate;
|
|
pub const qualifiedStructTemplate = lower_nominal.qualifiedStructTemplate;
|
|
pub const qualifiedMemberMissing = lower_nominal.qualifiedMemberMissing;
|
|
pub const bareVisibleStructDecl = lower_nominal.bareVisibleStructDecl;
|
|
pub const bareVisibleStructTemplate = lower_nominal.bareVisibleStructTemplate;
|
|
pub const registerGenericStructAlias = lower_nominal.registerGenericStructAlias;
|
|
|
|
// --- moved to lower/protocol.zig (lower_protocol) ---
|
|
pub const ProjectionPosition = lower_protocol.ProjectionPosition;
|
|
pub const PackProjection = lower_protocol.PackProjection;
|
|
pub const registerProtocolDecl = lower_protocol.registerProtocolDecl;
|
|
pub const instantiateParamProtocol = lower_protocol.instantiateParamProtocol;
|
|
pub const lookupProtocolArg = lower_protocol.lookupProtocolArg;
|
|
pub const lookupProtocolField = lower_protocol.lookupProtocolField;
|
|
pub const isProtocolType = lower_protocol.isProtocolType;
|
|
pub const getProtocolInfo = lower_protocol.getProtocolInfo;
|
|
pub const getOrCreateThunks = lower_protocol.getOrCreateThunks;
|
|
pub const emitDefaultContextGlobal = lower_protocol.emitDefaultContextGlobal;
|
|
pub const createProtocolThunk = lower_protocol.createProtocolThunk;
|
|
pub const buildProtocolValue = lower_protocol.buildProtocolValue;
|
|
pub const emitProtocolDispatch = lower_protocol.emitProtocolDispatch;
|
|
pub const resolveConcreteTypeName = lower_protocol.resolveConcreteTypeName;
|
|
pub const computeHasImpl = lower_protocol.computeHasImpl;
|
|
};
|
|
|
|
/// 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);
|
|
}
|