Skip to content

Commit c9612fe

Browse files
authored
Merge pull request #8492 from roc-lang/platform-fixes
Some fixes for platforms
2 parents 1bd66dd + 53ebeef commit c9612fe

File tree

33 files changed

+732
-301
lines changed

33 files changed

+732
-301
lines changed

src/canonicalize/Can.zig

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1753,6 +1753,10 @@ pub fn canonicalizeFile(
17531753
.platform => |h| {
17541754
self.env.module_kind = .platform;
17551755
try self.createExposedScope(h.exposes);
1756+
// Also add the 'provides' items (what platform provides to the host, e.g., main_for_host!)
1757+
// These need to be in the exposed scope so they become exports
1758+
// Platform provides uses curly braces { main_for_host! } so it's parsed as record fields
1759+
try self.addPlatformProvidesItems(h.provides);
17561760
// Extract required type signatures for type checking
17571761
// This stores the types in env.requires_types without creating local definitions
17581762
// Pass requires_rigids so R1, R2, etc. are in scope when processing signatures
@@ -2531,6 +2535,17 @@ fn createExposedScope(
25312535
self.exposed_scope.deinit(gpa);
25322536
self.exposed_scope = Scope.init(false);
25332537

2538+
try self.addToExposedScope(exposes);
2539+
}
2540+
2541+
/// Add items to the exposed scope without resetting it.
2542+
/// Used for platforms which have both 'exposes' (for apps) and 'provides' (for the host).
2543+
fn addToExposedScope(
2544+
self: *Self,
2545+
exposes: AST.Collection.Idx,
2546+
) std.mem.Allocator.Error!void {
2547+
const gpa = self.env.gpa;
2548+
25342549
const collection = self.parse_ir.store.getCollection(exposes);
25352550
const exposed_items = self.parse_ir.store.exposedItemSlice(.{ .span = collection.span });
25362551

@@ -2654,6 +2669,42 @@ fn createExposedScope(
26542669
}
26552670
}
26562671

2672+
/// Add platform provides items to the exposed scope.
2673+
/// Platform provides uses curly braces { main_for_host!: "main" } so it's parsed as record fields.
2674+
/// The string value is the FFI symbol name exported to the host (becomes roc__<symbol>).
2675+
fn addPlatformProvidesItems(
2676+
self: *Self,
2677+
provides: AST.Collection.Idx,
2678+
) std.mem.Allocator.Error!void {
2679+
const gpa = self.env.gpa;
2680+
2681+
const collection = self.parse_ir.store.getCollection(provides);
2682+
const record_fields = self.parse_ir.store.recordFieldSlice(.{ .span = collection.span });
2683+
2684+
for (record_fields) |field_idx| {
2685+
const field = self.parse_ir.store.getRecordField(field_idx);
2686+
2687+
// Get the identifier text from the field name token
2688+
if (self.parse_ir.tokens.resolveIdentifier(field.name)) |ident_idx| {
2689+
// Add to exposed_items for permanent storage
2690+
try self.env.addExposedById(ident_idx);
2691+
2692+
// Add to exposed_scope so it becomes an export
2693+
const dummy_idx = @as(Pattern.Idx, @enumFromInt(0));
2694+
try self.exposed_scope.put(gpa, .ident, ident_idx, dummy_idx);
2695+
2696+
// Also track in exposed_ident_texts
2697+
const token_region = self.parse_ir.tokens.resolve(@intCast(field.name));
2698+
const ident_text = self.parse_ir.env.source[token_region.start.offset..token_region.end.offset];
2699+
const region = self.parse_ir.tokenizedRegionToRegion(field.region);
2700+
_ = try self.exposed_ident_texts.getOrPut(gpa, ident_text);
2701+
if (self.exposed_ident_texts.getPtr(ident_text)) |ptr| {
2702+
ptr.* = region;
2703+
}
2704+
}
2705+
}
2706+
}
2707+
26572708
/// Process the requires_signatures from a platform header.
26582709
///
26592710
/// This extracts the required type signatures (like `main! : () => {}`) from the platform
@@ -8238,7 +8289,6 @@ fn canonicalizeBlock(self: *Self, e: AST.Block) std.mem.Allocator.Error!Canonica
82388289
// canonicalize the expr directly without adding it as a statement
82398290
switch (ast_stmt) {
82408291
.expr => |expr_stmt| {
8241-
//
82428292
last_expr = try self.canonicalizeExprOrMalformed(expr_stmt.expr);
82438293
},
82448294
.dbg => |dbg_stmt| {

src/canonicalize/ModuleEnv.zig

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ pub fn init(gpa: std.mem.Allocator, source: []const u8) std.mem.Allocator.Error!
440440
.external_decls = try CIR.ExternalDecl.SafeList.initCapacity(gpa, 16),
441441
.imports = CIR.Import.Store.init(),
442442
.module_name = undefined, // Will be set later during canonicalization
443-
.module_name_idx = undefined, // Will be set later during canonicalization
443+
.module_name_idx = Ident.Idx.NONE, // Will be set later during canonicalization
444444
.diagnostics = CIR.Diagnostic.Span{ .span = base.DataSpan{ .start = 0, .len = 0 } },
445445
.store = try NodeStore.initCapacity(gpa, 10_000), // Default node store capacity
446446
.evaluation_order = null, // Will be set after canonicalization completes
@@ -2624,12 +2624,19 @@ pub fn getMethodIdent(self: *const Self, type_name: []const u8, method_name: []c
26242624
const qualified = std.fmt.bufPrint(&buf, "{s}.{s}", .{ type_name, method_name }) catch return null;
26252625
return self.getIdentStoreConst().findByString(qualified);
26262626
} else {
2627-
// Need to add module prefix
2627+
// Try module-qualified name first (e.g., "Builtin.Num.U64.from_numeral")
26282628
const qualified = std.fmt.bufPrint(&buf, "{s}.{s}.{s}", .{ self.module_name, type_name, method_name }) catch return null;
2629-
return self.getIdentStoreConst().findByString(qualified);
2629+
if (self.getIdentStoreConst().findByString(qualified)) |idx| {
2630+
return idx;
2631+
}
2632+
// Fallback: try without module prefix (e.g., "Color.as_str" for app-defined types)
2633+
// This handles the case where methods are registered with just the type-qualified name
2634+
const simple_qualified = std.fmt.bufPrint(&buf, "{s}.{s}", .{ type_name, method_name }) catch return null;
2635+
return self.getIdentStoreConst().findByString(simple_qualified);
26302636
}
26312637
} else {
26322638
// Use heap allocation for large identifiers (rare case)
2639+
// Try module-qualified name first
26332640
const qualified = if (type_name.len > self.module_name.len and
26342641
std.mem.startsWith(u8, type_name, self.module_name) and
26352642
type_name[self.module_name.len] == '.')
@@ -2639,7 +2646,19 @@ pub fn getMethodIdent(self: *const Self, type_name: []const u8, method_name: []c
26392646
else
26402647
std.fmt.allocPrint(self.gpa, "{s}.{s}.{s}", .{ self.module_name, type_name, method_name }) catch return null;
26412648
defer self.gpa.free(qualified);
2642-
return self.getIdentStoreConst().findByString(qualified);
2649+
if (self.getIdentStoreConst().findByString(qualified)) |idx| {
2650+
return idx;
2651+
}
2652+
// Fallback for the module-qualified case
2653+
if (type_name.len <= self.module_name.len or
2654+
!std.mem.startsWith(u8, type_name, self.module_name) or
2655+
type_name[self.module_name.len] != '.')
2656+
{
2657+
const simple_qualified = std.fmt.allocPrint(self.gpa, "{s}.{s}", .{ type_name, method_name }) catch return null;
2658+
defer self.gpa.free(simple_qualified);
2659+
return self.getIdentStoreConst().findByString(simple_qualified);
2660+
}
2661+
return null;
26432662
}
26442663
}
26452664

src/check/Check.zig

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,15 +1121,10 @@ pub fn checkPlatformRequirements(
11211121
// Instantiate the copied variable before unifying (to avoid poisoning the cached copy)
11221122
const instantiated_required_var = try self.instantiateVar(copied_required_var, &env, .{ .explicit = required_type.region });
11231123

1124-
// Create a copy of the export's type for unification.
1125-
// This prevents unification failure from corrupting the app's actual types
1126-
// (which would cause the interpreter to fail when trying to get layouts).
1127-
const export_copy = try self.copyVar(export_var, self.cir, required_type.region);
1128-
const instantiated_export_copy = try self.instantiateVar(export_copy, &env, .{ .explicit = required_type.region });
1129-
1130-
// Unify the platform's required type with the COPY of the app's export type.
1131-
// The platform type is the "expected" type, app export copy is "actual".
1132-
_ = try self.unifyFromAnno(instantiated_required_var, instantiated_export_copy, &env);
1124+
// Unify the platform's required type with the app's export type.
1125+
// This constrains type variables in the export (e.g., closure params)
1126+
// to match the platform's expected types.
1127+
_ = try self.unifyFromAnno(instantiated_required_var, export_var, &env);
11331128
}
11341129
// Note: If the export is not found, the canonicalizer should have already reported an error
11351130
}

src/cli/app_stub.zig

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -161,12 +161,12 @@ fn addRocCallAbiStub(
161161
wip.cursor = .{ .block = entry };
162162

163163
// Generate actual implementation based on function name
164-
if (std.mem.eql(u8, name, "addInts")) {
164+
if (std.mem.eql(u8, name, "add_ints")) {
165165
try addIntsImplementation(&wip, llvm_builder);
166-
} else if (std.mem.eql(u8, name, "multiplyInts")) {
166+
} else if (std.mem.eql(u8, name, "multiply_ints")) {
167167
try multiplyIntsImplementation(&wip, llvm_builder);
168-
} else if (std.mem.eql(u8, name, "processString")) {
169-
// processString not supported in cross-compilation stubs - only int platform supported
168+
} else if (std.mem.eql(u8, name, "process_string")) {
169+
// process_string not supported in cross-compilation stubs - only int platform supported
170170
_ = try wip.retVoid();
171171
} else {
172172
// Default: just return void for unknown functions
@@ -180,11 +180,11 @@ fn addRocCallAbiStub(
180180
pub fn getTestPlatformEntrypoints(allocator: Allocator, platform_type: []const u8) ![]PlatformEntrypoint {
181181
if (std.mem.eql(u8, platform_type, "int")) {
182182
// Based on test/int/platform/host.zig:
183-
// extern fn roc__addInts(ops: *builtins.host_abi.RocOps, ret_ptr: *anyopaque, arg_ptr: ?*anyopaque) callconv(.c) void;
184-
// extern fn roc__multiplyInts(ops: *builtins.host_abi.RocOps, ret_ptr: *anyopaque, arg_ptr: ?*anyopaque) callconv(.c) void;
183+
// extern fn roc__add_ints(ops: *builtins.host_abi.RocOps, ret_ptr: *anyopaque, arg_ptr: ?*anyopaque) callconv(.c) void;
184+
// extern fn roc__multiply_ints(ops: *builtins.host_abi.RocOps, ret_ptr: *anyopaque, arg_ptr: ?*anyopaque) callconv(.c) void;
185185
const entrypoints = try allocator.alloc(PlatformEntrypoint, 2);
186-
entrypoints[0] = PlatformEntrypoint{ .name = "addInts" };
187-
entrypoints[1] = PlatformEntrypoint{ .name = "multiplyInts" };
186+
entrypoints[0] = PlatformEntrypoint{ .name = "add_ints" };
187+
entrypoints[1] = PlatformEntrypoint{ .name = "multiply_ints" };
188188
return entrypoints;
189189
}
190190

src/cli/main.zig

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -625,7 +625,8 @@ fn generatePlatformHostShim(allocs: *Allocators, cache_dir: []const u8, entrypoi
625625
}
626626

627627
// Create the complete platform shim
628-
platform_host_shim.createInterpreterShim(&llvm_builder, entrypoints.items) catch |err| {
628+
// Note: Symbol names include platform-specific prefixes (underscore for macOS)
629+
platform_host_shim.createInterpreterShim(&llvm_builder, entrypoints.items, target) catch |err| {
629630
std.log.err("Failed to create interpreter shim: {}", .{err});
630631
return err;
631632
};
@@ -1103,15 +1104,16 @@ fn runWithWindowsHandleInheritance(allocs: *Allocators, exe_path: []const u8, sh
11031104
_ = ipc.platform.windows.CloseHandle(process_info.hProcess);
11041105
_ = ipc.platform.windows.CloseHandle(process_info.hThread);
11051106

1106-
// Check exit code
1107+
// Check exit code and propagate to parent
11071108
if (exit_code != 0) {
1108-
std.log.err("Child process {s} exited with code: {}", .{ exe_path, exit_code });
1109+
std.log.debug("Child process {s} exited with code: {}", .{ exe_path, exit_code });
11091110
if (exit_code == 0xC0000005) { // STATUS_ACCESS_VIOLATION
11101111
std.log.err("Child process crashed with access violation (segfault)", .{});
11111112
} else if (exit_code >= 0xC0000000) { // NT status codes for exceptions
11121113
std.log.err("Child process crashed with exception code: 0x{X}", .{exit_code});
11131114
}
1114-
return error.ProcessExitedWithError;
1115+
// Propagate the exit code (truncated to u8 for compatibility)
1116+
std.process.exit(@truncate(exit_code));
11151117
}
11161118

11171119
std.log.debug("Child process completed successfully", .{});
@@ -1198,9 +1200,9 @@ fn runWithPosixFdInheritance(allocs: *Allocators, exe_path: []const u8, shm_hand
11981200
if (exit_code == 0) {
11991201
std.log.debug("Child process completed successfully", .{});
12001202
} else {
1201-
// The host exited with an error - it should have printed any error messages
1203+
// Propagate the exit code from the child process to our parent
12021204
std.log.debug("Child process {s} exited with code: {}", .{ temp_exe_path, exit_code });
1203-
return error.ProcessExitedWithError;
1205+
std.process.exit(exit_code);
12041206
}
12051207
},
12061208
.Signal => |signal| {
@@ -1212,7 +1214,8 @@ fn runWithPosixFdInheritance(allocs: *Allocators, exe_path: []const u8, shm_hand
12121214
} else if (signal == 9) { // SIGKILL
12131215
std.log.err("Child process was killed (SIGKILL)", .{});
12141216
}
1215-
return error.ProcessKilledBySignal;
1217+
// Standard POSIX convention: exit with 128 + signal number
1218+
std.process.exit(128 +| @as(u8, @truncate(signal)));
12161219
},
12171220
.Stopped => |signal| {
12181221
std.log.err("Child process {s} stopped by signal: {}", .{ temp_exe_path, signal });
@@ -1358,6 +1361,10 @@ pub fn setupSharedMemoryWithModuleEnv(allocs: *Allocators, roc_file_path: []cons
13581361
entry_count: u32,
13591362
def_indices_offset: u64,
13601363
module_envs_offset: u64,
1364+
/// Offset to platform's main.roc env (0 if no platform, entry points are in app)
1365+
platform_main_env_offset: u64,
1366+
/// Offset to app env (always present, used for e_lookup_required resolution)
1367+
app_env_offset: u64,
13611368
};
13621369

13631370
const header_ptr = try shm_allocator.create(Header);
@@ -1624,7 +1631,24 @@ pub fn setupSharedMemoryWithModuleEnv(allocs: *Allocators, roc_file_path: []cons
16241631
// Store app env at the last index (N-1, after platform modules at 0..N-2)
16251632
module_env_offsets_ptr[total_module_count - 1] = @intFromPtr(app_env_ptr) - @intFromPtr(shm.base_ptr);
16261633

1627-
const exports_slice = app_env.store.sliceDefs(app_env.exports);
1634+
// Store app env offset for e_lookup_required resolution
1635+
header_ptr.app_env_offset = @intFromPtr(app_env_ptr) - @intFromPtr(shm.base_ptr);
1636+
1637+
// Entry points are defined in the platform's `provides` section.
1638+
// The platform wraps app-provided functions (from `requires`) and exports them for the host.
1639+
// For example: `provides { main_for_host!: "main" }` where `main_for_host! = main!`
1640+
const platform_env = platform_main_env orelse {
1641+
std.log.err("No platform found. Every Roc app requires a platform.", .{});
1642+
return error.NoPlatformFound;
1643+
};
1644+
const exports_slice = platform_env.store.sliceDefs(platform_env.exports);
1645+
if (exports_slice.len == 0) {
1646+
std.log.err("Platform has no exports in `provides` clause.", .{});
1647+
return error.NoEntrypointFound;
1648+
}
1649+
1650+
// Store platform env offset for entry point lookups
1651+
header_ptr.platform_main_env_offset = @intFromPtr(platform_env) - @intFromPtr(shm.base_ptr);
16281652
header_ptr.entry_count = @intCast(exports_slice.len);
16291653

16301654
const def_indices_ptr = try shm_allocator.alloc(u32, exports_slice.len);
@@ -2347,15 +2371,39 @@ fn extractEntrypointsFromPlatform(allocs: *Allocators, roc_file_path: []const u8
23472371
const provides_coll = parse_ast.store.getCollection(platform_header.provides);
23482372
const provides_fields = parse_ast.store.recordFieldSlice(.{ .span = provides_coll.span });
23492373

2350-
// Extract all field names as entrypoints
2374+
// Extract FFI symbol names from provides clause
2375+
// Format: `provides { roc_identifier: "ffi_symbol_name" }`
2376+
// The string value specifies the symbol name exported to the host (becomes roc__<symbol>)
23512377
for (provides_fields) |field_idx| {
23522378
const field = parse_ast.store.getRecordField(field_idx);
2353-
const field_name = parse_ast.resolve(field.name);
2354-
// Strip trailing '!' from effectful function names for the exported symbol
2355-
const symbol_name = if (std.mem.endsWith(u8, field_name, "!"))
2356-
field_name[0 .. field_name.len - 1]
2357-
else
2358-
field_name;
2379+
2380+
// Require explicit string value for symbol name
2381+
const symbol_name = if (field.value) |value_idx| blk: {
2382+
const value_expr = parse_ast.store.getExpr(value_idx);
2383+
switch (value_expr) {
2384+
.string => |str_like| {
2385+
const parts = parse_ast.store.exprSlice(str_like.parts);
2386+
if (parts.len > 0) {
2387+
const first_part = parse_ast.store.getExpr(parts[0]);
2388+
switch (first_part) {
2389+
.string_part => |sp| break :blk parse_ast.resolve(sp.token),
2390+
else => {},
2391+
}
2392+
}
2393+
std.log.err("Invalid provides entry: string value is empty", .{});
2394+
return error.InvalidProvidesEntry;
2395+
},
2396+
.string_part => |str_part| break :blk parse_ast.resolve(str_part.token),
2397+
else => {
2398+
std.log.err("Invalid provides entry: expected string value for symbol name", .{});
2399+
return error.InvalidProvidesEntry;
2400+
},
2401+
}
2402+
} else {
2403+
const field_name = parse_ast.resolve(field.name);
2404+
std.log.err("Provides entry '{s}' missing symbol name. Use format: {{ {s}: \"symbol_name\" }}", .{ field_name, field_name });
2405+
return error.InvalidProvidesEntry;
2406+
};
23592407
try entrypoints.append(try allocs.arena.dupe(u8, symbol_name));
23602408
}
23612409

0 commit comments

Comments
 (0)