mem: implicit-context foundation + many compiler fixes
The session-long set of changes that lay the groundwork for the
Jai-literal implicit-Context-parameter refactor. Lots of accumulated
work; the new arrival is the implicit-ctx foundation (steps 1+2 of
the plan in current/CHECKPOINT-MEM.md):
Step 1 — `CAllocator :: struct {}` stateless allocator in
library/modules/allocators.sx, delegating directly to
libc_malloc/libc_free. `ConstantValue` in src/ir/inst.zig gains a
`func_ref: FuncId` leaf so nested aggregates can carry function
pointers (the inline Allocator value's fn-ptr fields). Switch
sites updated in emit_llvm.zig, print.zig, interp.zig.
Step 2 — `emitDefaultContextGlobal` in src/ir/lower.zig synthesises
a static `__sx_default_context` global with a nested-aggregate
init_val pointing at the CAllocator → Allocator thunks. The
second-pass `initVtableGlobals` in emit_llvm.zig is generalised
to handle `.aggregate` init_vals (re-emits after func_map is
populated so func_ref leaves resolve to real symbols).
Also folded in from earlier work this session:
- Phase 1.1: `xx value` heap-copy in `buildProtocolValue` routes
through `context.allocator` via the new `allocViaContext` helper.
- interp.zig: `marshalForeignArg` double-offset bug fixed —
`heapSlice` already adds `hp.offset` to the slice ptr, so the
extra `+ hp.offset` was scribbling memcpy/memset into adjacent
heap state, corrupting `heap.items[0]`. Symptom: `build_format`
at comptime produced zero bytes, all `print` calls failed.
- Lazy lowering: `lazyLowerFunction` now declares foreign-body
functions as extern stubs in the local (comptime) module so
cross-module foreign calls resolve.
- Allocator API: all stdlib allocators on one-line `init() -> *T`
(CAllocator/GPA: libc-backed; Arena/TrackingAllocator: parent-
backed; BufAlloc: embeds state at head of user buffer).
- issues 0038 (transitive #import), 0039 (chess + stdlib migration
fallout), 0040 (generic struct method dot-dispatch), 0041
(pointer types as type-arg), 0042 (alias name resolution) — all
fixed; regression tests in examples/.
- Diagnostic: `emitError` now embeds the lowering's
`current_source_file` and enclosing function in the literal
message; SX_TRACE_UNRESOLVED=1 dumps a Zig stack trace at the
emit site so misattributed spans can't hide where the failure
is.
- tools/verify-step.sh (all-platforms gate) and tools/scratch.sh
(interp/codegen parity tester) added.
Test suite: 152 example tests pass; chess builds + screenshots on
macOS / iOS sim / Android.
This commit is contained in:
481
src/ir/lower.zig
481
src/ir/lower.zig
@@ -161,6 +161,10 @@ pub const Lowering = struct {
|
||||
name: []const u8,
|
||||
param_types: []const TypeId, // excluding self
|
||||
ret_type: TypeId,
|
||||
// True when the AST return type was `Self` (encoded here as *void).
|
||||
// Lets the dispatcher distinguish Self-disguised-as-*void (auto-unbox
|
||||
// on the caller side) from a literal `-> *void` (return as-is).
|
||||
ret_is_self: bool = false,
|
||||
};
|
||||
|
||||
/// One impl block for a parameterised protocol (e.g. `impl Into(Block) for Closure() -> void`).
|
||||
@@ -214,6 +218,12 @@ pub const Lowering = struct {
|
||||
self.scanDecls(decls);
|
||||
// Pass 1b: inject compile-time constants (OS, ARCH, POINTER_SIZE) from target config
|
||||
self.injectComptimeConstants();
|
||||
// Pass 1c: emit the process-wide default Context global, statically
|
||||
// initialised to a CAllocator-backed Allocator value. Used by FFI
|
||||
// wrappers in Step 4 and by the interp's `callWithDefaultContext`
|
||||
// entry. Only fires when the program imports `std.sx` (so Context +
|
||||
// Allocator + CAllocator are all registered).
|
||||
self.emitDefaultContextGlobal();
|
||||
// Pass 2: lower main (and comptime side-effects)
|
||||
self.lowerMainAndComptime(decls);
|
||||
// Pass 3: lower deferred functions (any_to_string etc.) now that all types are registered
|
||||
@@ -418,10 +428,32 @@ pub const Lowering = struct {
|
||||
} else if (cd.value.data == .union_decl) {
|
||||
// Register plain union types in the type table
|
||||
_ = type_bridge.resolveAstType(cd.value, &self.module.types);
|
||||
} else if (cd.value.data == .type_expr) {
|
||||
// Type alias: MyFloat :: f64; → register MyFloat as alias for f64
|
||||
} else if (cd.value.data == .type_expr or
|
||||
cd.value.data == .pointer_type_expr or
|
||||
cd.value.data == .many_pointer_type_expr or
|
||||
cd.value.data == .array_type_expr or
|
||||
cd.value.data == .slice_type_expr or
|
||||
cd.value.data == .optional_type_expr or
|
||||
cd.value.data == .function_type_expr)
|
||||
{
|
||||
// Type alias: MyFloat :: f64; Ptr :: *u8; Cb :: (s32) -> s32;
|
||||
const target_ty = type_bridge.resolveAstType(cd.value, &self.module.types);
|
||||
self.type_alias_map.put(cd.name, target_ty) catch {};
|
||||
} else if (cd.value.data == .identifier) {
|
||||
// Identifier-RHS alias: MyAlias :: MyInt; WideAlias :: Wide;
|
||||
// Chase through type_alias_map, then look up named types
|
||||
// in the table. Forward references resolve lazily because
|
||||
// the .identifier branch of resolveTypeArg also consults
|
||||
// type_alias_map at use time.
|
||||
const rhs_name = cd.value.data.identifier.name;
|
||||
if (self.type_alias_map.get(rhs_name)) |chained| {
|
||||
self.type_alias_map.put(cd.name, chained) catch {};
|
||||
} else {
|
||||
const name_id = self.module.types.internString(rhs_name);
|
||||
if (self.module.types.findByName(name_id)) |tid| {
|
||||
self.type_alias_map.put(cd.name, tid) catch {};
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle generic struct instantiation: Vec3 :: Vec(3, f32)
|
||||
// Parser produces a .call node for these (not parameterized_type_expr)
|
||||
@@ -723,11 +755,38 @@ pub const Lowering = struct {
|
||||
// Only restrict C import fn_decls: foreign_expr with no library_ref
|
||||
if (fd.body.data != .foreign_expr) return true;
|
||||
if (fd.body.data.foreign_expr.library_ref != null) return true;
|
||||
// It's a C import fn_decl — check module scope
|
||||
return self.isNameVisible(fn_name);
|
||||
}
|
||||
|
||||
/// Non-transitive `#import` visibility check for top-level decls.
|
||||
///
|
||||
/// `module_scopes[F]` holds ONLY the names authored in file F (plus its
|
||||
/// namespace aliases). Cross-module visibility is joined here at query
|
||||
/// time by walking each direct flat-import edge in `import_graph` — a
|
||||
/// name is visible from F when it's authored in F or in any module F
|
||||
/// directly `#import`s. Doing the join here (instead of pre-merging in
|
||||
/// `resolveImports`) lets cyclic imports like std.sx ↔ allocators.sx
|
||||
/// still resolve, since the cycle's skipped edge is still recorded in
|
||||
/// `import_graph` and the partner's scope is filled in by the time
|
||||
/// lowering queries it.
|
||||
///
|
||||
/// Falls open when the scoping infrastructure isn't wired (comptime
|
||||
/// callers, directory imports without main_file, etc.). The caller is
|
||||
/// responsible for restricting the call to names that ARE known
|
||||
/// top-level decls; otherwise every local variable would be policed.
|
||||
fn isNameVisible(self: *Lowering, name: []const u8) bool {
|
||||
const scopes = self.module_scopes orelse return true;
|
||||
const source = self.current_source_file orelse return true;
|
||||
const scope = scopes.get(source) orelse return true;
|
||||
return scope.contains(fn_name);
|
||||
const own_scope = scopes.get(source) orelse return true;
|
||||
if (own_scope.contains(name)) return true;
|
||||
const graph = self.import_graph orelse return true;
|
||||
const direct = graph.get(source) orelse return true;
|
||||
var it = direct.iterator();
|
||||
while (it.next()) |kv| {
|
||||
const dep = scopes.get(kv.key_ptr.*) orelse continue;
|
||||
if (dep.contains(name)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Lazily lower a function body on demand. Called when lowerCall can't find
|
||||
@@ -737,8 +796,21 @@ pub const Lowering = struct {
|
||||
if (self.lowered_functions.contains(name)) return;
|
||||
// No AST? (builtins, foreign functions, or imported functions not in this file)
|
||||
const fd = self.fn_ast_map.get(name) orelse return;
|
||||
// Check builtin/foreign/generic — these stay as extern stubs
|
||||
if (fd.body.data == .builtin_expr or fd.body.data == .foreign_expr or fd.body.data == .compiler_expr) return;
|
||||
// Foreign declarations stay as extern stubs but need to be REGISTERED
|
||||
// in the current module so callers get a real FuncId. Without this,
|
||||
// a comptime-lowered function (e.g. `concat` from std.sx pulled into
|
||||
// a fresh ct_module via `evalComptimeString`) emits `.call` against a
|
||||
// FuncId that doesn't exist locally; the interp can't find the
|
||||
// foreign target and silently no-ops instead of dispatching to libc.
|
||||
if (fd.body.data == .foreign_expr) {
|
||||
if (self.resolveFuncByName(name) == null) {
|
||||
self.declareFunction(fd, name);
|
||||
self.lowered_functions.put(name, {}) catch {};
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Builtins / #compiler bodies stay as compiler-handled — no extern stub needed.
|
||||
if (fd.body.data == .builtin_expr or fd.body.data == .compiler_expr) return;
|
||||
if (fd.type_params.len > 0) return; // generics handled by monomorphization (Step 3.13)
|
||||
|
||||
// Defer functions with type-category matches until all types are registered.
|
||||
@@ -1715,12 +1787,29 @@ pub const Lowering = struct {
|
||||
}
|
||||
// Check module-level value constants (e.g. AF_INET :s32: 2)
|
||||
if (self.module_const_map.get(id.name)) |ci| {
|
||||
if (!self.isNameVisible(id.name)) {
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, node.span, "'{s}' is not visible; #import the module that declares it", .{id.name});
|
||||
break :blk self.emitError(id.name, node.span);
|
||||
}
|
||||
break :blk self.emitModuleConst(ci);
|
||||
}
|
||||
// Check if it's a function name — produce function pointer reference
|
||||
// Resolve mangled name for block-local functions
|
||||
const eff_fn_name = if (self.scope) |scope| scope.lookupFn(id.name) orelse id.name else id.name;
|
||||
if (self.fn_ast_map.contains(eff_fn_name)) {
|
||||
// Visibility check only for user-typed bare names (id.name
|
||||
// == eff_fn_name) without a UFCS alias. Mangled local-
|
||||
// scope names and UFCS rewrites are compiler indirections
|
||||
// and stay exempt.
|
||||
if (std.mem.eql(u8, eff_fn_name, id.name) and
|
||||
self.ufcs_alias_map.get(id.name) == null and
|
||||
!self.isNameVisible(eff_fn_name))
|
||||
{
|
||||
if (self.diagnostics) |d|
|
||||
d.addFmt(.err, node.span, "'{s}' is not visible; #import the module that declares it", .{eff_fn_name});
|
||||
break :blk self.emitError(eff_fn_name, node.span);
|
||||
}
|
||||
// Type-as-value: if target is Any (Type variable), produce a type name string
|
||||
if (self.target_type == .any) {
|
||||
const fd = self.fn_ast_map.get(eff_fn_name).?;
|
||||
@@ -4423,6 +4512,19 @@ pub const Lowering = struct {
|
||||
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.ufcs_alias_map.get(id_name) == null and
|
||||
self.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.fn_ast_map.get(eff_name)) |fd| {
|
||||
if (self.current_match_tags) |tags| {
|
||||
if (tags.len > 0 and self.hasCastWithRuntimeType(c)) {
|
||||
@@ -4608,19 +4710,8 @@ pub const Lowering = struct {
|
||||
}
|
||||
// Check builtins first (these are handled natively by interpreter and emitter)
|
||||
if (resolveBuiltin(id.name)) |bid| {
|
||||
// free(protocol_value) → extract ctx (field 0) and free it
|
||||
if (bid == .free and args.items.len == 1) {
|
||||
const arg_ty = self.builder.getRefType(args.items[0]);
|
||||
if (self.getProtocolInfo(arg_ty) != null) {
|
||||
const void_ptr_ty = self.module.types.ptrTo(.void);
|
||||
const ctx_ref = self.builder.emit(.{ .struct_get = .{ .base = args.items[0], .field_index = 0 } }, void_ptr_ty);
|
||||
return self.builder.emit(.{ .heap_free = .{ .operand = ctx_ref } }, .void);
|
||||
}
|
||||
}
|
||||
const ret_ty: TypeId = switch (bid) {
|
||||
.malloc => .s64, // pointer
|
||||
.size_of => .s64,
|
||||
.memcpy, .memset => .s64,
|
||||
.size_of, .align_of => .s64,
|
||||
.sqrt, .sin, .cos, .floor => blk: {
|
||||
// Math builtins: return type matches argument type ($T -> T)
|
||||
if (c.args.len > 0) {
|
||||
@@ -5041,6 +5132,53 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
|
||||
// Generic method on a non-template struct: `obj.method($T, ...)`
|
||||
// or inferred form `obj.method(val)` where val's type pins $T.
|
||||
if (self.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.buildTypeBindings(gen_fd, eff_args.items);
|
||||
defer gbindings.deinit();
|
||||
|
||||
const gmangled = self.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);
|
||||
self.coerceCallArgs(gvalue_args.items, gparams);
|
||||
return self.builder.call(gfid, gvalue_args.items, gret_ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try non-generic qualified method
|
||||
if (self.fn_ast_map.get(qualified)) |fd| {
|
||||
if (!self.lowered_functions.contains(qualified)) {
|
||||
@@ -5134,6 +5272,49 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// Falls back to `.heap_alloc` (libc malloc) only when the `context`
|
||||
/// global hasn't been registered (programs that don't `#import
|
||||
/// "modules/std.sx"`). All standard sx code imports std.sx via
|
||||
/// allocators.sx, so the fallback exists strictly for the bootstrapping
|
||||
/// edge case.
|
||||
fn allocViaContext(self: *Lowering, size_ref: Ref, void_ptr_ty: TypeId) Ref {
|
||||
const ctx_gi = self.global_names.get("context") orelse {
|
||||
return self.builder.emit(.{ .heap_alloc = .{ .operand = size_ref } }, void_ptr_ty);
|
||||
};
|
||||
const ctx_ty_info = self.module.types.get(ctx_gi.ty);
|
||||
if (ctx_ty_info != .@"struct" or ctx_ty_info.@"struct".fields.len < 1) {
|
||||
return self.builder.emit(.{ .heap_alloc = .{ .operand = size_ref } }, void_ptr_ty);
|
||||
}
|
||||
const allocator_ty = ctx_ty_info.@"struct".fields[0].ty;
|
||||
const ctx = self.builder.emit(.{ .global_get = ctx_gi.id }, ctx_gi.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);
|
||||
const args = self.alloc.dupe(Ref, &.{ alloc_ctx, size_ref }) catch unreachable;
|
||||
return self.builder.emit(.{ .call_indirect = .{
|
||||
.callee = fn_ptr,
|
||||
.args = args,
|
||||
} }, void_ptr_ty);
|
||||
}
|
||||
|
||||
/// Emit a call to a foreign-declared function looked up by name.
|
||||
/// Used for the compiler-internal byte-copy in the protocol-erasure
|
||||
/// heap path and the closure env-copy path, both of which need
|
||||
/// libc `memcpy` after the `#builtin` form was dropped.
|
||||
fn callForeign(self: *Lowering, name: []const u8, args: []const Ref, ret_ty: TypeId) Ref {
|
||||
const fid = self.resolveFuncByName(name) orelse @panic("foreign symbol missing — std.sx not imported?");
|
||||
return self.builder.call(fid, args, ret_ty);
|
||||
}
|
||||
|
||||
/// Pattern-match `context.allocator.alloc(size)` → heap_alloc,
|
||||
/// `context.allocator.dealloc(ptr)` → heap_free.
|
||||
fn matchContextAllocCall(self: *Lowering, fa: ast.FieldAccess, call_args: []const Ref) ?Ref {
|
||||
@@ -5177,11 +5358,8 @@ pub const Lowering = struct {
|
||||
.{ "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 },
|
||||
.{ "malloc", inst_mod.BuiltinId.malloc },
|
||||
.{ "free", inst_mod.BuiltinId.free },
|
||||
.{ "memcpy", inst_mod.BuiltinId.memcpy },
|
||||
.{ "memset", inst_mod.BuiltinId.memset },
|
||||
};
|
||||
inline for (builtins) |entry| {
|
||||
if (std.mem.eql(u8, name, entry[0])) return entry[1];
|
||||
@@ -5349,11 +5527,7 @@ pub const Lowering = struct {
|
||||
const env_byte_size_inner = self.computeEnvSize(capture_list);
|
||||
const env_size_val = self.builder.constInt(@intCast(env_byte_size_inner), .s64);
|
||||
// memcpy(local_alloca, env_param, size)
|
||||
const cp_args = self.alloc.dupe(Ref, &.{ env_local, env_param_ref, env_size_val }) catch unreachable;
|
||||
_ = self.builder.emit(.{ .call_builtin = .{
|
||||
.builtin = inst_mod.BuiltinId.memcpy,
|
||||
.args = cp_args,
|
||||
} }, self.module.types.ptrTo(.void));
|
||||
_ = self.callForeign("memcpy", &.{ env_local, env_param_ref, env_size_val }, self.module.types.ptrTo(.void));
|
||||
|
||||
for (capture_list, 0..) |cap, i| {
|
||||
// GEP into env struct to get field pointer
|
||||
@@ -5440,11 +5614,7 @@ pub const Lowering = struct {
|
||||
const ptr_void = self.module.types.ptrTo(.void);
|
||||
const env_heap = self.builder.emit(.{ .heap_alloc = .{ .operand = env_size } }, ptr_void);
|
||||
// memcpy(heap, stack_alloca, size)
|
||||
const args = self.alloc.dupe(Ref, &.{ env_heap, env_local, env_size }) catch unreachable;
|
||||
_ = self.builder.emit(.{ .call_builtin = .{
|
||||
.builtin = inst_mod.BuiltinId.memcpy,
|
||||
.args = args,
|
||||
} }, ptr_void);
|
||||
_ = self.callForeign("memcpy", &.{ env_heap, env_local, env_size }, ptr_void);
|
||||
|
||||
return self.builder.closureCreate(func_id, env_heap, closure_ty);
|
||||
} else {
|
||||
@@ -6369,31 +6539,26 @@ pub const Lowering = struct {
|
||||
|
||||
// ── Generic monomorphization ──────────────────────────────────
|
||||
|
||||
/// Lower a call to a generic function by monomorphizing it with inferred type arguments.
|
||||
fn lowerGenericCall(self: *Lowering, fd: *const ast.FnDecl, base_name: []const u8, call_node: *const ast.Call, lowered_args: []Ref) Ref {
|
||||
// Infer type param bindings from call arguments
|
||||
/// 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()`.
|
||||
fn buildTypeBindings(
|
||||
self: *Lowering,
|
||||
fd: *const ast.FnDecl,
|
||||
args_ast: []const *const Node,
|
||||
) std.StringHashMap(TypeId) {
|
||||
var bindings = std.StringHashMap(TypeId).init(self.alloc);
|
||||
defer bindings.deinit();
|
||||
|
||||
// Determine if type args are passed explicitly:
|
||||
// If call_node.args.len == fd.params.len, the caller passed type args explicitly
|
||||
// (e.g., are_equal(Point, p1, p2)). Otherwise, types are inferred from value args
|
||||
// (e.g., are_equal(p1, p2)).
|
||||
const types_passed_explicitly = call_node.args.len == fd.params.len;
|
||||
|
||||
const types_passed_explicitly = args_ast.len == fd.params.len;
|
||||
for (fd.type_params) |tp| {
|
||||
var found = false;
|
||||
|
||||
// Strategy 1: Direct type param declaration ($T: Type)
|
||||
// The param whose name matches the type param IS the declaration.
|
||||
// The call arg at that position is a type expression — resolve it directly.
|
||||
// Only applies when type args are passed explicitly in the call.
|
||||
// Strategy 1: explicit — the param whose name matches `tp.name` IS
|
||||
// the `$T: Type` declaration; the arg at that position is a type expression.
|
||||
if (types_passed_explicitly) {
|
||||
for (fd.params, 0..) |param, pi| {
|
||||
if (std.mem.eql(u8, param.name, tp.name)) {
|
||||
// This param IS the type param declaration
|
||||
if (pi < call_node.args.len) {
|
||||
const ty = self.resolveTypeArg(call_node.args[pi]);
|
||||
if (pi < args_ast.len and type_bridge.isTypeShapedAstNode(args_ast[pi], &self.module.types)) {
|
||||
const ty = self.resolveTypeArg(args_ast[pi]);
|
||||
bindings.put(tp.name, ty) catch {};
|
||||
found = true;
|
||||
}
|
||||
@@ -6402,11 +6567,8 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
if (found) continue;
|
||||
|
||||
// Strategy 2: Infer from params that USE the type param (e.g., a: $T, b: T, items: []$T)
|
||||
// Check ALL params whose type matches the type param name, pick widest type.
|
||||
// When types are inferred (not explicit), use a separate arg index that
|
||||
// skips type param declarations to correctly map params to call args.
|
||||
// Strategy 2: infer from value params that USE the type param
|
||||
// (e.g. a: $T, b: T, items: []$T). Pick widest type across matches.
|
||||
var inferred_ty: ?TypeId = null;
|
||||
var s2_arg_idx: usize = 0;
|
||||
for (fd.params) |param| {
|
||||
@@ -6420,8 +6582,8 @@ pub const Lowering = struct {
|
||||
}
|
||||
const matched = self.matchTypeParam(param.type_expr, tp.name);
|
||||
if (matched) {
|
||||
if (s2_arg_idx < call_node.args.len) {
|
||||
const arg_ty = self.inferExprType(call_node.args[s2_arg_idx]);
|
||||
if (s2_arg_idx < args_ast.len) {
|
||||
const arg_ty = self.inferExprType(args_ast[s2_arg_idx]);
|
||||
const extracted = self.extractTypeParam(param.type_expr, arg_ty, tp.name);
|
||||
if (extracted) |ety| {
|
||||
if (inferred_ty) |prev| {
|
||||
@@ -6441,8 +6603,17 @@ pub const Lowering = struct {
|
||||
bindings.put(tp.name, ty) catch {};
|
||||
}
|
||||
}
|
||||
return bindings;
|
||||
}
|
||||
|
||||
// Build mangled name: "func_name__Type1_Type2"
|
||||
/// Mangle a generic call site into "base__Type1_Type2".
|
||||
/// Returns a heap-allocated string owned by self.alloc.
|
||||
fn mangleGenericName(
|
||||
self: *Lowering,
|
||||
base_name: []const u8,
|
||||
fd: *const ast.FnDecl,
|
||||
bindings: *const std.StringHashMap(TypeId),
|
||||
) []const u8 {
|
||||
var mangled_buf: [256]u8 = undefined;
|
||||
var mangled_len: usize = 0;
|
||||
for (base_name) |ch| {
|
||||
@@ -6452,14 +6623,12 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
for (fd.type_params) |tp| {
|
||||
// Append separator
|
||||
for ("__") |ch| {
|
||||
if (mangled_len < mangled_buf.len) {
|
||||
mangled_buf[mangled_len] = ch;
|
||||
mangled_len += 1;
|
||||
}
|
||||
}
|
||||
// Append type name
|
||||
const ty = bindings.get(tp.name) orelse .s64;
|
||||
const type_name_str = self.mangleTypeName(ty);
|
||||
for (type_name_str) |ch| {
|
||||
@@ -6469,27 +6638,31 @@ pub const Lowering = struct {
|
||||
}
|
||||
}
|
||||
}
|
||||
const mangled_name = mangled_buf[0..mangled_len];
|
||||
return self.alloc.dupe(u8, mangled_buf[0..mangled_len]) catch base_name;
|
||||
}
|
||||
|
||||
/// Lower a call to a generic function by monomorphizing it with inferred type arguments.
|
||||
fn lowerGenericCall(self: *Lowering, fd: *const ast.FnDecl, base_name: []const u8, call_node: *const ast.Call, lowered_args: []Ref) Ref {
|
||||
var bindings = self.buildTypeBindings(fd, call_node.args);
|
||||
defer bindings.deinit();
|
||||
|
||||
const types_passed_explicitly = call_node.args.len == fd.params.len;
|
||||
const mangled_name = self.mangleGenericName(base_name, fd, &bindings);
|
||||
|
||||
// Check cache
|
||||
if (!self.lowered_functions.contains(mangled_name)) {
|
||||
// Monomorphize: create a new function with the mangled name and lower with type bindings
|
||||
self.monomorphizeFunction(fd, mangled_name, &bindings);
|
||||
}
|
||||
|
||||
// Resolve the monomorphized function and call it (stripping type args)
|
||||
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)
|
||||
// Use separate index for lowered_args since type params don't consume call 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)) {
|
||||
// Only skip in lowered_args if types were passed explicitly in the call
|
||||
if (types_passed_explicitly) arg_idx += 1;
|
||||
continue;
|
||||
}
|
||||
@@ -6894,6 +7067,11 @@ pub const Lowering = struct {
|
||||
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]);
|
||||
@@ -7019,9 +7197,13 @@ pub const Lowering = struct {
|
||||
if (self.type_bindings) |tb| {
|
||||
if (tb.get(id.name)) |ty| return ty;
|
||||
}
|
||||
// Try as a named type by name (resolveAstType doesn't handle .identifier)
|
||||
if (self.type_alias_map.get(id.name)) |alias_ty| return alias_ty;
|
||||
const name_id = self.module.types.internString(id.name);
|
||||
return self.module.types.findByName(name_id) orelse .s64;
|
||||
if (self.module.types.findByName(name_id)) |t| return t;
|
||||
if (self.diagnostics) |diags| {
|
||||
diags.addFmt(.err, node.span, "unresolved type: '{s}'", .{id.name});
|
||||
}
|
||||
return .void;
|
||||
},
|
||||
.type_expr => |te| {
|
||||
if (self.type_alias_map.get(te.name)) |alias_ty| return alias_ty;
|
||||
@@ -7031,6 +7213,14 @@ pub const Lowering = struct {
|
||||
// Handle type constructor calls: size_of(Sx(f32)), size_of(Complex(u32))
|
||||
return self.resolveTypeCallWithBindings(&cl);
|
||||
},
|
||||
.pointer_type_expr,
|
||||
.many_pointer_type_expr,
|
||||
.array_type_expr,
|
||||
.slice_type_expr,
|
||||
.optional_type_expr,
|
||||
.function_type_expr,
|
||||
.tuple_literal,
|
||||
=> return type_bridge.resolveAstType(node, &self.module.types),
|
||||
else => return .s64,
|
||||
}
|
||||
}
|
||||
@@ -7498,6 +7688,41 @@ pub const Lowering = struct {
|
||||
// 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.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)];
|
||||
var types_list = std.ArrayList(TypeId).empty;
|
||||
for (func.params) |p| {
|
||||
types_list.append(self.alloc, p.ty) catch unreachable;
|
||||
}
|
||||
return types_list.items;
|
||||
}
|
||||
if (self.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.
|
||||
@@ -8511,9 +8736,11 @@ pub const Lowering = struct {
|
||||
};
|
||||
ptypes.append(self.alloc, pty) catch unreachable;
|
||||
}
|
||||
var ret_is_self = false;
|
||||
const ret = if (method.return_type) |rt| blk: {
|
||||
if (rt.data == .type_expr) {
|
||||
if (std.mem.eql(u8, rt.data.type_expr.name, "Self")) {
|
||||
ret_is_self = true;
|
||||
break :blk void_ptr_ty;
|
||||
}
|
||||
if (self.type_alias_map.get(rt.data.type_expr.name)) |aliased| {
|
||||
@@ -8526,6 +8753,7 @@ pub const Lowering = struct {
|
||||
.name = method.name,
|
||||
.param_types = self.alloc.dupe(TypeId, ptypes.items) catch unreachable,
|
||||
.ret_type = ret,
|
||||
.ret_is_self = ret_is_self,
|
||||
}) catch unreachable;
|
||||
}
|
||||
self.protocol_decl_map.put(pd.name, .{
|
||||
@@ -8799,6 +9027,54 @@ pub const Lowering = struct {
|
||||
return owned;
|
||||
}
|
||||
|
||||
/// Emit the process-wide default Context as an LLVM static constant.
|
||||
///
|
||||
/// @__sx_default_context = internal constant %Context {
|
||||
/// %Allocator { ptr null,
|
||||
/// ptr @__thunk_CAllocator_Allocator_alloc,
|
||||
/// ptr @__thunk_CAllocator_Allocator_dealloc },
|
||||
/// ptr null
|
||||
/// }
|
||||
///
|
||||
/// Used by FFI inbound wrappers (Step 4) and the interp's default-
|
||||
/// context call entry (Step 7). Only emitted when the program imports
|
||||
/// `std.sx` — without that, Context / Allocator / CAllocator aren't
|
||||
/// registered and the global has no purpose.
|
||||
fn emitDefaultContextGlobal(self: *Lowering) void {
|
||||
const tbl = &self.module.types;
|
||||
const ctx_name_id = tbl.internString("Context");
|
||||
const ctx_ty = tbl.findByName(ctx_name_id) orelse return;
|
||||
if (tbl.findByName(tbl.internString("Allocator")) == null) return;
|
||||
if (tbl.findByName(tbl.internString("CAllocator")) == null) return;
|
||||
|
||||
// Force the CAllocator → Allocator thunks to exist so we can
|
||||
// reference them by FuncId in the static initializer.
|
||||
const thunks = self.getOrCreateThunks("Allocator", "CAllocator");
|
||||
if (thunks.len < 2) return;
|
||||
|
||||
// Inline Allocator value: { ctx: *void, alloc_fn: *void, dealloc_fn: *void }
|
||||
// CAllocator is stateless, so ctx is null.
|
||||
const alloc_fields = self.alloc.alloc(inst_mod.ConstantValue, 3) catch return;
|
||||
alloc_fields[0] = .null_val;
|
||||
alloc_fields[1] = .{ .func_ref = thunks[0] };
|
||||
alloc_fields[2] = .{ .func_ref = thunks[1] };
|
||||
|
||||
// Context value: { allocator: Allocator, data: *void }
|
||||
const ctx_fields = self.alloc.alloc(inst_mod.ConstantValue, 2) catch return;
|
||||
ctx_fields[0] = .{ .aggregate = alloc_fields };
|
||||
ctx_fields[1] = .null_val;
|
||||
|
||||
const global_name = "__sx_default_context";
|
||||
const global_name_id = tbl.internString(global_name);
|
||||
const gid = self.module.addGlobal(.{
|
||||
.name = global_name_id,
|
||||
.ty = ctx_ty,
|
||||
.init_val = .{ .aggregate = ctx_fields },
|
||||
.is_const = true,
|
||||
});
|
||||
self.global_names.put(global_name, .{ .id = gid, .ty = ctx_ty }) catch {};
|
||||
}
|
||||
|
||||
/// Create a thunk function: __thunk_ConcreteType_Protocol_method(ctx: *void, args...) -> ret
|
||||
/// The thunk calls ConcreteType.method(ctx, args...).
|
||||
fn createProtocolThunk(self: *Lowering, proto_name: []const u8, concrete_type_name: []const u8, method: ProtocolMethodInfo) FuncId {
|
||||
@@ -8925,12 +9201,8 @@ pub const Lowering = struct {
|
||||
if (heap_copy) {
|
||||
const concrete_size = self.module.types.typeSizeBytes(concrete_ty);
|
||||
const size_ref = self.builder.constInt(@intCast(concrete_size), .s64);
|
||||
const heap_ptr = self.builder.emit(.{ .heap_alloc = .{ .operand = size_ref } }, void_ptr_ty);
|
||||
const memcpy_args = self.alloc.dupe(Ref, &.{ heap_ptr, concrete_ptr, size_ref }) catch unreachable;
|
||||
_ = self.builder.emit(.{ .call_builtin = .{
|
||||
.builtin = inst_mod.BuiltinId.memcpy,
|
||||
.args = memcpy_args,
|
||||
} }, void_ptr_ty);
|
||||
const heap_ptr = self.allocViaContext(size_ref, void_ptr_ty);
|
||||
_ = self.callForeign("memcpy", &.{ heap_ptr, concrete_ptr, size_ref }, void_ptr_ty);
|
||||
ctx_ptr = heap_ptr;
|
||||
}
|
||||
|
||||
@@ -9055,12 +9327,11 @@ pub const Lowering = struct {
|
||||
const owned = self.alloc.dupe(Ref, call_args.items) catch unreachable;
|
||||
const raw_result = self.builder.emit(.{ .call_indirect = .{ .callee = fn_ptr, .args = owned } }, mi.ret_type);
|
||||
|
||||
// If protocol method returns *void (Self) and the caller expects a value type,
|
||||
// unbox: load the concrete value from the returned pointer. Real pointer
|
||||
// returns (declared `-> *T` for non-Self T) are NOT auto-loaded — the
|
||||
// pointee may be a single byte and reading `sizeof(target)` past it
|
||||
// segfaults. Self is encoded as `*void`, so test against that exact type.
|
||||
if (mi.ret_type == void_ptr) {
|
||||
// If the protocol method was declared `-> Self` (encoded here as *void)
|
||||
// and the caller expects a value type, unbox: load the concrete value
|
||||
// from the returned pointer. A literal `-> *void` return is NOT
|
||||
// auto-loaded — it's a real pointer whose pointee size we don't know.
|
||||
if (mi.ret_is_self) {
|
||||
if (self.target_type) |target| {
|
||||
const target_info = self.module.types.get(target);
|
||||
if (target_info != .pointer) {
|
||||
@@ -9149,7 +9420,7 @@ pub const Lowering = struct {
|
||||
}
|
||||
break :blk TypeId.f64;
|
||||
},
|
||||
.size_of, .malloc => .s64,
|
||||
.size_of, .align_of => .s64,
|
||||
.cast => if (c.args.len > 0) self.resolveTypeArg(c.args[0]) else .s64,
|
||||
else => .s64,
|
||||
};
|
||||
@@ -9376,15 +9647,17 @@ pub const Lowering = struct {
|
||||
defer tmp_bindings.deinit();
|
||||
|
||||
for (fd.type_params) |tp| {
|
||||
// Strategy 1: direct type param decl ($T: Type) — param.name == tp.name
|
||||
// Strategy 1: direct type param decl ($T: Type) — param.name == tp.name.
|
||||
// Only fires when the caller actually supplied a type expression at
|
||||
// that position; otherwise fall through to value-based inference.
|
||||
var found = false;
|
||||
for (fd.params, 0..) |param, pi| {
|
||||
if (std.mem.eql(u8, param.name, tp.name)) {
|
||||
if (pi < c.args.len) {
|
||||
if (pi < c.args.len and type_bridge.isTypeShapedAstNode(c.args[pi], &self.module.types)) {
|
||||
const ty = self.resolveTypeArg(c.args[pi]);
|
||||
tmp_bindings.put(tp.name, ty) catch {};
|
||||
found = true;
|
||||
}
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -9482,6 +9755,21 @@ pub const Lowering = struct {
|
||||
return self.buildProtocolErasure(operand, operand_node, src_ty, dst_ty);
|
||||
}
|
||||
|
||||
// Protocol → pointer: recover the typed ctx pointer (field 0).
|
||||
// The protocol value is `{ ctx, fn1, fn2, ... }` (inline) or
|
||||
// `{ ctx, vtable_ptr }` — either way, ctx lives at field 0.
|
||||
if (self.getProtocolInfo(src_ty)) |_| {
|
||||
if (!dst_ty.isBuiltin()) {
|
||||
const dst_info = self.module.types.get(dst_ty);
|
||||
if (dst_info == .pointer) {
|
||||
const void_ptr_ty = self.module.types.ptrTo(.void);
|
||||
const ctx_ref = self.builder.emit(.{ .struct_get = .{ .base = operand, .field_index = 0 } }, void_ptr_ty);
|
||||
if (dst_ty == void_ptr_ty) return ctx_ref;
|
||||
return self.builder.emit(.{ .bitcast = .{ .operand = ctx_ref, .from = void_ptr_ty, .to = dst_ty } }, dst_ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = self.coerceToType(operand, src_ty, dst_ty);
|
||||
|
||||
// User-space fallback via `impl Into(Target) for Source`. Only fires
|
||||
@@ -9900,7 +10188,26 @@ pub const Lowering = struct {
|
||||
|
||||
fn emitError(self: *Lowering, name: []const u8, span: ?ast.Span) Ref {
|
||||
if (self.diagnostics) |diags| {
|
||||
diags.addFmt(.err, span, "unresolved: '{s}'", .{name});
|
||||
// The literal message carries the lowering's `current_source_file`
|
||||
// and enclosing function name. The diagnostic renderer's
|
||||
// `source_file` -> `file:line:col` prefix can drift when a span is
|
||||
// offset into one source but the diagnostic falls back to another
|
||||
// (e.g. synthetic AST nodes inserted from `#insert` take their
|
||||
// span from the call site, not from the string being inserted).
|
||||
// Embedding the file + function in the message means a
|
||||
// misattributed span can never hide WHERE the lookup actually
|
||||
// failed. Setting SX_TRACE_UNRESOLVED=1 also dumps a Zig stack
|
||||
// trace at the emit site to surface the calling lowering path.
|
||||
const sf = self.current_source_file orelse "<unknown>";
|
||||
const fn_name: []const u8 = if (self.builder.func) |fid|
|
||||
self.module.types.getString(self.module.functions.items[@intFromEnum(fid)].name)
|
||||
else
|
||||
"<top-level>";
|
||||
if (std.c.getenv("SX_TRACE_UNRESOLVED") != null) {
|
||||
std.debug.print("\n== unresolved '{s}' (in {s} fn {s}) ==\n", .{ name, sf, fn_name });
|
||||
std.debug.dumpCurrentStackTrace(.{ .first_address = @returnAddress() });
|
||||
}
|
||||
diags.addFmt(.err, span, "unresolved '{s}' (in {s} fn {s})", .{ name, sf, fn_name });
|
||||
}
|
||||
return self.emitPlaceholder(name);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user