diff --git a/src/cli/main.zig b/src/cli/main.zig index 37d3cfda08..cb95012a36 100644 --- a/src/cli/main.zig +++ b/src/cli/main.zig @@ -1353,22 +1353,37 @@ pub fn setupSharedMemoryWithModuleEnv(allocs: *Allocators, roc_file_path: []cons defer builtin_modules.deinit(); // Try to find and compile platform modules - const app_dir = std.fs.path.dirname(roc_file_path) orelse "."; - const platform_dir = try std.fs.path.join(allocs.gpa, &[_][]const u8{ app_dir, "platform" }); - defer allocs.gpa.free(platform_dir); + // Extract the actual platform path from the app header to support paths like "../platform/main.roc" + const app_dir = std.fs.path.dirname(roc_file_path) orelse return error.InvalidAppPath; - const platform_main_path = try std.fs.path.join(allocs.gpa, &[_][]const u8{ platform_dir, "main.roc" }); - defer allocs.gpa.free(platform_main_path); + const platform_spec = try extractPlatformSpecFromApp(allocs, roc_file_path); + + // Resolve relative paths (starting with ./ or ../) relative to app directory + // Non-relative paths (like package URLs) are not local platform files + const platform_main_path: ?[]const u8 = if (std.mem.startsWith(u8, platform_spec, "./") or std.mem.startsWith(u8, platform_spec, "../")) + try std.fs.path.join(allocs.gpa, &[_][]const u8{ app_dir, platform_spec }) + else + null; + defer if (platform_main_path) |p| allocs.gpa.free(p); + + // Get the platform directory from the resolved path + const platform_dir: ?[]const u8 = if (platform_main_path) |p| + std.fs.path.dirname(p) orelse return error.InvalidPlatformPath + else + null; // Extract exposed modules from the platform header (if platform exists) var exposed_modules = std.ArrayList([]const u8).empty; defer exposed_modules.deinit(allocs.gpa); - var has_platform = true; - extractExposedModulesFromPlatform(allocs, platform_main_path, &exposed_modules) catch { - // No platform found - that's fine, just continue with no platform modules - has_platform = false; - }; + var has_platform = false; + if (platform_main_path) |pmp| { + has_platform = true; + extractExposedModulesFromPlatform(allocs, pmp, &exposed_modules) catch { + // Platform file not found or couldn't be parsed - continue without platform modules + has_platform = false; + }; + } // IMPORTANT: Create header FIRST before any module compilation. // The interpreter_shim expects the Header to be at FIRST_ALLOC_OFFSET (504). @@ -1402,10 +1417,13 @@ pub fn setupSharedMemoryWithModuleEnv(allocs: *Allocators, roc_file_path: []cons defer allocs.gpa.free(platform_env_ptrs); for (exposed_modules.items, 0..) |module_name, i| { + // platform_dir is guaranteed to be non-null if exposed_modules is non-empty + // because we only populate exposed_modules when platform_main_path is non-null + const plat_dir = platform_dir orelse unreachable; const module_filename = try std.fmt.allocPrint(allocs.gpa, "{s}.roc", .{module_name}); defer allocs.gpa.free(module_filename); - const module_path = try std.fs.path.join(allocs.gpa, &[_][]const u8{ platform_dir, module_filename }); + const module_path = try std.fs.path.join(allocs.gpa, &[_][]const u8{ plat_dir, module_filename }); defer allocs.gpa.free(module_path); const module_env_ptr = try compileModuleToSharedMemory( @@ -1459,9 +1477,10 @@ pub fn setupSharedMemoryWithModuleEnv(allocs: *Allocators, roc_file_path: []cons if (has_platform) { // Cast []*ModuleEnv to []const *ModuleEnv for the function parameter const const_platform_env_ptrs: []const *ModuleEnv = platform_env_ptrs; + // platform_main_path is guaranteed non-null when has_platform is true platform_main_env = compileModuleToSharedMemory( allocs, - platform_main_path, + platform_main_path.?, "main.roc", shm_allocator, &builtin_modules, diff --git a/src/cli/test/fx_platform_test.zig b/src/cli/test/fx_platform_test.zig index 225d16d061..ec269c6810 100644 --- a/src/cli/test/fx_platform_test.zig +++ b/src/cli/test/fx_platform_test.zig @@ -119,6 +119,60 @@ test "fx platform effectful functions" { try testing.expect(std.mem.indexOf(u8, run_result.stderr, "Line 3 to stdout") == null); } +test "fx platform with dotdot starting path" { + const allocator = testing.allocator; + + try ensureRocBinary(allocator); + + // Run the app from a subdirectory that uses ../ at the START of its platform path + // This tests that relative paths starting with .. are handled correctly + // Bug: paths starting with ../ fail with TypeMismatch, while ./path/../ works + const run_result = try std.process.Child.run(.{ + .allocator = allocator, + .argv = &[_][]const u8{ + "./zig-out/bin/roc", + "test/fx/subdir/app.roc", + }, + }); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + switch (run_result.term) { + .Exited => |code| { + if (code != 0) { + std.debug.print("Run failed with exit code {}\n", .{code}); + std.debug.print("STDOUT: {s}\n", .{run_result.stdout}); + std.debug.print("STDERR: {s}\n", .{run_result.stderr}); + return error.RunFailed; + } + }, + else => { + std.debug.print("Run terminated abnormally: {}\n", .{run_result.term}); + std.debug.print("STDOUT: {s}\n", .{run_result.stdout}); + std.debug.print("STDERR: {s}\n", .{run_result.stderr}); + return error.RunFailed; + }, + } + + // Verify stdout contains expected messages + try testing.expect(std.mem.indexOf(u8, run_result.stdout, "Hello from stdout!") != null); + try testing.expect(std.mem.indexOf(u8, run_result.stdout, "Line 1 to stdout") != null); + try testing.expect(std.mem.indexOf(u8, run_result.stdout, "Line 3 to stdout") != null); + + // Verify stderr contains expected messages + try testing.expect(std.mem.indexOf(u8, run_result.stderr, "Error from stderr!") != null); + try testing.expect(std.mem.indexOf(u8, run_result.stderr, "Line 2 to stderr") != null); + + // Verify stderr messages are NOT in stdout + try testing.expect(std.mem.indexOf(u8, run_result.stdout, "Error from stderr!") == null); + try testing.expect(std.mem.indexOf(u8, run_result.stdout, "Line 2 to stderr") == null); + + // Verify stdout messages are NOT in stderr + try testing.expect(std.mem.indexOf(u8, run_result.stderr, "Hello from stdout!") == null); + try testing.expect(std.mem.indexOf(u8, run_result.stderr, "Line 1 to stdout") == null); + try testing.expect(std.mem.indexOf(u8, run_result.stderr, "Line 3 to stdout") == null); +} + test "fx platform stdin to stdout" { const allocator = testing.allocator; diff --git a/test/fx/subdir/app.roc b/test/fx/subdir/app.roc new file mode 100644 index 0000000000..67f13cb127 --- /dev/null +++ b/test/fx/subdir/app.roc @@ -0,0 +1,15 @@ +app [main!] { pf: platform "../platform/main.roc" } + +import pf.Stdout +import pf.Stderr + +str : Str -> Str +str = |s| s + +main! = || { + Stdout.line!(str("Hello from stdout!")) + Stdout.line!(str("Line 1 to stdout")) + Stderr.line!(str("Line 2 to stderr")) + Stdout.line!(str("Line 3 to stdout")) + Stderr.line!(str("Error from stderr!")) +}