Skip to content

Commit 42ed333

Browse files
committed
Reject numeric literals followed by ident/digit (JS maximal-munch rule)
A numeric literal may not be immediately followed by an IdentifierStart or DecimalDigit. Without enforcing this, bad literals like 0b21010 / 0B1102110 / 0o81010 munched a valid prefix and left the rest as a second token, so the file parsed as two statements instead of being rejected. Append a negative lookahead (?![0-9A-Za-z_$\\]) to every numeric token; the bad literal then matches no token and the lexer throws — the correct rejection. Surfaced and fixed a latent gap: the exponent allowed no numeric separator, so 1.1e10_0 only 'parsed' by splitting into 1.1e10 + _0; exponent now allows (_[0-9]+)* like the rest. FP 120->116 (7 over-accepts now rejected incl. 3 multi-file separator tests), FN stays 0 (3376/3376 valid-code), TP unchanged. Artifacts regenerated.
1 parent 3ee9785 commit 42ed333

6 files changed

Lines changed: 46 additions & 31 deletions

File tree

examples/lezer/tokens.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
// JSDoc: /\/\*\*(?!\/)[\s\S]*?\*\// (skip)
1313
// BlockComment: /\/\*[\s\S]*?\*\// (skip)
1414
// Ident: /(?:[a-zA-Z_$]|\\u[0-9a-fA-F]{4}|\\u\{[0-9a-fA-F]+\})(?:[a-zA-Z0-9_$]|\\u[0-9a-fA-F]{4}|\\u\{[0-9a-fA-F]+\})*/
15-
// Number: /[0-9]+(_[0-9]+)*(?:\.[0-9]*(_[0-9]+)*)?(?:[eE][+-]?[0-9]+)?/
15+
// HexNumber: /0[xX][0-9a-fA-F]+(_[0-9a-fA-F]+)*(?![0-9A-Za-z_$\\])/
16+
// OctalNumber: /0[oO][0-7]+(_[0-7]+)*(?![0-9A-Za-z_$\\])/
17+
// BinaryNumber: /0[bB][01]+(_[01]+)*(?![0-9A-Za-z_$\\])/
18+
// BigInt: /[0-9]+(_[0-9]+)*n(?![0-9A-Za-z_$\\])/
19+
// Number: /[0-9]+(_[0-9]+)*(?:\.[0-9]*(_[0-9]+)*)?(?:[eE][+-]?[0-9]+(_[0-9]+)*)?(?![0-9A-Za-z_$\\])/
1620
// String: /"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*'/
1721
// Decorator: /@(?:[a-zA-Z_$][a-zA-Z0-9_$.]*)?/
1822
//

