Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/backend/dev/LirCodeGen.zig
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,8 @@ pub const BoxyBuiltinFn = enum {
materialize_call_result,
register_proc,
register_erased_proc,
erased_callable_result_desc,
erased_callable_arg_desc,
call_erased,
list_concat,
list_prepend,
Expand Down Expand Up @@ -347,6 +349,8 @@ pub const BoxyBuiltinFn = enum {
.materialize_call_result => "roc_boxy_materialize_call_result",
.register_proc => "roc_boxy_register_proc",
.register_erased_proc => "roc_boxy_register_erased_proc",
.erased_callable_result_desc => "roc_boxy_erased_callable_result_desc",
.erased_callable_arg_desc => "roc_boxy_erased_callable_arg_desc",
.call_erased => "roc_boxy_call_erased",
.list_concat => "roc_boxy_list_concat",
.list_prepend => "roc_boxy_list_prepend",
Expand Down Expand Up @@ -410,6 +414,8 @@ pub const BoxyBuiltinFn = enum {
.dynamic_frac_literal_ref,
.materialize_call_result,
.register_proc,
.erased_callable_result_desc,
.erased_callable_arg_desc,
=> null,
};
}
Expand Down Expand Up @@ -5279,6 +5285,38 @@ pub fn LirCodeGen(comptime target: RocTarget) type {
return .{ .stack = .{ .offset = result_offset, .size = ValueSize.fromByteCount(elem_size) } };
}
},
.erased_callable_result_desc => {
const callable_loc = try self.emitValueLocal(GuardedList.at(args, 0));
const callable_reg = try self.ensureInGeneralReg(callable_loc);
var builder = try Builder.init(&self.codegen.emit, &self.codegen.stack_offset);
try builder.addRegArg(callable_reg);
try self.callBoxyBuiltin(&builder, .erased_callable_result_desc);
self.codegen.freeGeneral(callable_reg);

const result_offset = self.codegen.allocStackSlot(8);
try self.emitStore(.w64, frame_ptr, result_offset, ret_reg_0);
return self.stackLocationForLayout(ll.ret_layout, result_offset);
},
.erased_callable_arg_desc => {
const callable_loc = try self.emitValueLocal(GuardedList.at(args, 0));
const arg_index_loc = try self.emitValueLocal(GuardedList.at(args, 1));
const descriptor_index_loc = try self.emitValueLocal(GuardedList.at(args, 2));
const callable_reg = try self.ensureInGeneralReg(callable_loc);
const arg_index_reg = try self.ensureInGeneralReg(arg_index_loc);
const descriptor_index_reg = try self.ensureInGeneralReg(descriptor_index_loc);
var builder = try Builder.init(&self.codegen.emit, &self.codegen.stack_offset);
try builder.addRegArg(callable_reg);
try builder.addRegArg(arg_index_reg);
try builder.addRegArg(descriptor_index_reg);
try self.callBoxyBuiltin(&builder, .erased_callable_arg_desc);
self.codegen.freeGeneral(descriptor_index_reg);
self.codegen.freeGeneral(arg_index_reg);
self.codegen.freeGeneral(callable_reg);

const result_offset = self.codegen.allocStackSlot(8);
try self.emitStore(.w64, frame_ptr, result_offset, ret_reg_0);
return self.stackLocationForLayout(ll.ret_layout, result_offset);
},
.ptr_alloca => {
// ptr_alloca: () -> Ptr(T). Reserve a zeroed stack slot for T and
// yield its address. Target local layout is ptr(T).
Expand Down Expand Up @@ -6838,6 +6876,8 @@ pub fn LirCodeGen(comptime target: RocTarget) type {
.dec_to_u8_try_unsafe,
.dict_pseudo_seed,
.erased_capture_load,
.erased_callable_result_desc,
.erased_callable_arg_desc,
.f32_from_bits,
.f32_from_str,
.f32_to_bits,
Expand Down
66 changes: 49 additions & 17 deletions src/backend/llvm/MonoLlvmCodeGen.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3300,8 +3300,8 @@ pub const MonoLlvmCodeGen = struct {
.local, .runtime, .dict_method_arg, .dict_method_hidden => return error.CompilationFailed,
};
const wip = self.wip orelse return error.CompilationFailed;
const ids = wip.alloca(.normal, .i32, try self.boxyInt(.i32, captures.len), LlvmBuilder.Alignment.fromByteUnits(4), .default, "boxy_capture_ids") catch return error.OutOfMemory;
const descs = wip.alloca(.normal, ptr_ty, try self.boxyInt(.i32, captures.len), self.targetPointerAlignment(), .default, "boxy_capture_descs") catch return error.OutOfMemory;
const ids = try self.allocEntryBlockSlot(.i32, @intCast(captures.len), LlvmBuilder.Alignment.fromByteUnits(4), "boxy_capture_ids");
const descs = try self.allocEntryBlockSlot(ptr_ty, @intCast(captures.len), self.targetPointerAlignment(), "boxy_capture_descs");
for (0..captures.len) |i| {
const capture = GuardedList.at(captures, i);
const id_ptr = try self.offsetPtr(ids, @intCast(i * 4));
Expand Down Expand Up @@ -3362,8 +3362,7 @@ pub const MonoLlvmCodeGen = struct {
}

fn boxyOutDescPtr(self: *MonoLlvmCodeGen, name: []const u8) Error!LlvmBuilder.Value {
const wip = self.wip orelse return error.CompilationFailed;
return wip.alloca(.normal, try self.ptrType(), .@"1", self.targetPointerAlignment(), .default, name) catch return error.OutOfMemory;
return self.allocEntryBlockSlot(try self.ptrType(), 1, self.targetPointerAlignment(), name);
}

fn emitBoxyBox(self: *MonoLlvmCodeGen, assign: anytype) Error!void {
Expand Down Expand Up @@ -3542,7 +3541,7 @@ pub const MonoLlvmCodeGen = struct {
const arg_stride = desc_offset + word;

const args_ptr = if (arg_locals.len == 0) try self.boxyNullPtr() else blk: {
const raw = wip.alloca(.normal, .i8, try self.boxyInt(.i32, arg_locals.len * arg_stride), self.targetPointerAlignment(), .default, "boxy_call_args") catch return error.OutOfMemory;
const raw = try self.allocEntryBlockSlot(.i8, @intCast(arg_locals.len * arg_stride), self.targetPointerAlignment(), "boxy_call_args");
for (0..arg_locals.len) |i| {
const local = GuardedList.at(arg_locals, i);
const desc_local = GuardedList.at(arg_desc_locals, i);
Expand All @@ -3556,7 +3555,7 @@ pub const MonoLlvmCodeGen = struct {
};

const hidden_ptr = if (hidden_locals.len == 0) try self.boxyNullPtr() else blk: {
const raw = wip.alloca(.normal, ptr_ty, try self.boxyInt(.i32, hidden_locals.len), self.targetPointerAlignment(), .default, "boxy_hidden_args") catch return error.OutOfMemory;
const raw = try self.allocEntryBlockSlot(ptr_ty, @intCast(hidden_locals.len), self.targetPointerAlignment(), "boxy_hidden_args");
for (0..hidden_locals.len) |i| {
const local = GuardedList.at(hidden_locals, i);
try self.storePointer(try self.offsetPtr(raw, @intCast(i * word)), try self.loadPointer(self.slot(local).ptr));
Expand Down Expand Up @@ -3594,9 +3593,8 @@ pub const MonoLlvmCodeGen = struct {
default_layout: layout.Idx,
fractional: bool,
) Error!void {
const wip = self.wip orelse return error.CompilationFailed;
const ptr_ty = try self.ptrType();
const literal_ptr = wip.alloca(.normal, .i128, .@"1", LlvmBuilder.Alignment.fromByteUnits(16), .default, "boxy_literal") catch return error.OutOfMemory;
const literal_ptr = try self.allocEntryBlockSlot(.i128, 1, LlvmBuilder.Alignment.fromByteUnits(16), "boxy_literal");
try self.storeI128Literal(literal_ptr, .i128, value);
const out_desc = try self.boxyOutDescPtr("boxy_literal_desc");
try self.callBoxyVoid(
Expand Down Expand Up @@ -3715,7 +3713,6 @@ pub const MonoLlvmCodeGen = struct {
try self.prepareLocalWrite(target);
try self.materializeLocalIfDeferred(closure);
const builder = self.builder orelse return error.CompilationFailed;
const wip = self.wip orelse return error.CompilationFailed;
const ptr_ty = try self.ptrType();
const closure_ptr = try self.loadPointer(self.slot(closure).ptr);
const fn_ptr = try self.loadPointer(closure_ptr);
Expand Down Expand Up @@ -3761,8 +3758,7 @@ pub const MonoLlvmCodeGen = struct {
const arg_descs_ptr = if (arg_desc_locals.len == 0)
builder.nullValue(ptr_ty) catch return error.OutOfMemory
else blk: {
const desc_count = builder.intValue(.i32, arg_desc_locals.len) catch return error.OutOfMemory;
const desc_buf = wip.alloca(.normal, ptr_ty, desc_count, self.targetPointerAlignment(), .default, "erased_arg_descs") catch return error.OutOfMemory;
const desc_buf = try self.allocEntryBlockSlot(ptr_ty, @intCast(arg_desc_locals.len), self.targetPointerAlignment(), "erased_arg_descs");
for (0..arg_desc_locals.len) |i| {
const desc_local = GuardedList.at(arg_desc_locals, i);
try self.storePointer(
Expand Down Expand Up @@ -3821,7 +3817,6 @@ pub const MonoLlvmCodeGen = struct {
if (capture) |capture_local| try self.materializeLocalIfDeferred(capture_local);
if (reuse) |reuse_local| try self.materializeLocalIfDeferred(reuse_local);
const builder = self.builder orelse return error.CompilationFailed;
const wip = self.wip orelse return error.CompilationFailed;
const ptr_ty = try self.ptrType();
const capture_size = if (capture_layout) |idx| self.layoutByteSize(idx) else 0;
const metadata_offset: u32 = @intCast(builtins.erased_callable.compilerMetadataOffset(capture_size));
Expand All @@ -3848,14 +3843,12 @@ pub const MonoLlvmCodeGen = struct {
const metadata_desc = if (result_desc) |desc| try self.resolveBoxyDesc(desc) else try self.boxyNullPtr();

const data_ptr = if (reuse) |reuse_local| blk: {
const capture_src = wip.alloca(
.normal,
const capture_src = try self.allocEntryBlockSlot(
.i8,
try self.boxyInt(.i32, total_capture_size),
total_capture_size,
LlvmBuilder.Alignment.fromByteUnits(builtins.erased_callable.capture_alignment),
.default,
"erased_repack_capture",
) catch return error.OutOfMemory;
);
if (capture) |capture_local| {
if (capture_size > 0) {
try self.copyBytes(capture_src, self.slot(capture_local).ptr, capture_size, self.alignmentForLayout(capture_layout.?));
Expand Down Expand Up @@ -4398,6 +4391,13 @@ pub const MonoLlvmCodeGen = struct {
.box_unbox => try self.emitBoxUnbox(target, GuardedList.at(arg_locals, 0)),
.box_prepare_update => try self.emitBoxPrepareUpdate(target, GuardedList.at(arg_locals, 0), unique_args),
.erased_capture_load => try self.emitErasedCaptureLoad(target, GuardedList.at(arg_locals, 0)),
.erased_callable_result_desc => try self.emitErasedCallableResultDesc(target, GuardedList.at(arg_locals, 0)),
.erased_callable_arg_desc => try self.emitErasedCallableArgDesc(
target,
GuardedList.at(arg_locals, 0),
GuardedList.at(arg_locals, 1),
GuardedList.at(arg_locals, 2),
),
.ptr_alloca => try self.emitPtrAlloca(target),
.box_alloc_zeroed => try self.emitBoxAllocZeroed(target),
.ptr_store => try self.emitPtrStore(GuardedList.at(arg_locals, 0), GuardedList.at(arg_locals, 1)),
Expand Down Expand Up @@ -10192,6 +10192,38 @@ pub const MonoLlvmCodeGen = struct {
if (self.slot(target).size > 0) try self.copyBytes(self.slot(target).ptr, capture_ptr, self.slot(target).size, self.slot(target).alignment);
}

fn emitErasedCallableResultDesc(self: *MonoLlvmCodeGen, target: LocalId, arg: LocalId) Error!void {
const ptr_ty = try self.ptrType();
const callable = try self.loadPointer(self.slot(arg).ptr);
const desc = try self.callBoxy(
"roc_boxy_erased_callable_result_desc",
ptr_ty,
&.{ptr_ty},
&.{callable},
);
try self.storePointer(self.slot(target).ptr, desc);
}

fn emitErasedCallableArgDesc(
self: *MonoLlvmCodeGen,
target: LocalId,
callable_arg: LocalId,
arg_index_arg: LocalId,
descriptor_index_arg: LocalId,
) Error!void {
const ptr_ty = try self.ptrType();
const callable = try self.loadPointer(self.slot(callable_arg).ptr);
const arg_index = try self.loadScalar(self.slot(arg_index_arg).ptr, self.localLayout(arg_index_arg));
const descriptor_index = try self.loadScalar(self.slot(descriptor_index_arg).ptr, self.localLayout(descriptor_index_arg));
const desc = try self.callBoxy(
"roc_boxy_erased_callable_arg_desc",
ptr_ty,
&.{ ptr_ty, .i64, .i64 },
&.{ callable, arg_index, descriptor_index },
);
try self.storePointer(self.slot(target).ptr, desc);
}

/// ptr_alloca: () -> Ptr(T). Reserve a zeroed slot for T and store its
/// address into the target. TRMC emits this once per proc entry (pre-loop),
/// and allocEntryBlockSlot keeps the physical slot in the entry frame.
Expand Down
14 changes: 14 additions & 0 deletions src/backend/wasm/WasmCodeGen.zig
Original file line number Diff line number Diff line change
Expand Up @@ -9030,6 +9030,8 @@ pub fn registerBoxySymbolTargets(self: *Self) HostedSymbolError!void {
try self.registerBoxySymbol("roc_boxy_call_dict", &.{ .i32, .i32, .i32, .i32, .i32, .i32, .i32, .i32, .i32, .i32, .i32, .i32 }, &.{});
try self.registerBoxySymbol("roc_boxy_register_proc", &.{ .i32, .i32, .i32, .i64, .i32, .i64 }, &.{});
try self.registerBoxySymbol("roc_boxy_register_erased_proc", &.{ .i32, .i32, .i32, .i32, .i32, .i32, .i32, .i32, .i32 }, &.{});
try self.registerBoxySymbol("roc_boxy_erased_callable_result_desc", &.{.i32}, &.{.i32});
try self.registerBoxySymbol("roc_boxy_erased_callable_arg_desc", &.{ .i32, .i64, .i64 }, &.{.i32});
try self.registerBoxySymbol("roc_boxy_call_erased", &.{ .i32, .i32, .i32, .i32, .i32, .i32, .i32, .i32, .i32, .i32, .i32, .i32, .i32, .i32, .i32, .i32 }, &.{});
try self.registerBoxySymbol("roc_boxy_list_concat", &.{ .i32, .i32, .i32, .i32, .i32, .i32, .i32, .i32, .i32, .i32, .i32, .i64, .i32 }, &.{});
try self.registerBoxySymbol("roc_boxy_list_prepend", &.{ .i32, .i32, .i32, .i32, .i32, .i32, .i32, .i32, .i32, .i32, .i32 }, &.{});
Expand Down Expand Up @@ -14732,6 +14734,16 @@ fn generateLowLevel(self: *Self, ll: anytype) Allocator.Error!void {
}
}
},
.erased_callable_result_desc => {
try self.emitProcLocal(GuardedList.at(args, 0));
try self.emitBoxyCall("roc_boxy_erased_callable_result_desc");
},
.erased_callable_arg_desc => {
try self.emitProcLocal(GuardedList.at(args, 0));
try self.emitProcLocal(GuardedList.at(args, 1));
try self.emitProcLocal(GuardedList.at(args, 2));
try self.emitBoxyCall("roc_boxy_erased_callable_arg_desc");
},

.ptr_alloca => {
// ptr_alloca: () -> Ptr(T). Reserve a zeroed shadow-stack slot for T
Expand Down Expand Up @@ -16510,6 +16522,8 @@ fn numericOpFromLowLevel(op: LIR.LowLevel) NumericOp {
.box_unbox,
.box_prepare_update,
.erased_capture_load,
.erased_callable_result_desc,
.erased_callable_arg_desc,
.ptr_alloca,
.box_alloc_zeroed,
.ptr_store,
Expand Down
13 changes: 13 additions & 0 deletions src/base/LowLevel.zig
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,12 @@ pub const LowLevel = enum(u16) {
/// retains nested payload children, and releases the consumed input.
box_prepare_update,
erased_capture_load,
/// Read the exact result descriptor stored in compiler-private metadata on
/// a Roc-created erased callable. Introduced only by Boxy lowering.
erased_callable_result_desc,
/// Read one exact direct argument descriptor from a Roc-created erased
/// callable's registered capture slot. Introduced only by Boxy lowering.
erased_callable_arg_desc,

// Compiler-internal pointer operations, introduced by the TRMC pass
// (src/lir/trmc.zig). Never produced by user code or canonicalization.
Expand Down Expand Up @@ -971,6 +977,13 @@ pub const LowLevel = enum(u16) {
// result cannot name a lender to borrow from.
.erased_capture_load => RcEffect.retainsResult(),

// The callable is borrowed for the metadata read and the returned
// descriptor pointer is immutable runtime metadata, not an owned
// Roc value.
.erased_callable_result_desc,
.erased_callable_arg_desc,
=> RcEffect.none(),

.box_alloc_zeroed => RcEffect.allocates(),

// The stored value's ownership transfers into the pointed-at structure.
Expand Down
2 changes: 2 additions & 0 deletions src/boxy_runtime/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ comptime {
"roc_boxy_materialize_call_result",
"roc_boxy_register_proc",
"roc_boxy_register_erased_proc",
"roc_boxy_erased_callable_result_desc",
"roc_boxy_erased_callable_arg_desc",
"roc_boxy_call_erased",
"roc_boxy_list_concat",
"roc_boxy_list_prepend",
Expand Down
8 changes: 5 additions & 3 deletions src/cli/test/fx_test_specs.zig
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ pub const TestSpec = struct {

/// Regression coverage for #9401: boxed erased callables must use the same
/// payload ABI and RC/drop semantics when they are created by Roc, created by
/// the host, passed to the host, stored by the host, and returned to Roc.
/// the host, passed to the host, stored by the host, and returned to Roc. The
/// conditional call also keeps fixed-lifetime LLVM scratch slots in the entry
/// block when an erased call is emitted from a branch.
/// Kept outside `io_spec_tests` so the explicit Zig tests can run this narrow
/// host-boundary fixture independently for interpreter and dev backends.
pub const host_boxed_fn_boundary_test = TestSpec{
.roc_file = "test/fx/host_boxed_fn_boundary.roc",
.io_spec = "1>primitive: 42|1>nested record: 39|1>recursive tree: 42|1>host returns boxed capture: 42|1>host consumes primitive: 42|1>host consumes nested record: 40|1>host consumes recursive tree: 43|1>host consumes boxed capture: 15|1>host roundtrip: 42|1>host declines reuse: 42|1>host consumes reuse: 42|1>host store: 42|1>drops primitive=1 nested_record=1 nested_str=1 recursive_tree=1 tree_child_boxes=4 boxed_capture=1 transition_outer=1 transition_inner=1 transition_nonnull=1",
.description = "Regression test: Boxed erased callables across the host boundary in both directions",
.io_spec = "1>primitive: 42|1>nested record: 39|1>recursive tree: 42|1>host returns boxed capture: 42|1>host consumes primitive: 42|1>host consumes nested record: 40|1>host consumes recursive tree: 43|1>host consumes boxed capture: 15|1>host roundtrip: 42|1>host declines reuse: 42|1>host consumes reuse: 42|1>host store: 42|0<call|1>erased call in branch: 42|1>drops primitive=2 nested_record=1 nested_str=1 recursive_tree=1 tree_child_boxes=4 boxed_capture=1 transition_outer=1 transition_inner=1 transition_nonnull=1",
.description = "Regression test: boxed erased callables across the host boundary and inside control flow",
};

/// All fx platform tests that can be run with --test mode IO specs.
Expand Down
Loading
Loading