The for header is now a comma-separated list of iterables with a
positional capture group and no ':' separator:
for xs (x) { } // collection
for 0..n (i) { } // range (end exclusive)
for 1..=5 (a) { } // ..= inclusive end
for xs, 0.. (x, i) { } // index idiom (replaces (x, i))
for xs, ys (x, y) { } // parallel (zip) iteration
for xs (x) => sum += x; // arrow body (full statement)
First-iterable-wins: the first iterable's length drives the loop and
must be bounded; the other positions follow by their own cursors (a
non-first range's end is not consulted or evaluated; a shorter
non-first collection is read past its length on mismatch). The old
single-iterable index capture is replaced by the trailing open range.
Capture/call disambiguation is positional: the paren group immediately
before '{' or '=>' is the capture, every earlier top-level group is a
call. 'for zip(a, b) (x, y)' calls zip; 'for f(n) { }' reads (n) as
the capture and errors with a parenthesize/add-capture hint. The old
':' form errors with a migration hint.
Lowering is unified across forms: one cursor slot per position (ranges
start at their start, collections at 0), all advanced together, the
first position's bound terminating. inline for keeps the single
bounded comptime range.
Migrated the full corpus (examples, library modules, issue repros,
in-source test strings). New coverage: examples/0050 (the full feature
surface) and examples/1149-1155 (seven diagnostic faces). specs.md For
Loop section + grammar rewritten; readme teaser updated.
704 lines
32 KiB
Zig
704 lines
32 KiB
Zig
const std = @import("std");
|
|
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 TypeId = types.TypeId;
|
|
const Ref = inst_mod.Ref;
|
|
const FuncId = inst_mod.FuncId;
|
|
const Function = inst_mod.Function;
|
|
|
|
const lower = @import("../lower.zig");
|
|
const Lowering = lower.Lowering;
|
|
const Scope = lower.Scope;
|
|
|
|
pub fn lowerLambda(self: *Lowering, lam: *const ast.Lambda) Ref {
|
|
// Lower the lambda body as a new anonymous function
|
|
var buf: [64]u8 = undefined;
|
|
const name = std.fmt.bufPrint(&buf, "__lambda_{d}", .{self.block_counter}) catch "__lambda";
|
|
self.block_counter += 1;
|
|
|
|
// Collect lambda param names for exclusion from captures
|
|
var param_names = std.StringHashMap(void).init(self.alloc);
|
|
defer param_names.deinit();
|
|
for (lam.params) |p| {
|
|
param_names.put(p.name, {}) catch {};
|
|
}
|
|
|
|
// Pre-scan lambda body AST for free variables (captures)
|
|
var captures = std.ArrayList(CaptureInfo).empty;
|
|
defer captures.deinit(self.alloc);
|
|
self.collectCaptures(lam.body, ¶m_names, &captures);
|
|
|
|
// Deduplicate captures
|
|
var seen = std.StringHashMap(void).init(self.alloc);
|
|
defer seen.deinit();
|
|
var deduped = std.ArrayList(CaptureInfo).empty;
|
|
defer deduped.deinit(self.alloc);
|
|
for (captures.items) |cap| {
|
|
if (!seen.contains(cap.name)) {
|
|
seen.put(cap.name, {}) catch {};
|
|
deduped.append(self.alloc, cap) catch {};
|
|
}
|
|
}
|
|
const capture_list = deduped.items;
|
|
|
|
// Build env struct type if there are captures
|
|
var env_struct_ty: TypeId = .void;
|
|
if (capture_list.len > 0) {
|
|
const env_field_data = self.alloc.alloc(types.TypeInfo.StructInfo.Field, capture_list.len) catch unreachable;
|
|
for (capture_list, 0..) |cap, i| {
|
|
var nbuf: [32]u8 = undefined;
|
|
const fname = std.fmt.bufPrint(&nbuf, "cap_{d}", .{i}) catch "cap";
|
|
env_field_data[i] = .{
|
|
.name = self.module.types.internString(fname),
|
|
.ty = cap.ty,
|
|
};
|
|
}
|
|
const env_name = std.fmt.bufPrint(&buf, "__env_{d}", .{self.block_counter}) catch "__env";
|
|
const env_name_id = self.module.types.internString(env_name);
|
|
env_struct_ty = self.module.types.intern(.{ .@"struct" = .{
|
|
.name = env_name_id,
|
|
.fields = env_field_data,
|
|
} });
|
|
}
|
|
|
|
// Save current builder state
|
|
const saved_func = self.builder.func;
|
|
const saved_block = self.builder.current_block;
|
|
const saved_counter = self.builder.inst_counter;
|
|
const saved_scope = self.scope;
|
|
|
|
// Build param list. Convention when implicit_ctx is enabled:
|
|
// slot 0 = __sx_ctx: *void
|
|
// slot 1 = env: *void
|
|
// slot 2+ = user params
|
|
// Without implicit_ctx, env is slot 0 and user params follow.
|
|
var params = std.ArrayList(Function.Param).empty;
|
|
const env_ptr_ty = self.module.types.ptrTo(.void);
|
|
const lambda_wants_ctx = self.implicit_ctx_enabled and lam.call_conv != .c;
|
|
if (lambda_wants_ctx) {
|
|
params.append(self.alloc, .{
|
|
.name = self.module.types.internString("__sx_ctx"),
|
|
.ty = env_ptr_ty,
|
|
}) catch unreachable;
|
|
}
|
|
params.append(self.alloc, .{
|
|
.name = self.module.types.internString("env"),
|
|
.ty = env_ptr_ty,
|
|
}) catch unreachable;
|
|
// Get target closure param types for inference (from Closure(T1, T2) -> R annotations)
|
|
const target_closure_params: ?[]const TypeId = if (self.target_type) |tt| blk: {
|
|
if (!tt.isBuiltin()) {
|
|
const tti = self.module.types.get(tt);
|
|
if (tti == .closure) break :blk tti.closure.params;
|
|
// Unwrap ?Closure(...) → Closure(...)
|
|
if (tti == .optional) {
|
|
const inner = tti.optional.child;
|
|
if (!inner.isBuiltin()) {
|
|
const inner_info = self.module.types.get(inner);
|
|
if (inner_info == .closure) break :blk inner_info.closure.params;
|
|
}
|
|
}
|
|
}
|
|
break :blk null;
|
|
} else null;
|
|
// User params follow the ctx (optional) + env slots in `params`.
|
|
const user_param_base: usize = (if (lambda_wants_ctx) @as(usize, 1) else 0) + 1;
|
|
for (lam.params, 0..) |p, pi| {
|
|
const pty: TypeId = blk: {
|
|
// Unannotated lambda params take their type positionally from
|
|
// the target `Closure(T0, …)` signature. Resolve them here so
|
|
// `resolveParamType` (which would diagnose a missing annotation)
|
|
// is only called for params that carry one.
|
|
if (p.type_expr.data == .inferred_type) {
|
|
if (target_closure_params != null and pi < target_closure_params.?.len) {
|
|
break :blk target_closure_params.?[pi];
|
|
}
|
|
if (self.diagnostics) |d| {
|
|
d.addFmt(.err, p.type_expr.span, "cannot infer type of lambda parameter '{s}'; annotate it or use the lambda where a closure type is expected", .{p.name});
|
|
}
|
|
break :blk .unresolved;
|
|
}
|
|
break :blk self.resolveParamType(&p);
|
|
};
|
|
params.append(self.alloc, .{
|
|
.name = self.module.types.internString(p.name),
|
|
.ty = pty,
|
|
}) catch unreachable;
|
|
}
|
|
|
|
const ret_ty = blk: {
|
|
if (lam.return_type) |rt| {
|
|
break :blk type_bridge.resolveAstType(rt, &self.module.types, &self.program_index.type_alias_map, &self.program_index.module_const_map);
|
|
}
|
|
// Use target closure return type if available — but only when it's
|
|
// a resolved type. An `.unresolved` ret comes from an unbound
|
|
// generic (`Closure(..) -> $R`); fall through to infer it from the
|
|
// body so the concrete return drives `$R` inference at the call site.
|
|
if (self.target_type) |tt| {
|
|
if (!tt.isBuiltin()) {
|
|
const tti = self.module.types.get(tt);
|
|
if (tti == .closure and tti.closure.ret != .unresolved) break :blk tti.closure.ret;
|
|
// Unwrap ?Closure(...) → Closure(...)
|
|
if (tti == .optional) {
|
|
const inner = tti.optional.child;
|
|
if (!inner.isBuiltin()) {
|
|
const inner_info = self.module.types.get(inner);
|
|
if (inner_info == .closure and inner_info.closure.ret != .unresolved) break :blk inner_info.closure.ret;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Arrow lambda without explicit return type — infer from body expression
|
|
// Temporarily bind params in scope so inferExprType can resolve param types
|
|
var temp_scope = Scope.init(self.alloc, self.scope);
|
|
const saved = self.scope;
|
|
self.scope = &temp_scope;
|
|
for (lam.params, 0..) |p, i| {
|
|
const pty = params.items[user_param_base + i].ty;
|
|
temp_scope.put(p.name, .{ .ref = @enumFromInt(0), .ty = pty, .is_alloca = false });
|
|
}
|
|
const inferred = self.inferExprType(lam.body);
|
|
self.scope = saved;
|
|
temp_scope.deinit();
|
|
break :blk inferred;
|
|
};
|
|
const name_id = self.module.types.internString(name);
|
|
const func_id = self.builder.beginFunction(name_id, params.items, ret_ty);
|
|
if (lam.call_conv == .c) {
|
|
self.module.getFunctionMut(func_id).call_conv = .c;
|
|
}
|
|
self.builder.currentFunc().has_implicit_ctx = lambda_wants_ctx;
|
|
|
|
// Param-slot layout: ctx at 0 (if present), env at ctx_slots,
|
|
// user args at ctx_slots+1.
|
|
const lambda_ctx_slots: u32 = if (lambda_wants_ctx) 1 else 0;
|
|
const env_param_idx: u32 = lambda_ctx_slots;
|
|
const user_param_base_lam: u32 = lambda_ctx_slots + 1;
|
|
|
|
// Save + rebind current_ctx_ref so the body's sx-to-sx calls
|
|
// forward the trampoline's own ctx (slot 0).
|
|
const saved_ctx_ref_lam = self.current_ctx_ref;
|
|
defer self.current_ctx_ref = saved_ctx_ref_lam;
|
|
if (lambda_wants_ctx) self.current_ctx_ref = Ref.fromIndex(0);
|
|
|
|
// A lambda is its own function: its `return` must drain only ITS OWN
|
|
// `defer`s, not the enclosing function's. Open a fresh defer window
|
|
// (like `lowerFunction`/`monomorphizeFunction`) and restore on exit —
|
|
// otherwise lowering a closure literal inside a `defer` body re-enters
|
|
// the enclosing function's defer drain (infinite recursion).
|
|
const saved_func_defer_base = self.func_defer_base;
|
|
const saved_defer_len = self.defer_stack.items.len;
|
|
defer {
|
|
self.func_defer_base = saved_func_defer_base;
|
|
self.defer_stack.shrinkRetainingCapacity(saved_defer_len);
|
|
}
|
|
self.func_defer_base = saved_defer_len;
|
|
|
|
// Create entry block
|
|
const entry_name = self.module.types.internString("entry");
|
|
const entry = self.builder.appendBlock(entry_name, &.{});
|
|
self.builder.switchToBlock(entry);
|
|
|
|
// Create scope WITHOUT parent — captures are bound from env, not parent scope
|
|
var lambda_scope = Scope.init(self.alloc, null);
|
|
self.scope = &lambda_scope;
|
|
|
|
// Bind captures from env struct (at env_param_idx)
|
|
if (capture_list.len > 0) {
|
|
const env_param_ref = Ref.fromIndex(env_param_idx);
|
|
// Alloca env struct locally so struct_gep can resolve the type
|
|
const env_local = self.builder.alloca(env_struct_ty);
|
|
// Compute env size
|
|
const env_byte_size_inner = self.computeEnvSize(capture_list);
|
|
const env_size_val = self.builder.constInt(@intCast(env_byte_size_inner), .s64);
|
|
// memcpy(local_alloca, env_param, size)
|
|
_ = self.callForeign("memcpy", &.{ env_local, env_param_ref, env_size_val }, self.module.types.ptrTo(.void));
|
|
|
|
for (capture_list, 0..) |cap, i| {
|
|
// GEP into env struct to get field pointer
|
|
const field_ptr = self.builder.structGepTyped(env_local, @intCast(i), self.module.types.ptrTo(cap.ty), env_struct_ty);
|
|
// Load the captured value into a local alloca
|
|
const loaded = self.builder.load(field_ptr, cap.ty);
|
|
const slot = self.builder.alloca(cap.ty);
|
|
self.builder.store(slot, loaded);
|
|
lambda_scope.put(cap.name, .{ .ref = slot, .ty = cap.ty, .is_alloca = true });
|
|
}
|
|
}
|
|
|
|
// Also need parent scope for function lookups (but not variable lookups)
|
|
// Set up fn_names from parent scope chain
|
|
{
|
|
var s: ?*Scope = saved_scope;
|
|
while (s) |scope| {
|
|
var it = scope.fn_names.iterator();
|
|
while (it.next()) |e| {
|
|
if (!lambda_scope.fn_names.contains(e.key_ptr.*)) {
|
|
lambda_scope.fn_names.put(e.key_ptr.*, e.value_ptr.*) catch {};
|
|
}
|
|
}
|
|
s = scope.parent;
|
|
}
|
|
}
|
|
|
|
// Bind params (user args start at user_param_base_lam, shifted past ctx + env).
|
|
// Use the signature types computed above (`params`), which already
|
|
// applied contextual typing from the target closure to untyped params —
|
|
// `resolveParamType` alone would drop it and default each to s64.
|
|
for (lam.params, 0..) |p, i| {
|
|
const pty = params.items[user_param_base + i].ty;
|
|
const slot = self.builder.alloca(pty);
|
|
const param_ref = Ref.fromIndex(user_param_base_lam + @as(u32, @intCast(i)));
|
|
self.builder.store(slot, param_ref);
|
|
lambda_scope.put(p.name, .{ .ref = slot, .ty = pty, .is_alloca = true });
|
|
}
|
|
|
|
// Lower body — capture last expression as return value. The
|
|
// `in_lambda_body` flag scopes the lambda-specific `raise`-not-failable
|
|
// hint; save/restore so a lambda nested inside a regular function (or a
|
|
// lambda inside a lambda) restores the enclosing context.
|
|
const saved_in_lambda = self.in_lambda_body;
|
|
self.in_lambda_body = true;
|
|
if (ret_ty != .void) {
|
|
if (self.lowerBlockValue(lam.body)) |val| {
|
|
if (!self.currentBlockHasTerminator()) {
|
|
const val_ty = self.builder.getRefType(val);
|
|
// A value-carrying failable arrow lambda (`-> (T, !) => expr`)
|
|
// yields the bare success value; the compiler appends the
|
|
// no-error slot (0) — same as a `return v` in a block body.
|
|
if (!ret_ty.isBuiltin() and self.module.types.get(ret_ty) == .tuple and self.errorChannelOf(ret_ty) != null) {
|
|
self.lowerFailableSuccessReturn(val, ret_ty, lam.body.span);
|
|
} else {
|
|
const coerced = if (val_ty != .void) self.coerceToType(val, val_ty, ret_ty) else val;
|
|
self.builder.ret(coerced, ret_ty);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
self.lowerBlock(lam.body);
|
|
}
|
|
self.in_lambda_body = saved_in_lambda;
|
|
self.ensureTerminator(ret_ty);
|
|
self.builder.finalize();
|
|
|
|
// Restore builder state
|
|
self.scope = saved_scope;
|
|
lambda_scope.deinit();
|
|
self.builder.func = saved_func;
|
|
self.builder.current_block = saved_block;
|
|
self.builder.inst_counter = saved_counter;
|
|
// Restore the caller's `current_ctx_ref` BEFORE we emit the env
|
|
// alloc/memcpy below — those run in the caller's scope, and
|
|
// `allocViaContext` reads `current_ctx_ref` to find the
|
|
// installed allocator. Without this, the env_heap dispatch
|
|
// would still see `Ref.fromIndex(0)` (the lambda's own ctx
|
|
// param), which doesn't exist in the caller's frame and
|
|
// silently routes through the default context instead of any
|
|
// surrounding `push Context.{ allocator = ... }`.
|
|
self.current_ctx_ref = saved_ctx_ref_lam;
|
|
|
|
// Closure flowing into a BARE function-pointer slot (`(T) -> U`, no env):
|
|
// the slot is called without the closure env arg, so the closure fn can't
|
|
// be passed directly. For a capture-free closure whose return type matches
|
|
// the slot, emit an adapter with the bare ABI. Reject the cases the bare
|
|
// ABI can't represent: a capturing closure (env has nowhere to live), and
|
|
// a failable closure into a non-failable slot (foreign code can't observe
|
|
// the error channel — ERR E5.1 FFI-boundary rule).
|
|
if (self.target_type) |tt| {
|
|
if (!tt.isBuiltin() and self.module.types.get(tt) == .function) {
|
|
const slot_ret = self.module.types.get(tt).function.ret;
|
|
const widen_ok = self.errorChannelOf(slot_ret) != null and self.errorChannelOf(ret_ty) == null and self.failableSuccessType(slot_ret) == ret_ty;
|
|
if (capture_list.len > 0) {
|
|
if (self.diagnostics) |d| d.addFmt(.err, lam.body.span, "a capturing closure cannot be passed as a bare function pointer; declare the parameter type as `Closure(...)` so its environment is carried", .{});
|
|
} else if (ret_ty == slot_ret or widen_ok) {
|
|
// Matching ABI, or a non-failable closure widening into a
|
|
// failable slot (∅ ⊆ slot set) — the adapter wraps {value, 0}.
|
|
const adapter = self.createClosureToBareFnAdapter(func_id, self.module.types.get(tt).function, ret_ty, lam.body.span);
|
|
return self.builder.emit(.{ .func_ref = adapter }, tt);
|
|
} else if (self.errorChannelOf(ret_ty) != null and self.errorChannelOf(slot_ret) == null) {
|
|
if (self.diagnostics) |d| d.addFmt(.err, lam.body.span, "failable closure cannot be assigned to a non-failable function-type slot; foreign code can't observe the error channel — handle the error in a wrapper closure that absorbs it", .{});
|
|
} else if (self.diagnostics) |d| {
|
|
d.addFmt(.err, lam.body.span, "closure return type does not match the function-type slot", .{});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create proper closure type (user-visible params only — skip ctx + env).
|
|
const skip_count: usize = if (lambda_wants_ctx) 2 else 1;
|
|
var param_types_list = std.ArrayList(TypeId).empty;
|
|
for (params.items[skip_count..]) |p| {
|
|
param_types_list.append(self.alloc, p.ty) catch unreachable;
|
|
}
|
|
const closure_ty = self.module.types.closureType(param_types_list.items, ret_ty);
|
|
|
|
// Build env and closure in the caller's scope
|
|
if (capture_list.len > 0) {
|
|
// Alloca env struct on stack (so struct_gep can resolve the type)
|
|
const env_local = self.builder.alloca(env_struct_ty);
|
|
|
|
// Store captured values into env struct fields
|
|
for (capture_list, 0..) |cap, i| {
|
|
const gep = self.builder.structGepTyped(env_local, @intCast(i), self.module.types.ptrTo(cap.ty), env_struct_ty);
|
|
const val = if (cap.is_alloca)
|
|
self.builder.load(cap.ref, cap.ty)
|
|
else
|
|
cap.ref;
|
|
self.builder.store(gep, val);
|
|
}
|
|
|
|
// Copy env to heap (so it outlives the stack frame).
|
|
// Route through `context.allocator.alloc` rather than calling
|
|
// libc malloc directly so closures respect a surrounding
|
|
// `push Context.{ allocator = ... }` and a tracker / arena
|
|
// counts the env allocation alongside everything else.
|
|
const env_byte_size = self.computeEnvSize(capture_list);
|
|
const env_size = self.builder.constInt(@intCast(env_byte_size), .s64);
|
|
const ptr_void = self.module.types.ptrTo(.void);
|
|
const env_heap = self.allocViaContext(env_size, ptr_void);
|
|
// memcpy(heap, stack_alloca, size)
|
|
_ = self.callForeign("memcpy", &.{ env_heap, env_local, env_size }, ptr_void);
|
|
|
|
return self.builder.closureCreate(func_id, env_heap, closure_ty);
|
|
} else {
|
|
return self.builder.closureCreate(func_id, Ref.none, closure_ty);
|
|
}
|
|
}
|
|
|
|
/// Create a trampoline function that wraps a bare function for closure auto-promotion.
|
|
/// The trampoline has signature `(env: *void, args...) -> ret` and simply calls the
|
|
/// bare function with `(args...)`, ignoring the env parameter.
|
|
pub fn createBareFnTrampoline(self: *Lowering, bare_func_id: FuncId, closure_info: types.TypeInfo.ClosureInfo) FuncId {
|
|
// Build trampoline params: [__sx_ctx]? + env + closure params.
|
|
// When the program uses Context, every sx-side trampoline carries
|
|
// the implicit ctx at slot 0 and forwards it to the wrapped
|
|
// function (which is also sx-side and expects it at slot 0).
|
|
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
|
defer params.deinit(self.alloc);
|
|
const void_ptr_ty = self.module.types.ptrTo(.void);
|
|
const wants_ctx = self.implicit_ctx_enabled;
|
|
if (wants_ctx) {
|
|
params.append(self.alloc, .{ .name = self.module.types.internString("__sx_ctx"), .ty = void_ptr_ty }) catch unreachable;
|
|
}
|
|
const env_name = self.module.types.internString("env");
|
|
params.append(self.alloc, .{ .name = env_name, .ty = void_ptr_ty }) catch unreachable;
|
|
for (closure_info.params, 0..) |pty, i| {
|
|
var buf: [32]u8 = undefined;
|
|
const pname = std.fmt.bufPrint(&buf, "a{d}", .{i}) catch "arg";
|
|
params.append(self.alloc, .{ .name = self.module.types.internString(pname), .ty = pty }) catch unreachable;
|
|
}
|
|
|
|
// Generate unique trampoline name
|
|
const bare_func = self.module.functions.items[bare_func_id.index()];
|
|
const bare_name = self.module.types.getString(bare_func.name);
|
|
var name_buf: [128]u8 = undefined;
|
|
const tramp_name = std.fmt.bufPrint(&name_buf, "__tramp_{s}", .{bare_name}) catch "__tramp";
|
|
const tramp_name_id = self.module.types.internString(tramp_name);
|
|
|
|
// Save builder state
|
|
const saved_func = self.builder.func;
|
|
const saved_block = self.builder.current_block;
|
|
const saved_counter = self.builder.inst_counter;
|
|
|
|
// Create function
|
|
const owned_params = self.alloc.dupe(inst_mod.Function.Param, params.items) catch unreachable;
|
|
var func = inst_mod.Function.init(tramp_name_id, owned_params, closure_info.ret);
|
|
func.has_implicit_ctx = wants_ctx;
|
|
const func_id = self.module.addFunction(func);
|
|
self.builder.func = func_id;
|
|
self.builder.inst_counter = @intCast(owned_params.len); // params occupy refs 0..N-1
|
|
const entry_name = self.module.types.internString("entry");
|
|
const entry_block = self.builder.appendBlock(entry_name, &.{});
|
|
self.builder.switchToBlock(entry_block);
|
|
|
|
// Build call args: forward [__sx_ctx]? + user_params (skip env).
|
|
// Trampoline slots: 0=ctx (if present), {0|1}=env, then user args.
|
|
const ctx_slots: usize = if (wants_ctx) 1 else 0;
|
|
const user_arg_start: u32 = @intCast(ctx_slots + 1); // skip ctx + env
|
|
var call_args = std.ArrayList(Ref).empty;
|
|
defer call_args.deinit(self.alloc);
|
|
if (wants_ctx and bare_func.has_implicit_ctx) {
|
|
call_args.append(self.alloc, Ref.fromIndex(0)) catch unreachable; // forward our ctx
|
|
}
|
|
for (closure_info.params, 0..) |_, i| {
|
|
call_args.append(self.alloc, Ref.fromIndex(user_arg_start + @as(u32, @intCast(i)))) catch unreachable;
|
|
}
|
|
const owned_args = self.alloc.dupe(Ref, call_args.items) catch unreachable;
|
|
const result = self.builder.emit(.{ .call = .{ .callee = bare_func_id, .args = owned_args } }, closure_info.ret);
|
|
|
|
// Return result (or void)
|
|
if (closure_info.ret != .void) {
|
|
self.builder.ret(result, closure_info.ret);
|
|
} else {
|
|
self.builder.retVoid();
|
|
}
|
|
self.builder.finalize();
|
|
|
|
// Restore builder state
|
|
self.builder.func = saved_func;
|
|
self.builder.current_block = saved_block;
|
|
self.builder.inst_counter = saved_counter;
|
|
|
|
return func_id;
|
|
}
|
|
|
|
/// Adapter for coercing a closure into a BARE function-pointer slot
|
|
/// (`(T) -> U`, no env). The closure's underlying function has signature
|
|
/// `[ctx?] + env + user-params`, but a bare fn-ptr slot is *called* without
|
|
/// the env arg — so the closure fn can't be used directly (the env slot
|
|
/// would swallow the first user arg). This adapter carries the bare ABI
|
|
/// (`[ctx?] + user-params`) and forwards to the closure fn with a null env.
|
|
/// Only sound for capture-free closures (a null env is correct iff the body
|
|
/// reads no captures); the caller rejects capturing closures.
|
|
///
|
|
/// When `closure_ret` differs from `fn_info.ret`, this is the ∅-widening
|
|
/// case (a non-failable closure into a failable slot): the closure returns
|
|
/// the success value and the adapter wraps it into the slot's `{value, 0}`
|
|
/// failable tuple (ERR E5.1 non-failable→failable widening).
|
|
pub fn createClosureToBareFnAdapter(self: *Lowering, closure_func_id: FuncId, fn_info: types.TypeInfo.FunctionInfo, closure_ret: TypeId, span: ast.Span) FuncId {
|
|
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
|
defer params.deinit(self.alloc);
|
|
const void_ptr_ty = self.module.types.ptrTo(.void);
|
|
const wants_ctx = self.implicit_ctx_enabled;
|
|
if (wants_ctx) {
|
|
params.append(self.alloc, .{ .name = self.module.types.internString("__sx_ctx"), .ty = void_ptr_ty }) catch unreachable;
|
|
}
|
|
for (fn_info.params, 0..) |pty, i| {
|
|
var buf: [32]u8 = undefined;
|
|
const pname = std.fmt.bufPrint(&buf, "a{d}", .{i}) catch "arg";
|
|
params.append(self.alloc, .{ .name = self.module.types.internString(pname), .ty = pty }) catch unreachable;
|
|
}
|
|
|
|
const closure_func = self.module.functions.items[closure_func_id.index()];
|
|
const closure_name = self.module.types.getString(closure_func.name);
|
|
var name_buf: [128]u8 = undefined;
|
|
const adapter_name = std.fmt.bufPrint(&name_buf, "__cl2fn_{s}", .{closure_name}) catch "__cl2fn";
|
|
const adapter_name_id = self.module.types.internString(adapter_name);
|
|
|
|
const saved_func = self.builder.func;
|
|
const saved_block = self.builder.current_block;
|
|
const saved_counter = self.builder.inst_counter;
|
|
|
|
const owned_params = self.alloc.dupe(inst_mod.Function.Param, params.items) catch unreachable;
|
|
var func = inst_mod.Function.init(adapter_name_id, owned_params, fn_info.ret);
|
|
func.has_implicit_ctx = wants_ctx;
|
|
const func_id = self.module.addFunction(func);
|
|
self.builder.func = func_id;
|
|
self.builder.inst_counter = @intCast(owned_params.len);
|
|
const entry_name = self.module.types.internString("entry");
|
|
const entry_block = self.builder.appendBlock(entry_name, &.{});
|
|
self.builder.switchToBlock(entry_block);
|
|
|
|
// Forward [ctx?] + null env + user params to the closure fn.
|
|
const ctx_slots: usize = if (wants_ctx) 1 else 0;
|
|
var call_args = std.ArrayList(Ref).empty;
|
|
defer call_args.deinit(self.alloc);
|
|
if (wants_ctx) call_args.append(self.alloc, Ref.fromIndex(0)) catch unreachable;
|
|
call_args.append(self.alloc, self.builder.constNull(void_ptr_ty)) catch unreachable;
|
|
for (fn_info.params, 0..) |_, i| {
|
|
call_args.append(self.alloc, Ref.fromIndex(@intCast(ctx_slots + i))) catch unreachable;
|
|
}
|
|
const owned_args = self.alloc.dupe(Ref, call_args.items) catch unreachable;
|
|
const result = self.builder.emit(.{ .call = .{ .callee = closure_func_id, .args = owned_args } }, closure_ret);
|
|
if (closure_ret == fn_info.ret) {
|
|
if (fn_info.ret != .void) {
|
|
self.builder.ret(result, fn_info.ret);
|
|
} else {
|
|
self.builder.retVoid();
|
|
}
|
|
} else {
|
|
// ∅-widening: closure returns the success value; wrap `{value, 0}`
|
|
// into the slot's failable tuple.
|
|
self.lowerFailableSuccessReturn(result, fn_info.ret, span);
|
|
}
|
|
self.builder.finalize();
|
|
|
|
self.builder.func = saved_func;
|
|
self.builder.current_block = saved_block;
|
|
self.builder.inst_counter = saved_counter;
|
|
return func_id;
|
|
}
|
|
|
|
/// Walk an AST node and collect free variable references (identifiers that are
|
|
/// in the current scope but not in lambda params).
|
|
pub fn collectCaptures(self: *Lowering, node: *const Node, param_names: *std.StringHashMap(void), captures: *std.ArrayList(CaptureInfo)) void {
|
|
switch (node.data) {
|
|
.identifier => |id| {
|
|
// Skip lambda params
|
|
if (param_names.contains(id.name)) return;
|
|
// Skip function names
|
|
if (self.program_index.fn_ast_map.contains(id.name)) return;
|
|
// Skip type names
|
|
if (self.program_index.struct_template_map.contains(id.name)) return;
|
|
// Check if it's a variable in the parent scope
|
|
if (self.scope) |scope| {
|
|
if (scope.lookup(id.name)) |binding| {
|
|
captures.append(self.alloc, .{
|
|
.name = id.name,
|
|
.ty = binding.ty,
|
|
.ref = binding.ref,
|
|
.is_alloca = binding.is_alloca,
|
|
}) catch {};
|
|
}
|
|
}
|
|
},
|
|
.binary_op => |bo| {
|
|
self.collectCaptures(bo.lhs, param_names, captures);
|
|
self.collectCaptures(bo.rhs, param_names, captures);
|
|
},
|
|
.unary_op => |uo| {
|
|
self.collectCaptures(uo.operand, param_names, captures);
|
|
},
|
|
.call => |cl| {
|
|
self.collectCaptures(cl.callee, param_names, captures);
|
|
for (cl.args) |arg| {
|
|
self.collectCaptures(arg, param_names, captures);
|
|
}
|
|
},
|
|
.block => |blk| {
|
|
for (blk.stmts) |stmt| {
|
|
self.collectCaptures(stmt, param_names, captures);
|
|
}
|
|
},
|
|
.if_expr => |ie| {
|
|
self.collectCaptures(ie.condition, param_names, captures);
|
|
self.collectCaptures(ie.then_branch, param_names, captures);
|
|
if (ie.else_branch) |eb| self.collectCaptures(eb, param_names, captures);
|
|
},
|
|
.while_expr => |we| {
|
|
self.collectCaptures(we.condition, param_names, captures);
|
|
self.collectCaptures(we.body, param_names, captures);
|
|
},
|
|
.return_stmt => |rs| {
|
|
if (rs.value) |v| self.collectCaptures(v, param_names, captures);
|
|
},
|
|
.var_decl => |vd| {
|
|
if (vd.value) |v| self.collectCaptures(v, param_names, captures);
|
|
// Register the local var name so it's not captured
|
|
param_names.put(vd.name, {}) catch {};
|
|
},
|
|
.const_decl => |cd| {
|
|
self.collectCaptures(cd.value, param_names, captures);
|
|
param_names.put(cd.name, {}) catch {};
|
|
},
|
|
.assignment => |a| {
|
|
self.collectCaptures(a.target, param_names, captures);
|
|
self.collectCaptures(a.value, param_names, captures);
|
|
},
|
|
.destructure_decl => |dd| {
|
|
self.collectCaptures(dd.value, param_names, captures);
|
|
for (dd.names) |name| {
|
|
param_names.put(name, {}) catch {};
|
|
}
|
|
},
|
|
.field_access => |fa| {
|
|
self.collectCaptures(fa.object, param_names, captures);
|
|
},
|
|
.index_expr => |ie| {
|
|
self.collectCaptures(ie.object, param_names, captures);
|
|
self.collectCaptures(ie.index, param_names, captures);
|
|
},
|
|
.struct_literal => |sl| {
|
|
for (sl.field_inits) |fi| {
|
|
self.collectCaptures(fi.value, param_names, captures);
|
|
}
|
|
},
|
|
.array_literal => |al| {
|
|
for (al.elements) |elem| {
|
|
self.collectCaptures(elem, param_names, captures);
|
|
}
|
|
},
|
|
.lambda => |inner_lam| {
|
|
// For nested lambdas, the inner lambda captures from our scope too
|
|
// But its own params should be excluded
|
|
var inner_params = std.StringHashMap(void).init(self.alloc);
|
|
defer inner_params.deinit();
|
|
// Copy current param_names
|
|
var it = param_names.iterator();
|
|
while (it.next()) |e| {
|
|
inner_params.put(e.key_ptr.*, {}) catch {};
|
|
}
|
|
for (inner_lam.params) |p| {
|
|
inner_params.put(p.name, {}) catch {};
|
|
}
|
|
self.collectCaptures(inner_lam.body, &inner_params, captures);
|
|
},
|
|
.match_expr => |me| {
|
|
self.collectCaptures(me.subject, param_names, captures);
|
|
for (me.arms) |arm| {
|
|
self.collectCaptures(arm.body, param_names, captures);
|
|
}
|
|
},
|
|
.null_coalesce => |nc| {
|
|
self.collectCaptures(nc.lhs, param_names, captures);
|
|
self.collectCaptures(nc.rhs, param_names, captures);
|
|
},
|
|
.deref_expr => |de| {
|
|
self.collectCaptures(de.operand, param_names, captures);
|
|
},
|
|
.for_expr => |fe| {
|
|
for (fe.iterables) |it| {
|
|
self.collectCaptures(it.expr, param_names, captures);
|
|
if (it.range_end) |re| self.collectCaptures(re, param_names, captures);
|
|
}
|
|
// Register capture names as locals so they're not captured
|
|
for (fe.captures) |cap| param_names.put(cap.name, {}) catch {};
|
|
self.collectCaptures(fe.body, param_names, captures);
|
|
},
|
|
.slice_expr => |se| {
|
|
self.collectCaptures(se.object, param_names, captures);
|
|
if (se.start) |s| self.collectCaptures(s, param_names, captures);
|
|
if (se.end) |e| self.collectCaptures(e, param_names, captures);
|
|
},
|
|
.tuple_literal => |tl| {
|
|
for (tl.elements) |elem| {
|
|
self.collectCaptures(elem.value, param_names, captures);
|
|
}
|
|
},
|
|
.force_unwrap => |fu| {
|
|
self.collectCaptures(fu.operand, param_names, captures);
|
|
},
|
|
.chained_comparison => |cc| {
|
|
for (cc.operands) |op| {
|
|
self.collectCaptures(op, param_names, captures);
|
|
}
|
|
},
|
|
.defer_stmt => |ds| {
|
|
self.collectCaptures(ds.expr, param_names, captures);
|
|
},
|
|
.ffi_intrinsic_call => |fic| {
|
|
self.collectCaptures(fic.return_type, param_names, captures);
|
|
for (fic.args) |arg| {
|
|
self.collectCaptures(arg, param_names, captures);
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
|
|
/// Compute the byte size of the env struct based on captured value types.
|
|
pub fn computeEnvSize(self: *Lowering, capture_list: []const CaptureInfo) usize {
|
|
// Must match LLVM's struct layout: fields are aligned to their natural alignment
|
|
var offset: usize = 0;
|
|
var max_align: usize = 1;
|
|
for (capture_list) |cap| {
|
|
const field_size = self.typeSizeBytes(cap.ty);
|
|
const field_align = self.typeAlignBytes(cap.ty);
|
|
if (field_align > max_align) max_align = field_align;
|
|
// Align offset to field alignment
|
|
offset = (offset + field_align - 1) & ~(field_align - 1);
|
|
offset += field_size;
|
|
}
|
|
// Align total to max field alignment (matches LLVM's struct alignment)
|
|
return (offset + max_align - 1) & ~(max_align - 1);
|
|
}
|
|
|
|
pub const CaptureInfo = struct {
|
|
name: []const u8,
|
|
ty: TypeId,
|
|
ref: Ref, // alloca or value ref in the parent scope
|
|
is_alloca: bool,
|
|
};
|