Regression in 69fde05 (#947, streamed lexical goals). I found it bisecting with a parser-only probe (git bisect run, 7 steps, f2bafeb good). Everything below was executed on Debug builds, with node v24.4.1 as the oracle.
After a for statement, a lexer error later in the source is dropped and lexing resumes past the offending character. Invalid programs parse and run:
| program | node | f2bafeb | main (b5fb0ec) |
|---|---|---|---|
var C = class { # x; }; | SyntaxError | UnexpectedCharacter | UnexpectedCharacter |
for (;;) break; then var C = class { # x; }; | SyntaxError | UnexpectedCharacter | parses and runs |
for (;;) break; then var q = 1; # x; q | SyntaxError | UnexpectedCharacter | runs, then ReferenceError for x |
for (var k in {}) ; then var q = 1; \u00; q | SyntaxError | UnexpectedCharacter | OK 1 |
while (0); or an arrow, then # x | SyntaxError | UnexpectedCharacter | UnexpectedCharacter (unaffected) |
Parser.tokens for for (;;) break;\nvar C = class { # x; }; on main is
for ( ; ; ) break ; var C = class { x ; } ; eof: the # is simply missing. That suggests the error is recorded, but a later re-lex resumes from the lexer's cursor, which already stepped past #. The for-only trigger points at classicForHeadAhead keeping its forked lexer on the success path. That is my reading, not verified.
test262: 64 negative tests that pass at f2bafeb fail on main, 32 each in test/language/expressions/class and test/language/statements/class. They are all under elements/syntax/early-errors: grammar-privatename-whitespace-error-*, private-*-cannot-escape-token, and grammar-field-identifier-invalid-zwnj-error. The trigger is the harness: assert.js defines compareArray with a for loop, so every test that follows it has a preceding for. The runner then sees $DONOTEVALUATE() run instead of a parse error.
Minimal pin (parser-only, so it needs no engine link):
try std.testing.expect(rejects("for (;;) break;\nvar C = class { # x; };"));
try std.testing.expect(rejects("for (var k in {}) ;\nvar q = 1; \\u00; q"));@chrisbbreuer — leaving this one to you, since it sits inside the streaming work you are still landing.