ReviewOS

also looking at this

home-lang/home

fix(interpreter): decode escape sequences in interpolated strings

#934
Open mkantautas wants to merge fix/interpreter-interpolation-escapes into main
2 files +35 -5

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

packages/interpreter/src/interpreter.zigmodified+13-5
Changes to packages/interpreter/src/interpreter.zig
@@ -1200,7 +1200,7 @@ pub const Interpreter = struct {
12001200 },
12011201 .StringLiteral => |lit| {
12021202 // Process escape sequences in string literals
1203 const processed = try self.processStringEscapes(lit.value);
1203 const processed = try self.processStringEscapes(lit.value, false);
12041204 return Value{ .String = processed };
12051205 },
12061206 .InterpolatedString => |interp| {
@@ -1208,8 +1208,9 @@ pub const Interpreter = struct {
12081208 var result = std.ArrayList(u8).empty;
12091209 defer result.deinit(self.arena.allocator());
12101210
1211 // Add first part
1212 try result.appendSlice(self.arena.allocator(), interp.parts[0]);
1211 // Add first part. Parts keep their escape sequences as
1212 // written, like string literals, so decode them the same way.
1213 try result.appendSlice(self.arena.allocator(), try self.processStringEscapes(interp.parts[0], true));
12131214
12141215 // Interleave expressions and remaining parts
12151216 for (interp.expressions, 0..) |interp_expr, i| {
@@ -1219,7 +1220,7 @@ pub const Interpreter = struct {
12191220
12201221 // Add next part if it exists
12211222 if (i + 1 < interp.parts.len) {
1222 try result.appendSlice(self.arena.allocator(), interp.parts[i + 1]);
1223 try result.appendSlice(self.arena.allocator(), try self.processStringEscapes(interp.parts[i + 1], true));
12231224 }
12241225 }
12251226
@@ -12075,7 +12076,9 @@ pub const Interpreter = struct {
1207512076
1207612077 /// Process escape sequences in a string literal
1207712078 /// Converts \n, \t, \r, \\, \", \', \0, \xNN to their actual character values
12078 fn processStringEscapes(self: *Interpreter, input: []const u8) InterpreterError![]const u8 {
12079 /// `interpolated` decodes `\$`, which only interpolated strings accept as
12080 /// an escape (so a literal `${` can be written).
12081 fn processStringEscapes(self: *Interpreter, input: []const u8, interpolated: bool) InterpreterError![]const u8 {
1207912082 // Quick check: if no backslashes, return as-is
1208012083 var has_escape = false;
1208112084 for (input) |c| {
@@ -12118,6 +12121,11 @@ pub const Interpreter = struct {
1211812121 try result.append(self.arena.allocator(), '\'');
1211912122 i += 2;
1212012123 },
12124 '$' => {
12125 if (!interpolated) try result.append(self.arena.allocator(), '\\');
12126 try result.append(self.arena.allocator(), '$');
12127 i += 2;
12128 },
1212112129 '0' => {
1212212130 try result.append(self.arena.allocator(), 0);
1212312131 i += 2;
packages/interpreter/tests/program_eval_test.zigmodified+22-0
Changes to packages/interpreter/tests/program_eval_test.zig
@@ -89,6 +89,28 @@ test "eval: finite recursive calls remain valid" {
8989 );
9090}
9191
92test "eval: interpolated strings decode escape sequences" {
93 try runProgram(
94 \\fn main() {
95 \\ let x = 1
96 \\ assert(len("\n${x}") == 2)
97 \\ assert("a\t${x}\tb" == "a\t1\tb")
98 \\ assert("q: \"${x}\"" == "q: \"1\"")
99 \\ assert(len("\${x} is ${x}") == 9)
100 \\}
101 );
102}
103
104test "eval: raw and triple-quoted strings keep a backslash before $" {
105 try runProgram(
106 \\fn main() {
107 \\ assert(r"\$".contains("\\"))
108 \\ assert(r#"\$"#.contains("\\"))
109 \\ assert("""\$""".contains("\\"))
110 \\}
111 );
112}
113
92114test "eval: runaway user recursion returns a runtime error" {
93115 try testing.expectError(error.RuntimeError, runProgram(
94116 \\fn recurse(n: i32): i32 {