examples/lezer/typescript.grammar

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -254,11 +254,11 @@ Program {
254254
LineComment { "/""/"![\n]* }
255255
// INCOMPLETE: BlockComment block span "\\/\\*"…"\\*\\/" → external tokenizer.
256256
// INCOMPLETE: Ident regex /(?:[a-zA-Z_$]|\\u[0-9a-fA-F]{4}|\\u\{[0-9a-fA-F]+\})(?:[a-zA-Z0-9_$]|\\u[0-9a-fA-F]{4}|\\u\{[0-9a-fA-F]+\})*/ exceeds Lezer token syntax; external tokenizer.
257-
HexNumber { "0"$[xX]$[0-9a-fA-F]+("_"$[0-9a-fA-F]+)* }
258-
OctalNumber { "0"$[oO]$[0-7]+("_"$[0-7]+)* }
259-
BinaryNumber { "0"$[bB]$[01]+("_"$[01]+)* }
260-
BigInt { $[0-9]+("_"$[0-9]+)*"n" }
261-
// INCOMPLETE: Number regex /[0-9]+(_[0-9]+)*(?:\.[0-9]*(_[0-9]+)*)?(?:[eE][+-]?[0-9]+)?/ exceeds Lezer token syntax; external tokenizer.
257+
// INCOMPLETE: HexNumber regex /0[xX][0-9a-fA-F]+(_[0-9a-fA-F]+)*(?![0-9A-Za-z_$\\])/ exceeds Lezer token syntax; external tokenizer.
258+
// INCOMPLETE: OctalNumber regex /0[oO][0-7]+(_[0-7]+)*(?![0-9A-Za-z_$\\])/ exceeds Lezer token syntax; external tokenizer.
259+
// INCOMPLETE: BinaryNumber regex /0[bB][01]+(_[01]+)*(?![0-9A-Za-z_$\\])/ exceeds Lezer token syntax; external tokenizer.
260+
// INCOMPLETE: BigInt regex /[0-9]+(_[0-9]+)*n(?![0-9A-Za-z_$\\])/ exceeds Lezer token syntax; external tokenizer.
261+
// INCOMPLETE: Number regex /[0-9]+(_[0-9]+)*(?:\.[0-9]*(_[0-9]+)*)?(?:[eE][+-]?[0-9]+(_[0-9]+)*)?(?![0-9A-Za-z_$\\])/ exceeds Lezer token syntax; external tokenizer.
262262
// INCOMPLETE: String regex /"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*'/ exceeds Lezer token syntax; external tokenizer.
263263
// INCOMPLETE: Template is produced by the external tokenizer
264264
// (context-sensitive: template interpolation).
@@ -329,6 +329,10 @@ Program {
329329
JSDoc,
330330
BlockComment,
331331
Ident,
332+
HexNumber,
333+
OctalNumber,
334+
BinaryNumber,
335+
BigInt,
332336
Number,
333337
String,
334338
Template,

examples/tree-sitter/grammar.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -163,15 +163,15 @@ module.exports = grammar({
163163

164164
ident: $ => token(/(?:[a-zA-Z_$]|\\u[0-9a-fA-F]{4}|\\u\{[0-9a-fA-F]+\})(?:[a-zA-Z0-9_$]|\\u[0-9a-fA-F]{4}|\\u\{[0-9a-fA-F]+\})*/),
165165

166-
hex_number: $ => token(/0[xX][0-9a-fA-F]+(_[0-9a-fA-F]+)*/),
166+
hex_number: $ => token(/0[xX][0-9a-fA-F]+(_[0-9a-fA-F]+)*(?![0-9A-Za-z_$\\])/),
167167

168-
octal_number: $ => token(/0[oO][0-7]+(_[0-7]+)*/),
168+
octal_number: $ => token(/0[oO][0-7]+(_[0-7]+)*(?![0-9A-Za-z_$\\])/),
169169

170-
binary_number: $ => token(/0[bB][01]+(_[01]+)*/),
170+
binary_number: $ => token(/0[bB][01]+(_[01]+)*(?![0-9A-Za-z_$\\])/),
171171

172-
big_int: $ => token(/[0-9]+(_[0-9]+)*n/),
172+
big_int: $ => token(/[0-9]+(_[0-9]+)*n(?![0-9A-Za-z_$\\])/),
173173

174-
number: $ => token(/[0-9]+(_[0-9]+)*(?:\.[0-9]*(_[0-9]+)*)?(?:[eE][+-]?[0-9]+)?/),
174+
number: $ => token(/[0-9]+(_[0-9]+)*(?:\.[0-9]*(_[0-9]+)*)?(?:[eE][+-]?[0-9]+(_[0-9]+)*)?(?![0-9A-Za-z_$\\])/),
175175

176176
string: $ => token(/"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*'/),
177177

examples/typescript.monarch.json

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -520,35 +520,35 @@
520520
}
521521
],
522522
[
523-
"0[xX][0-9a-fA-F]+(_[0-9a-fA-F]+)*",
523+
"0[xX][0-9a-fA-F]+(_[0-9a-fA-F]+)*(?![0-9A-Za-z_$\\\\])",
524524
{
525525
"token": "number.hex",
526526
"switchTo": "@value"
527527
}
528528
],
529529
[
530-
"0[oO][0-7]+(_[0-7]+)*",
530+
"0[oO][0-7]+(_[0-7]+)*(?![0-9A-Za-z_$\\\\])",
531531
{
532532
"token": "number.octal",
533533
"switchTo": "@value"
534534
}
535535
],
536536
[
537-
"0[bB][01]+(_[01]+)*",
537+
"0[bB][01]+(_[01]+)*(?![0-9A-Za-z_$\\\\])",
538538
{
539539
"token": "number.binary",
540540
"switchTo": "@value"
541541
}
542542
],
543543
[
544-
"[0-9]+(_[0-9]+)*n",
544+
"[0-9]+(_[0-9]+)*n(?![0-9A-Za-z_$\\\\])",
545545
{
546546
"token": "number",
547547
"switchTo": "@value"
548548
}
549549
],
550550
[
551-
"[0-9]+(_[0-9]+)*(?:\\.[0-9]*(_[0-9]+)*)?(?:[eE][+-]?[0-9]+)?",
551+
"[0-9]+(_[0-9]+)*(?:\\.[0-9]*(_[0-9]+)*)?(?:[eE][+-]?[0-9]+(_[0-9]+)*)?(?![0-9A-Za-z_$\\\\])",
552552
{
553553
"token": "number.float",
554554
"switchTo": "@value"
@@ -1079,23 +1079,23 @@
10791079
],
10801080
"interpExprBody": [
10811081
[
1082-
"0[xX][0-9a-fA-F]+(_[0-9a-fA-F]+)*",
1082+
"0[xX][0-9a-fA-F]+(_[0-9a-fA-F]+)*(?![0-9A-Za-z_$\\\\])",
10831083
"number.hex"
10841084
],
10851085
[
1086-
"0[oO][0-7]+(_[0-7]+)*",
1086+
"0[oO][0-7]+(_[0-7]+)*(?![0-9A-Za-z_$\\\\])",
10871087
"number.octal"
10881088
],
10891089
[
1090-
"0[bB][01]+(_[01]+)*",
1090+
"0[bB][01]+(_[01]+)*(?![0-9A-Za-z_$\\\\])",
10911091
"number.binary"
10921092
],
10931093
[
1094-
"[0-9]+(_[0-9]+)*n",
1094+
"[0-9]+(_[0-9]+)*n(?![0-9A-Za-z_$\\\\])",
10951095
"number"
10961096
],
10971097
[
1098-
"[0-9]+(_[0-9]+)*(?:\\.[0-9]*(_[0-9]+)*)?(?:[eE][+-]?[0-9]+)?",
1098+
"[0-9]+(_[0-9]+)*(?:\\.[0-9]*(_[0-9]+)*)?(?:[eE][+-]?[0-9]+(_[0-9]+)*)?(?![0-9A-Za-z_$\\\\])",
10991099
"number.float"
11001100
],
11011101
[

examples/typescript.tmLanguage.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -434,23 +434,23 @@
434434
},
435435
"hexnumber": {
436436
"name": "constant.numeric.hex.typescript",
437-
"match": "0[xX][0-9a-fA-F]+(_[0-9a-fA-F]+)*"
437+
"match": "0[xX][0-9a-fA-F]+(_[0-9a-fA-F]+)*(?![0-9A-Za-z_$\\\\])"
438438
},
439439
"octalnumber": {
440440
"name": "constant.numeric.octal.typescript",
441-
"match": "0[oO][0-7]+(_[0-7]+)*"
441+
"match": "0[oO][0-7]+(_[0-7]+)*(?![0-9A-Za-z_$\\\\])"
442442
},
443443
"binarynumber": {
444444
"name": "constant.numeric.binary.typescript",
445-
"match": "0[bB][01]+(_[01]+)*"
445+
"match": "0[bB][01]+(_[01]+)*(?![0-9A-Za-z_$\\\\])"
446446
},
447447
"bigint": {
448448
"name": "constant.numeric.bigint.typescript",
449-
"match": "[0-9]+(_[0-9]+)*n"
449+
"match": "[0-9]+(_[0-9]+)*n(?![0-9A-Za-z_$\\\\])"
450450
},
451451
"number": {
452452
"name": "constant.numeric.float.typescript",
453-
"match": "[0-9]+(_[0-9]+)*(?:\\.[0-9]*(_[0-9]+)*)?(?:[eE][+-]?[0-9]+)?"
453+
"match": "[0-9]+(_[0-9]+)*(?:\\.[0-9]*(_[0-9]+)*)?(?:[eE][+-]?[0-9]+(_[0-9]+)*)?(?![0-9A-Za-z_$\\\\])"
454454
},
455455
"string-double": {
456456
"name": "string.quoted.double.typescript",

examples/typescript.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,18 @@ import {
88
// ── Tokens ──
99

1010
const Ident = token(/(?:[a-zA-Z_$]|\\u[0-9a-fA-F]{4}|\\u\{[0-9a-fA-F]+\})(?:[a-zA-Z0-9_$]|\\u[0-9a-fA-F]{4}|\\u\{[0-9a-fA-F]+\})*/, { identifier: true });
11-
const HexNumber = token(/0[xX][0-9a-fA-F]+(_[0-9a-fA-F]+)*/, { scope: 'constant.numeric.hex' });
12-
const OctalNumber = token(/0[oO][0-7]+(_[0-7]+)*/, { scope: 'constant.numeric.octal' });
13-
const BinaryNumber = token(/0[bB][01]+(_[01]+)*/, { scope: 'constant.numeric.binary' });
14-
const BigInt_ = token(/[0-9]+(_[0-9]+)*n/, { scope: 'constant.numeric.bigint' });
15-
const Number_ = token(/[0-9]+(_[0-9]+)*(?:\.[0-9]*(_[0-9]+)*)?(?:[eE][+-]?[0-9]+)?/);
11+
// Numeric tokens end with `(?![0-9A-Za-z_$\\])`: the spec rule that a numeric literal
12+
// may not be immediately followed by an IdentifierStart or DecimalDigit. Without it,
13+
// `0b2`/`0B1102110`/`0o81010` would munch a valid prefix (`0b1`, `0B110`) and leave the
14+
// rest as a second token, so the file parses as two statements instead of being rejected.
15+
// With it the bad literal matches no token and the lexer throws — the correct rejection.
16+
// (ASCII IdentifierStart + `\` for `\u`-escapes; the lexer compiles patterns without the
17+
// /u flag so \p{L} is unavailable, and every affected conformance case is ASCII.)
18+
const HexNumber = token(/0[xX][0-9a-fA-F]+(_[0-9a-fA-F]+)*(?![0-9A-Za-z_$\\])/, { scope: 'constant.numeric.hex' });
19+
const OctalNumber = token(/0[oO][0-7]+(_[0-7]+)*(?![0-9A-Za-z_$\\])/, { scope: 'constant.numeric.octal' });
20+
const BinaryNumber = token(/0[bB][01]+(_[01]+)*(?![0-9A-Za-z_$\\])/, { scope: 'constant.numeric.binary' });
21+
const BigInt_ = token(/[0-9]+(_[0-9]+)*n(?![0-9A-Za-z_$\\])/, { scope: 'constant.numeric.bigint' });
22+
const Number_ = token(/[0-9]+(_[0-9]+)*(?:\.[0-9]*(_[0-9]+)*)?(?:[eE][+-]?[0-9]+(_[0-9]+)*)?(?![0-9A-Za-z_$\\])/);
1623
const String_ = token(/"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*'/, {
1724
string: true,
1825
escape: /\\(?:[nrtbfv0'"\\]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|u\{[0-9a-fA-F]+\})/,

0 commit comments

Comments
 (0)