ReviewOS

zig-utils/zig-js

Errors: RegExp compile failures lose the reason JavaScriptCore reports

#902
Closed chris opened this 11 days ago ·
11 days ago

Parent: #459

Problem

Every RegExp pattern defect collapses to one constant. JavaScriptCore prefixes Invalid regular expression: and names the reason — 12 distinct reasons observed in an 18-case differential.

patternzig-jsJavaScriptCore
/(/invalid regular expressionInvalid regular expression: missing )
/[a/invalid regular expressionInvalid regular expression: missing terminating ] for character class
/a{2,1}/invalid regular expressionInvalid regular expression: numbers out of order in {} quantifier
/*/invalid regular expressionInvalid regular expression: nothing to repeat
/a\\/invalid regular expressionInvalid regular expression: \ at end of pattern
/(?<1>a)/invalid regular expressionInvalid regular expression: invalid group specifier name
/[z-a]/invalid regular expressionInvalid regular expression: range out of order in character class

Other observed reasons: duplicate group specifier name, invalid \k<> named backreference, invalid property expression, invalid escaped character for Unicode pattern, invalid range in character class for Unicode pattern, invalid Unicode code point \u{} escape.

Flags are a separate constant, and JSC uses one string for all of them — unknown flag, duplicate flag, and the u+v conflict alike:

zig-js:          SyntaxError: Invalid regular expression flags
JavaScriptCore:  SyntaxError: Invalid flags supplied to RegExp constructor.

Note the trailing period, which zig-js's version lacks.

Why this one is structural

Unlike the Intl case, this is not a string table. The reason is lost at a module boundary: zig-regex's compile error is flattened to a single value before zig-js sees it. The dependency has to carry a reason discriminant across the boundary — which makes this a change in ../zig-regex as well as in zig-js.

The same flattened message is reached through RegExp.prototype.compile as well as the constructor and literals, so the mapping belongs at the compile-error boundary, not at any one caller.

Verification

scratchpad/sweep/probe.sh, against a real JavaScriptCore backend. Note a regex literal with a bad pattern is a parse-time error, so probe those through eval or the RegExp constructor to keep them catchable.

11 days ago

Investigation notes, and a correction to this issue's premise.

The reason is NOT lost at the module boundary

I wrote above that "zig-regex's compile error is flattened to a single value before zig-js sees it". That is wrong. zig-regex already exposes a discriminated RegexError with 20+ parse variants (src/errors.zig); zig-js simply catches all of them with one blanket catch and emits invalid regular expression.

But the tags are too coarse, and partly mis-assigned

I instrumented the boundary to print @errorName(err) per pattern. One tag stands in for up to four distinct JavaScriptCore reasons:

zig-regex tagJavaScriptCore reasons behind itexample patterns
UnexpectedCharactermissing ) · missing terminating ] for character class · nothing to repeat · unrecognized character after (?( · [a · * + ? · (?
InvalidEscapeSequenceinvalid property expression · invalid escaped character for Unicode pattern · invalid Unicode code point \u{} escape\p{Nope} · \q \x \c · \u{110000}
InvalidCharacterClassinvalid group specifier name · range out of order in character class · invalid range in character class for Unicode pattern(?<1>a) · [z-a] · [\d-a]
InvalidQuantifiernumbers out of order in {} quantifier · nothing to repeata{2,1} · a**
InvalidBackreferenceinvalid \k<> named backreference · invalid backreference for Unicode pattern\k<z> · \1 (u-mode)
UnexpectedEndOfPattern\ at end of pattern · invalid group specifier namea\ · (?<

Note (?<1>a) reports InvalidCharacterClass, which looks like a mis-assignment in zig-regex independent of any message work.

What landed now

Only the three mappings that are provably exact, verified across a wider sample so they are 1:1 rather than 1:1-in-one-example:

  • flags — one constant for every shape (unknown flag, duplicate flag, u+v, non-string argument): Invalid flags supplied to RegExp constructor. — 8/8 shapes confirmed. Note the trailing period zig-js lacked.
  • UnmatchedParenthesis → Invalid regular expression: unmatched parentheses — 6/6.
  • DuplicateGroupName → Invalid regular expression: duplicate group specifier name — 4/4.

The coarse tags are deliberately left emitting the generic string. Emitting a JSC-shaped sentence with the wrong reason would be worse than an obviously different one, and picking a "most common" reason per tag would be guessing.

What the real fix needs

A reason discriminant carried out of zig-regex. Zig error sets cannot carry a payload, so it needs an out-parameter or a field on the compile context, set at roughly 111 throw sites across a 2,606-line parser.zig — comparable in size to #899. That is a two-repo change (zig-utils/zig-regex plus this one) and wants to be its own piece of work rather than a rider on a message sweep.

The table above is the specification for it: 17 target reasons, with example patterns for each.

11 days ago

Implemented and pushed to main in fa49b0f9 (fix(regexp): preserve exact compile diagnostics (#902)).

The dependency side landed first in zig-utils/zig-regex:

  • 4783058 adds an opt-in CompileErrorReason covering 18 exact parse reasons while preserving every existing coarse RegexError tag.
  • 73cc22d keeps diagnostic state off the dependency's default compile path; only opt-in embedders carry it.

zig-js now consumes the reason at RegExp, RegExp.prototype.compile, and regex-literal validation boundaries. The mapping includes missing delimiters, quantifier order/nothing-to-repeat, trailing backslash, group names, duplicate names, class ranges, named/numeric backreferences, property escapes, Unicode escaped-character/\\u/\\u{}/octal cases, and unknown (? extensions. Invalid flags keep JavaScriptCore's one constant with the trailing period.

Evidence:

  • System JavaScriptCore differential: 21/21 exact name/message rows, including constructor, compile, and literal eval paths.
  • zig-regex: focused 19/19 reason matrix, regex root 111/111, and full zig build test pass.
  • zig-js standalone parser root: 217/217 pass.
  • Exact-parent Test262 with the same dependency revision: named groups 36 -> 36, CharacterClassEscapes 12 -> 12, regexp-modifier syntax 8 -> 8, regexp literals 238 -> 238; zero failures on both sides.

The earlier source-audit premise is now resolved without changing the dependency's established error contract or adding a second parser.

Sign in to comment on this issue.