From b9dd8cf338247d4b62d612fc8acf5f96a221edca Mon Sep 17 00:00:00 2001 From: Luke Boswell Date: Mon, 1 Dec 2025 21:26:39 +1100 Subject: [PATCH 1/2] fix semantics for Str.split_on --- src/canonicalize/Expression.zig | 2 +- test/fx/multiline_split_leak.roc | 19 +++++++++++++++++++ .../repl/multiline_string_split_7_lines.md | 16 ++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 test/fx/multiline_split_leak.roc create mode 100644 test/snapshots/repl/multiline_string_split_7_lines.md diff --git a/src/canonicalize/Expression.zig b/src/canonicalize/Expression.zig index c94f8b50ae..1e20c92ef8 100644 --- a/src/canonicalize/Expression.zig +++ b/src/canonicalize/Expression.zig @@ -812,9 +812,9 @@ pub const Expr = union(enum) { .str_reserve => &.{ .consume, .borrow }, .str_release_excess_capacity => &.{.consume}, .str_join_with => &.{ .consume, .borrow }, // list consumed, separator borrowed - .str_split_on => &.{ .consume, .borrow }, // String operations - borrowing with seamless slice result (incref internally) + .str_split_on => &.{ .borrow, .borrow }, .str_to_utf8 => &.{.borrow}, .str_drop_prefix, .str_drop_suffix => &.{ .borrow, .borrow }, diff --git a/test/fx/multiline_split_leak.roc b/test/fx/multiline_split_leak.roc new file mode 100644 index 0000000000..4fe2841a2b --- /dev/null +++ b/test/fx/multiline_split_leak.roc @@ -0,0 +1,19 @@ +app [main!] { pf: platform "./platform/main.roc" } + +import pf.Stdout + +input = + \\This is a longer line number one + \\This is a longer line number two + \\L68 + \\L30 + \\R48 + \\L5 + \\R60 + \\L55 + \\L1 + +main! = || { + for line in input.split_on("\n") + Stdout.line!(line) +} diff --git a/test/snapshots/repl/multiline_string_split_7_lines.md b/test/snapshots/repl/multiline_string_split_7_lines.md new file mode 100644 index 0000000000..d772e80b76 --- /dev/null +++ b/test/snapshots/repl/multiline_string_split_7_lines.md @@ -0,0 +1,16 @@ +# META +~~~ini +description=Multiline string with 7 lines split - memory leak test +type=repl +~~~ +# SOURCE +~~~roc +» input = "L68\nL30\nR48\nL5\nR60\nL55\nL1" +» input.split_on("\n") +~~~ +# OUTPUT +assigned `input` +--- +["L68", "L30", "R48", "L5", "R60", "L55", "L1"] +# PROBLEMS +NIL From 25e2e0a403fd7030a9577b62140f22e6900fa7cd Mon Sep 17 00:00:00 2001 From: Luke Boswell Date: Tue, 2 Dec 2025 07:16:29 +1100 Subject: [PATCH 2/2] add test for fx split leak --- src/cli/test/fx_platform_test.zig | 42 +++++++++++++++++++++++++++++++ test/fx/multiline_split_leak.roc | 1 + 2 files changed, 43 insertions(+) diff --git a/src/cli/test/fx_platform_test.zig b/src/cli/test/fx_platform_test.zig index 1fdf2acd18..43179f691d 100644 --- a/src/cli/test/fx_platform_test.zig +++ b/src/cli/test/fx_platform_test.zig @@ -745,3 +745,45 @@ test "string literal pattern matching" { try testing.expect(has_alice); try testing.expect(has_bob); } + +test "multiline string split_on" { + // Tests splitting a multiline string and iterating over the lines. + // This is a regression test to ensure split_on works correctly with + // multiline strings and doesn't cause memory issues. + const allocator = testing.allocator; + + try ensureRocBinary(allocator); + + const run_result = try std.process.Child.run(.{ + .allocator = allocator, + .argv = &[_][]const u8{ + "./zig-out/bin/roc", + "test/fx/multiline_split_leak.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 the output contains lines from the multiline string + try testing.expect(std.mem.indexOf(u8, run_result.stdout, "This is a longer line number one") != null); + try testing.expect(std.mem.indexOf(u8, run_result.stdout, "This is a longer line number two") != null); + try testing.expect(std.mem.indexOf(u8, run_result.stdout, "L68") != null); + try testing.expect(std.mem.indexOf(u8, run_result.stdout, "The last line is here") != null); +} diff --git a/test/fx/multiline_split_leak.roc b/test/fx/multiline_split_leak.roc index 4fe2841a2b..1b94734d37 100644 --- a/test/fx/multiline_split_leak.roc +++ b/test/fx/multiline_split_leak.roc @@ -12,6 +12,7 @@ input = \\R60 \\L55 \\L1 + \\The last line is here main! = || { for line in input.split_on("\n")