2281 lines
120 KiB
Zig
2281 lines
120 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 type_bridge = @import("../type_bridge.zig");
|
|
const unescape = @import("../../unescape.zig");
|
|
const errors = @import("../../errors.zig");
|
|
const program_index_mod = @import("../program_index.zig");
|
|
const ProtocolMethodInfo = program_index_mod.ProtocolMethodInfo;
|
|
const GlobalInfo = program_index_mod.GlobalInfo;
|
|
const CallResolver = @import("../calls.zig").CallResolver;
|
|
|
|
const TypeId = types.TypeId;
|
|
const Ref = inst_mod.Ref;
|
|
const BlockId = inst_mod.BlockId;
|
|
const FuncId = inst_mod.FuncId;
|
|
const Function = inst_mod.Function;
|
|
|
|
const lower = @import("../lower.zig");
|
|
const Lowering = lower.Lowering;
|
|
const SelectedFunc = Lowering.SelectedFunc;
|
|
const isTypeParamDecl = Lowering.isTypeParamDecl;
|
|
const isPackFn = Lowering.isPackFn;
|
|
const headNameOfCallee = Lowering.headNameOfCallee;
|
|
const hasComptimeParams = Lowering.hasComptimeParams;
|
|
|
|
pub 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.
|
|
//
|
|
// 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;
|
|
};
|
|
}
|
|
// 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;
|
|
// `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;
|
|
};
|
|
// 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
|
|
// for any compile-time-resolvable type argument. The gate is the
|
|
// canonical `isStaticTypeArg` (the one `type_name`/`type_eq` use),
|
|
// so compound shapes (`*T`, `[]T`, `?T`, `[*]T`, `[N]T`) resolve
|
|
// statically instead of falling into the runtime-dispatch path
|
|
// and dying unresolved (issue 0118). An identifier bound in scope
|
|
// as a runtime `Type` value (the `cast(type) val` category-arm
|
|
// form) still classifies as non-static and falls through.
|
|
if (std.mem.eql(u8, id.name, "cast") and c.args.len >= 2) {
|
|
const type_arg = c.args[0];
|
|
if (self.isStaticTypeArg(type_arg)) {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// 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. 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.
|
|
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.resolveGlobalRef(id.name, c.callee.span)) |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,
|
|
// `alias.Type.method()` — strip the alias so the existing
|
|
// `Type.method` qualified machinery resolves the static.
|
|
.field_access => self.namespaceRootedMember(fa.object),
|
|
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;
|
|
// The carry gate (issue 0114): a plain-identifier root that is
|
|
// a namespace ALIAS (not a type / fn global name — those are
|
|
// the `Type.method` paths below) must be visible under the
|
|
// carry rule, and its fn members dispatch pinned to the
|
|
// alias's TARGET module — never the global first-wins
|
|
// qualified registration, never the last-wins bare fallback.
|
|
gate: {
|
|
if (fa.object.data != .identifier) break :gate;
|
|
const oname = fa.object.data.identifier.name;
|
|
if (self.program_index.global_names.contains(oname)) break :gate;
|
|
switch (self.namespaceAliasVerdict(oname)) {
|
|
.target => |target| {
|
|
const fd = Lowering.namespaceFnMember(&target, fa.field) orelse break :gate;
|
|
// Foreign / builtin / #compiler bodies keep their
|
|
// literal global symbol — the existing bare-name
|
|
// machinery below resolves them.
|
|
switch (fd.body.data) {
|
|
.foreign_expr, .builtin_expr, .compiler_expr => break :gate,
|
|
else => {},
|
|
}
|
|
if (hasComptimeParams(fd)) return self.lowerComptimeCall(fd, c);
|
|
if (fd.type_params.len > 0) return self.lowerGenericCall(fd, fa.field, c, args.items);
|
|
var sf = SelectedFunc{ .decl = fd, .source = target.target_module_path };
|
|
const fid = self.selectedFuncId(&sf, fa.field);
|
|
const func = &self.module.functions.items[@intFromEnum(fid)];
|
|
self.packVariadicCallArgs(fd, c, &args);
|
|
const final_args = self.prependCtxIfNeeded(func, args.items);
|
|
self.coerceCallArgs(final_args, func.params);
|
|
if (func.is_variadic) self.promoteCVariadicArgs(final_args, func.params.len);
|
|
return self.builder.call(fid, final_args, func.ret);
|
|
},
|
|
.ambiguous => {
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, fa.object.span, "namespace '{s}' is ambiguous: aliases from multiple flat-imported modules point at different targets; declare the alias locally", .{oname});
|
|
return Ref.none;
|
|
},
|
|
.none => {
|
|
if (self.aliasDeclaredAnywhere(oname)) {
|
|
if (self.diagnostics) |d|
|
|
d.addFmt(.err, fa.object.span, "namespace '{s}' is not visible; #import the module that declares it", .{oname});
|
|
return Ref.none;
|
|
}
|
|
},
|
|
}
|
|
}
|
|
// 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 — but only for the protocol's OWN methods. A
|
|
// non-member field falls through to the free-fn ufcs machinery
|
|
// (`context.allocator.create(Session)` — a ufcs fn taking the
|
|
// protocol value as its first param).
|
|
if (self.getProtocolInfo(obj_ty)) |proto_info| {
|
|
if (protocolHasMethod(proto_info, fa.field)) {
|
|
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| {
|
|
if (protocolHasMethod(proto_info, fa.field)) {
|
|
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);
|
|
}
|
|
}
|
|
|
|
// Free-function dot-call (`recv.fn(args)` → `fn(recv, args)`)
|
|
// is OPT-IN: only a fn declared `name :: ufcs (...) {...}` or a
|
|
// `name :: ufcs target;` alias dispatches. A plain fn is
|
|
// callable directly or via `|>` only — a dot-call on one gets a
|
|
// tailored diagnostic rather than silently becoming a method.
|
|
//
|
|
// 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 (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 alias_target = self.program_index.ufcs_alias_map.get(fa.field);
|
|
const eff_field = alias_target orelse fa.field;
|
|
const ufcs_fd = self.program_index.fn_ast_map.get(eff_field);
|
|
const ufcs_opted_in = alias_target != null or (ufcs_fd != null and ufcs_fd.?.is_ufcs);
|
|
|
|
if (ufcs_opted_in) {
|
|
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;
|
|
}
|
|
// Generic ufcs target: monomorphize with the receiver's AST
|
|
// node prepended so bindings align with fd.params[0].
|
|
if (ufcs_fd) |fd| {
|
|
if (fd.type_params.len > 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) |arg| eff_args.append(self.alloc, arg) catch unreachable;
|
|
var gbindings = self.genericResolver().buildTypeBindings(fd, eff_args.items);
|
|
defer gbindings.deinit();
|
|
const gmangled = self.genericResolver().mangleGenericName(eff_field, fd, &gbindings);
|
|
if (!self.lowered_functions.contains(gmangled)) {
|
|
self.monomorphizeFunction(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. method_args[0] is the
|
|
// receiver (a VALUE — a type-expr receiver
|
|
// classifies as a namespace call, never here),
|
|
// so fd.params[0] is a value param.
|
|
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 == fd.params.len;
|
|
var arg_idx: usize = 1;
|
|
for (fd.params[1..]) |p| {
|
|
if (isTypeParamDecl(&p, 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);
|
|
}
|
|
return self.emitError(eff_field, c.callee.span);
|
|
}
|
|
}
|
|
const ufcs_fid: ?FuncId = blk_uf: {
|
|
if (sel_author) |sf| {
|
|
break :blk_uf self.selectedFuncId(sf, eff_field);
|
|
}
|
|
if (ufcs_fd != null) {
|
|
if (!self.lowered_functions.contains(eff_field)) {
|
|
self.lazyLowerFunction(eff_field);
|
|
}
|
|
}
|
|
break :blk_uf self.resolveFuncByName(eff_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
|
|
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(eff_field, c.callee.span);
|
|
}
|
|
|
|
// A fn by this name exists but is not dot-callable: tailored help.
|
|
if (ufcs_fd != null or self.resolveFuncByName(fa.field) != null) {
|
|
if (self.diagnostics) |d| {
|
|
const id = d.addFmtId(.err, c.callee.span, "'{s}' is not a ufcs function — a plain function does not dispatch via dot-call", .{fa.field});
|
|
d.addHelpFmt(id, c.callee.span, null, "call it directly (`{s}(receiver, ...)`), pipe it (`receiver |> {s}(...)`), or declare it `{s} :: ufcs (...) {{ ... }}`", .{ fa.field, fa.field, fa.field });
|
|
}
|
|
return Ref.none;
|
|
}
|
|
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.
|
|
pub 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;
|
|
}
|
|
|
|
|
|
fn protocolHasMethod(proto_info: anytype, name: []const u8) bool {
|
|
for (proto_info.methods) |m| {
|
|
if (std.mem.eql(u8, m.name, name)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// ── Generic calls ─────────────────────────────────────────────
|
|
|
|
/// 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.
|
|
pub 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).
|
|
pub 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.
|
|
pub 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);
|
|
}
|
|
|
|
/// Try to lower a call as a reflection builtin (expanded inline during lowering).
|
|
/// Returns null if the call is not a recognized reflection builtin.
|
|
pub 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`. 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`.
|
|
///
|
|
/// 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.
|
|
pub 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.
|
|
pub 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);
|
|
}
|
|
|
|
/// 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.
|
|
pub 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).
|
|
pub 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;
|
|
};
|
|
// 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.
|
|
pub 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;
|
|
}
|
|
|
|
pub 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;
|
|
};
|
|
|
|
// 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 (quiet author-aware lookup —
|
|
// param typing only; the call site diagnoses ambiguity / visibility)
|
|
if (self.program_index.global_names.get(bare_name)) |gi_global| {
|
|
const gi: ?GlobalInfo = switch (self.selectGlobalAuthor(bare_name)) {
|
|
.resolved => |g| g,
|
|
.untracked => gi_global,
|
|
else => null,
|
|
};
|
|
if (gi) |g| {
|
|
if (!g.ty.isBuiltin()) {
|
|
const ti = self.module.types.get(g.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 &.{};
|
|
}
|