Skip to content

Commit 477799c

Browse files
committed
fix(regex): accept annex b control escapes
Test262 flips through zig-js: - test/annexB/built-ins/RegExp: 5 fail/57 pass -> 0 fail/62 pass (+5) - test/annexB: 61 fail/1025 pass -> 56 fail/1030 pass (+5) Verification: - zig test --dep regex -Mroot=tests/parser_compiler_edge_cases.zig -Mregex=src/root.zig --test-filter "annex b character class escapes" - zig test --dep regex -Mroot=tests/parser_compiler_edge_cases.zig -Mregex=src/root.zig - zig run --dep regex -Mroot=/private/tmp/zig_regex_probe.zig -Mregex=/Users/chris/Code/Libraries/zig-regex/src/root.zig - (zig-js) zig build test262-bin - (zig-js) ./zig-out/bin/test262 --diag test/annexB/built-ins/RegExp - (zig-js) ./zig-out/bin/test262 --diag test/language/literals/regexp - (zig-js) ./zig-out/bin/test262 --diag test/annexB - git diff --check - git diff --cached --check Notes: - zig build test in zig-regex was interrupted after the benchmark shard stayed silent; the focused parser/compiler file passed with explicit module wiring. - test/language/literals/regexp still has 3 existing braced-quantifier negative failures.
1 parent 773dc0c commit 477799c

2 files changed

Lines changed: 48 additions & 3 deletions

File tree

src/parser.zig

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,16 @@ pub const Lexer = struct {
137137
};
138138
}
139139

140+
fn literalWithPending(self: *Lexer, first: u8, rest: []const u8) Token {
141+
self.pending_len = 0;
142+
self.pending_pos = 0;
143+
for (rest) |b| {
144+
self.pending[self.pending_len] = b;
145+
self.pending_len += 1;
146+
}
147+
return self.makeToken(.literal, first);
148+
}
149+
140150
fn parseEscape(self: *Lexer) !Token {
141151
// We've already consumed the backslash
142152
const c = self.advance() orelse return RegexError.UnexpectedEndOfPattern;
@@ -215,7 +225,10 @@ pub const Lexer = struct {
215225
'x' => try self.parseHexEscape(), // \xHH → one byte
216226
'c' => blk: {
217227
// \cX control escape: the control letter's code mod 32.
218-
const x = self.peek() orelse break :blk RegexError.InvalidEscapeSequence;
228+
const x = self.peek() orelse {
229+
if (self.unicode_strict) break :blk RegexError.InvalidEscapeSequence;
230+
break :blk self.literalWithPending('\\', "c");
231+
};
219232
if (std.ascii.isAlphabetic(x)) {
220233
_ = self.advance();
221234
break :blk self.makeToken(.escape_char, x % 32);
@@ -225,7 +238,7 @@ pub const Lexer = struct {
225238
break :blk self.makeToken(.escape_char, x % 32);
226239
}
227240
if (self.unicode_strict) break :blk RegexError.InvalidEscapeSequence;
228-
break :blk self.makeToken(.literal, 'c'); // not a control letter: literal 'c'
241+
break :blk self.literalWithPending('\\', "c"); // Annex B: invalid \c is literal "\c"
229242
},
230243
'u' => try self.parseUnicodeEscape(), // \uHHHH or \u{...} → UTF-8 byte(s)
231244
'p' => self.parsePropertyEscape(false),
@@ -1533,10 +1546,17 @@ pub const Parser = struct {
15331546
.lbrace => '{',
15341547
.rbrace => '}',
15351548
.dollar => '$',
1549+
.caret => '^',
15361550
.lbracket => '[', // Allow [ as literal (for non-POSIX cases)
15371551
.escape_b => 0x08, // Inside a class, \b is backspace, not a word boundary.
1552+
.backref => blk: {
1553+
if (self.unicode or self.unicode_sets) break :blk null;
1554+
var bytes: [8]u8 = undefined;
1555+
const fallback = self.decimalEscapeFallbackBytes(self.current_token, &bytes);
1556+
break :blk if (fallback.len == 0) null else fallback[0];
1557+
},
15381558
// These should not appear here
1539-
.rbracket, .caret, .backslash, .escape_d, .escape_D, .escape_w, .escape_W, .escape_s, .escape_S, .escape_B, .escape_A, .escape_z, .escape_Z, .backref, .escape_p, .escape_P, .eof => null,
1559+
.rbracket, .backslash, .escape_d, .escape_D, .escape_w, .escape_W, .escape_s, .escape_S, .escape_B, .escape_A, .escape_z, .escape_Z, .escape_p, .escape_P, .eof => null,
15401560
};
15411561
}
15421562

tests/parser_compiler_edge_cases.zig

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,12 @@ test "parser: annex b character class escapes and shorthand ranges" {
241241
var outside = try Regex.compile(allocator, "\\c0");
242242
defer outside.deinit();
243243
try std.testing.expect(!try outside.isMatch("\x10"));
244+
try std.testing.expect(try outside.isMatch("\\c0"));
245+
246+
var invalid_control = try Regex.compile(allocator, "\\c\u{0410}");
247+
defer invalid_control.deinit();
248+
try std.testing.expect(!try invalid_control.isMatch("c\u{0410}"));
249+
try std.testing.expect(try invalid_control.isMatch("\\c\u{0410}"));
244250

245251
var control_digit = try Regex.compile(allocator, "[\\c0]");
246252
defer control_digit.deinit();
@@ -250,6 +256,25 @@ test "parser: annex b character class escapes and shorthand ranges" {
250256
defer control_underscore.deinit();
251257
try std.testing.expect(try control_underscore.isMatch("\x1f"));
252258

259+
var invalid_control_class = try Regex.compile(allocator, "[\\c!]");
260+
defer invalid_control_class.deinit();
261+
try std.testing.expect(try invalid_control_class.isMatch("\\"));
262+
try std.testing.expect(try invalid_control_class.isMatch("c"));
263+
try std.testing.expect(try invalid_control_class.isMatch("!"));
264+
try std.testing.expect(!try invalid_control_class.isMatch("\x03"));
265+
266+
var caret_literal = try Regex.compile(allocator, "[0-9A-Za-z_\\$(|)\\[\\]\\/\\\\^]");
267+
defer caret_literal.deinit();
268+
try std.testing.expect(try caret_literal.isMatch("^"));
269+
270+
var decimal_range = try Regex.compile(allocator, "[\\d][\\12-\\14]{1,}[^\\d]");
271+
defer decimal_range.deinit();
272+
if (try decimal_range.find("line1\n\n\n\n\nline2")) |match| {
273+
var owned = match;
274+
defer owned.deinit(allocator);
275+
try std.testing.expectEqualStrings("1\n\n\n\n\nl", match.slice);
276+
} else return error.TestExpectedMatch;
277+
253278
var range_left_shorthand = try Regex.compile(allocator, "[\\d-a]+");
254279
defer range_left_shorthand.deinit();
255280
if (try range_left_shorthand.find(":a0123456789-:")) |match| {

0 commit comments

Comments
 (0)