also looking at this
fix(interpreter): decode escape sequences in interpolated strings
#934
2 files
+35
-5
| @@ -1200,7 +1200,7 @@ pub const Interpreter = struct { | ||
| 1200 | 1200 | }, |
| 1201 | 1201 | .StringLiteral => |lit| { |
| 1202 | 1202 | // Process escape sequences in string literals |
| 1203 | const processed = try self.processStringEscapes(lit.value); | |
| 1203 | const processed = try self.processStringEscapes(lit.value, false); | |
| 1204 | 1204 | return Value{ .String = processed }; |
| 1205 | 1205 | }, |
| 1206 | 1206 | .InterpolatedString => |interp| { |
| @@ -1208,8 +1208,9 @@ pub const Interpreter = struct { | ||
| 1208 | 1208 | var result = std.ArrayList(u8).empty; |
| 1209 | 1209 | defer result.deinit(self.arena.allocator()); |
| 1210 | 1210 | |
| 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)); | |
| 1213 | 1214 | |
| 1214 | 1215 | // Interleave expressions and remaining parts |
| 1215 | 1216 | for (interp.expressions, 0..) |interp_expr, i| { |
| @@ -1219,7 +1220,7 @@ pub const Interpreter = struct { | ||
| 1219 | 1220 | |
| 1220 | 1221 | // Add next part if it exists |
| 1221 | 1222 | 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)); | |
| 1223 | 1224 | } |
| 1224 | 1225 | } |
| 1225 | 1226 | |
| @@ -12075,7 +12076,9 @@ pub const Interpreter = struct { | ||
| 12075 | 12076 | |
| 12076 | 12077 | /// Process escape sequences in a string literal |
| 12077 | 12078 | /// 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 { | |
| 12079 | 12082 | // Quick check: if no backslashes, return as-is |
| 12080 | 12083 | var has_escape = false; |
| 12081 | 12084 | for (input) |c| { |
| @@ -12118,6 +12121,11 @@ pub const Interpreter = struct { | ||
| 12118 | 12121 | try result.append(self.arena.allocator(), '\''); |
| 12119 | 12122 | i += 2; |
| 12120 | 12123 | }, |
| 12124 | '$' => { | |
| 12125 | if (!interpolated) try result.append(self.arena.allocator(), '\\'); | |
| 12126 | try result.append(self.arena.allocator(), '$'); | |
| 12127 | i += 2; | |
| 12128 | }, | |
| 12121 | 12129 | '0' => { |
| 12122 | 12130 | try result.append(self.arena.allocator(), 0); |
| 12123 | 12131 | i += 2; |
| @@ -89,6 +89,28 @@ test "eval: finite recursive calls remain valid" { | ||
| 89 | 89 | ); |
| 90 | 90 | } |
| 91 | 91 | |
| 92 | test "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 | ||
| 104 | test "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 | ||
| 92 | 114 | test "eval: runaway user recursion returns a runtime error" { |
| 93 | 115 | try testing.expectError(error.RuntimeError, runProgram( |
| 94 | 116 | \\fn recurse(n: i32): i32 { |