1- // JavaScript grammar for Monogram.
1+ // JavaScript grammar for Monogram — the STANDALONE BASE of the ECMAScript family .
22//
3- // JavaScript is a syntactic subset of TypeScript, so this grammar is the TS
4- // grammar (examples/typescript.ts) with every type construct removed: no type
3+ // JavaScript is the syntactic SUBSET of TypeScript (TS = JS + a type layer), so
4+ // this file owns the shared, type-free ECMAScript *vocabulary* — the token set,
5+ // the `notReserved`/`notReservedExpr` reserved-word guards, the precedence ladder
6+ // (`ecmaPrec`), and the JS scope map (`jsScopes`) — and exports it. The TS grammar
7+ // (the sibling superset file) then imports that vocabulary and EXTENDS it with the
8+ // type layer. The dependency runs subset → superset only: this file imports nothing
9+ // from the TS grammar (it has no type knowledge) and must stand alone — its only
10+ // import is the engine's combinator API.
11+ //
12+ // The rules are NOT shared either direction: combinator rules bind their
13+ // references at definition time, so a JS rule must reference the OTHER JS rule
14+ // consts — it can't reuse TS's rule objects (and vice-versa). Each file therefore
15+ // keeps its own rule consts; only the vocabulary above is shared.
16+ //
17+ // This grammar is the TS grammar with every type construct removed: no type
518// annotations, type parameters/arguments, `interface`/`type`/`enum`/`namespace`/
619// `declare`/`module`-as-type declarations, `as`/`satisfies`/`<T>`-cast/non-null
720// (`!`) expressions, and no type-optional `?` on params/members. Everything that
1023// operator set (`??`, `?.`, `**`, optional chaining), destructuring, template
1124// literals (tagged + interpolated), regex, every module import/export VALUE form,
1225// control flow, numeric literals, and Stage-3 decorators on classes.
13- //
14- // The type-free *vocabulary* (tokens, the precedence ladder) is imported from
15- // typescript.ts rather than duplicated; the rules are copied and stripped here
16- // because combinator rules bind their references at definition time — a JS rule
17- // must reference the OTHER JS rule consts, so it can't reuse the TS rule objects.
1826
1927import {
20- rule , defineGrammar ,
28+ token , rule , defineGrammar ,
2129 left , right , none , noUnaryLhs ,
2230 op , prefix , postfix , sameLine ,
2331 sep , opt , many , many1 , alt , exclude , not ,
2432} from '../src/api.ts' ;
25- import {
26- ecmaTokens , ecmaPrec ,
33+
34+ // ── Tokens ──
35+
36+ const Ident = token ( / (?: [ a - z A - Z _ $ ] | \\ u [ 0 - 9 a - f A - F ] { 4 } | \\ u \{ [ 0 - 9 a - f A - F ] + \} ) (?: [ a - z A - Z 0 - 9 _ $ ] | \\ u [ 0 - 9 a - f A - F ] { 4 } | \\ u \{ [ 0 - 9 a - f A - F ] + \} ) * / , { identifier : true } ) ;
37+ // Numeric tokens end with `(?![0-9A-Za-z_$\\])`: the spec rule that a numeric literal
38+ // may not be immediately followed by an IdentifierStart or DecimalDigit. Without it,
39+ // `0b2`/`0B1102110`/`0o81010` would munch a valid prefix (`0b1`, `0B110`) and leave the
40+ // rest as a second token, so the file parses as two statements instead of being rejected.
41+ // With it the bad literal matches no token and the lexer throws — the correct rejection.
42+ // (ASCII IdentifierStart + `\` for `\u`-escapes; the lexer compiles patterns without the
43+ // /u flag so \p{L} is unavailable, and every affected conformance case is ASCII.)
44+ const HexNumber = token ( / 0 [ x X ] [ 0 - 9 a - f A - F ] + ( _ [ 0 - 9 a - f A - F ] + ) * (? ! [ 0 - 9 A - Z a - z _ $ \\ ] ) / , { scope : 'constant.numeric.hex' } ) ;
45+ const OctalNumber = token ( / 0 [ o O ] [ 0 - 7 ] + ( _ [ 0 - 7 ] + ) * (? ! [ 0 - 9 A - Z a - z _ $ \\ ] ) / , { scope : 'constant.numeric.octal' } ) ;
46+ const BinaryNumber = token ( / 0 [ b B ] [ 0 1 ] + ( _ [ 0 1 ] + ) * (? ! [ 0 - 9 A - Z a - z _ $ \\ ] ) / , { scope : 'constant.numeric.binary' } ) ;
47+ const BigInt_ = token ( / [ 0 - 9 ] + ( _ [ 0 - 9 ] + ) * n (? ! [ 0 - 9 A - Z a - z _ $ \\ ] ) / , { scope : 'constant.numeric.bigint' } ) ;
48+ const Number_ = token ( / [ 0 - 9 ] + ( _ [ 0 - 9 ] + ) * (?: \. [ 0 - 9 ] * ( _ [ 0 - 9 ] + ) * ) ? (?: [ e E ] [ + - ] ? [ 0 - 9 ] + ( _ [ 0 - 9 ] + ) * ) ? (? ! [ 0 - 9 A - Z a - z _ $ \\ ] ) / ) ;
49+ // A well-formed JS escape, used in the string-body pattern below. `\u`/`\x` must
50+ // match their strict forms — a `\u{cp}` with cp ≤ 0x10FFFF, a 4-hex `\uXXXX`, or a
51+ // 2-hex `\xXX` — while `\` + any *other* char (\n, \\, \q non-escape, line
52+ // continuation) stays valid via `[^ux]`. A malformed `\u`/`\x` (e.g. `\u{110000}`,
53+ // `\u{r}`, `\u{}`, `\u{67`) matches no escape, so the string matches no token and the
54+ // lexer throws — TS's exact rejection. The in-range codepoint is `0*` leading zeros
55+ // then 1–5 hex (0–0xFFFFF) or `10`+4 hex (0x100000–0x10FFFF).
56+ const codePoint = String . raw `0*(?:[0-9a-fA-F]{1,5}|10[0-9a-fA-F]{4})` ;
57+ const escape = String . raw `\\(?:u\{${ codePoint } \}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|[^ux])` ;
58+ const String_ = token ( new RegExp ( `"(?:[^"\\\\]|${ escape } )*"|'(?:[^'\\\\]|${ escape } )*'` ) , {
59+ string : true ,
60+ escape : / \\ (?: [ n r t b f v 0 ' " \\ ] | x [ 0 - 9 a - f A - F ] { 2 } | u [ 0 - 9 a - f A - F ] { 4 } | u \{ [ 0 - 9 a - f A - F ] + \} ) / ,
61+ } ) ;
62+ const Template = token ( / ` (?: [ ^ ` \\ $ ] | \\ .| \$ (? ! \{ ) ) * ` / , {
63+ escape : / \\ (?: [ n r t b f v 0 ' " \\ ` $ ] | x [ 0 - 9 a - f A - F ] { 2 } | u [ 0 - 9 a - f A - F ] { 4 } | u \{ [ 0 - 9 a - f A - F ] + \} ) / ,
64+ // Same well-formed-escape rule as strings; the lexer rejects a malformed `\u`/`\x`
65+ // in an *untagged* template (`\u{110000}`, `\u{r}`), but allows it when tagged.
66+ escapeValid : new RegExp ( escape ) ,
67+ template : { open : '`' , interpOpen : '${' , interpClose : '}' } ,
68+ } ) ;
69+ const Regex_ = token ( / \/ (?: [ ^ \/ \\ \[ \n ] | \\ .| \[ (?: [ ^ \] \\ \n ] | \\ .) * \] ) + \/ [ g i m s u y d v ] * / , {
70+ regex : true ,
71+ regexContext : {
72+ divisionAfterTypes : [ 'Ident' , 'Number' , 'String' , 'Template' , 'BigInt' ] ,
73+ divisionAfterTexts : [ ')' , ']' , '++' , '--' , 'this' , 'super' , 'true' , 'false' , 'null' , 'undefined' ] ,
74+ regexAfterTexts : [ 'in' , 'of' , 'instanceof' , 'typeof' , 'delete' , 'void' , 'await' , 'yield' , 'throw' , 'return' , 'case' , 'do' , 'else' , 'new' ] ,
75+ // `kw ( … )` heads (control-flow): the closing `)` is a statement head, not a
76+ // value, so `if (a) /re/` parses `/re/` as a regex rather than division.
77+ regexAfterParenKeywords : [ 'if' , 'while' , 'for' , 'with' ] ,
78+ // member accessors: after one, those keywords are property NAMES, so
79+ // `obj.for(x) / y` stays a method call + division.
80+ memberAccessTexts : [ '.' , '?.' ] ,
81+ } ,
82+ } ) ;
83+ const Decorator = token ( / @ (?: [ a - z A - Z _ $ ] [ a - z A - Z 0 - 9 _ $ . ] * ) ? / , { scope : 'entity.name.function.decorator' } ) ;
84+ const PrivateField = token ( / # [ a - z A - Z _ $ ] [ a - z A - Z 0 - 9 _ $ ] * / , { scope : 'variable.other.property' } ) ;
85+ const Shebang = token ( / ^ # ! [ ^ \n ] * / , { skip : true , scope : 'comment.line.shebang' } ) ;
86+ const JSDoc = token ( / \/ \* \* (? ! \/ ) [ \s \S ] * ?\* \/ / , { skip : true , scope : 'comment.block.documentation' , embed : 'jsdoc' } ) ;
87+ const TripleSlash = token ( / \/ \/ \/ \s * < [ ^ \n ] * / , { skip : true , scope : 'comment.line.triple-slash' } ) ;
88+ const LineComment = token ( / \/ \/ [ ^ \n ] * / , { skip : true } ) ;
89+ const BlockComment = token ( / \/ \* [ \s \S ] * ?\* \/ / , { skip : true } ) ;
90+
91+ // The token consts, reserved-word guards, precedence ladder, and scope map are
92+ // pure ECMAScript vocabulary — no rule wiring — so the TS grammar imports them from
93+ // here and extends them rather than duplicating them.
94+ export {
95+ Shebang , JSDoc , TripleSlash , LineComment , BlockComment ,
2796 Ident , HexNumber , OctalNumber , BinaryNumber , BigInt_ ,
2897 Number_ , String_ , Template , Regex_ , Decorator , PrivateField ,
29- notReserved , notReservedExpr ,
30- } from './typescript.ts' ;
98+ } ;
99+
100+ // ── Always-reserved words ──
101+ // The `Ident` token deliberately swallows keywords (they lex as identifiers), so
102+ // every keyword can otherwise fall back to a bare identifier. These words are
103+ // reserved in EVERY context (ECMAScript ReservedWord ∪ TS's always-reserved), so
104+ // they are valid as an identifier NOWHERE — not as an expression, a shorthand
105+ // property, or a binding name. `notReserved` is a zero-width guard placed before an
106+ // identifier position to forbid exactly these. Excluded on purpose: contextual
107+ // keywords (as/async/from/type/of/…) and strict-mode-only reserved words
108+ // (let/static/implements/yield/await/…) — those ARE valid identifiers in some
109+ // context a CFG can't detect (sloppy mode, non-generator/non-async), so forbidding
110+ // them here would reject valid code (`var let = 1`, `function f(yield) {}`).
111+ export const notReserved = not ( alt (
112+ 'break' , 'case' , 'catch' , 'class' , 'const' , 'continue' , 'debugger' , 'default' ,
113+ 'delete' , 'do' , 'else' , 'enum' , 'export' , 'extends' , 'false' , 'finally' , 'for' ,
114+ 'function' , 'if' , 'import' , 'in' , 'instanceof' , 'new' , 'null' , 'return' , 'super' ,
115+ 'switch' , 'this' , 'throw' , 'true' , 'try' , 'typeof' , 'var' , 'void' , 'while' , 'with' ,
116+ ) ) ;
117+
118+ // A NARROWER guard for the *expression* identifier-NUD only. The full `notReserved`
119+ // set can NOT be used at expression position: most always-reserved words legitimately
120+ // begin an expression via their own dedicated forms (`new`/`new.target`, `class`/
121+ // `function` expressions, `import(…)`/`import<T>`, `super`, `this`, `true`/`false`/
122+ // `null`, …), and TS's own error-recovery tolerates several reserved words sliding into
123+ // the bare-identifier fallback inside otherwise-valid files (e.g. `export default …`,
124+ // undeclared `for (x in …)`, `class … extends (e)`, a decorator before `export`). The
125+ // words below have NO such role: they are the prefix operators `void`/`typeof`/`delete`
126+ // (which must take an operand) plus the `catch`/`throw` keywords and `enum`. Forbidding
127+ // the bare-identifier fallback for exactly these rejects `catch(x){}` with no `try`,
128+ // `void ;`/`typeof ;`/`delete ;` (operatorless prefix op), and `throw ;` — while leaving
129+ // every valid expression (and TS's recovery cases) untouched. Verified: widening this
130+ // set to other reserved words regresses valid code; these five are the FN-safe maximum.
131+ export const notReservedExpr = not ( alt (
132+ 'catch' , 'delete' , 'enum' , 'throw' , 'typeof' , 'void' ,
133+ ) ) ;
134+
135+ // ── Precedence ladder (shared ECMAScript operator precedence) ──
136+
137+ export const ecmaPrec = [
138+ right ( '=' , '+=' , '-=' , '*=' , '/=' , '%=' , '**=' , '<<=' , '>>=' , '>>>=' , '&=' , '|=' , '^=' ) ,
139+ right ( '??=' , '||=' , '&&=' ) ,
140+ left ( '??' ) ,
141+ left ( '||' ) ,
142+ left ( '&&' ) ,
143+ left ( '|' ) ,
144+ left ( '^' ) ,
145+ left ( '&' ) ,
146+ none ( '==' , '!=' , '===' , '!==' ) ,
147+ none ( '<' , '>' , '<=' , '>=' ) ,
148+ left ( '<<' , '>>' , '>>>' ) ,
149+ left ( '+' , '-' ) ,
150+ left ( '*' , '/' , '%' ) ,
151+ right ( noUnaryLhs ( '**' ) ) , // `-x ** y` is a syntax error: a unary-prefix expr can't be a `**` LHS
152+ right ( prefix ( '!' , '~' , '+' , '-' , 'typeof' , 'void' , 'delete' , 'await' , 'yield' ) ) ,
153+ right ( prefix ( '++' , '--' ) ) ,
154+ left ( postfix ( '++' , '--' ) ) ,
155+ ] ;
31156
32157// ── Decorators ──
33158// Stage-3 / JS-real decorators: `@dec` and `@dec(args)`. The TS-only
@@ -296,13 +421,63 @@ const Program = rule($ => [
296421 many ( alt ( Decl , Stmt ) ) , // Decl first: prefer declaration over IIFE expression-statement
297422] ) ;
298423
424+ // ── Scope map ──
425+ // The JS scope map: scopes every real-JS token. The TS grammar imports this and
426+ // extends it with the type-only scope keys (storage.type.interface/type/enum/
427+ // namespace, the keyof/as/satisfies/is/infer/asserts keyword.operator.expression
428+ // entries, support.type.primitive) — those names are plain identifiers in JS, not
429+ // types, so they are deliberately absent here.
430+ export const jsScopes = {
431+ 'keyword.control.conditional' : [ 'if' , 'else' , 'switch' , 'case' , 'default' ] ,
432+ 'keyword.control.loop' : [ 'for' , 'while' , 'do' , 'in' , 'of' ] ,
433+ 'keyword.control.flow' : [ 'return' , 'break' , 'continue' , 'await' , 'yield' ] ,
434+ 'keyword.control.trycatch' : [ 'try' , 'catch' , 'finally' , 'throw' ] ,
435+ 'keyword.control' : [ 'debugger' , 'with' ] ,
436+ 'keyword.control.import' : [ 'import' , 'export' , 'from' ] ,
437+ 'storage.type' : [ 'let' , 'const' , 'var' , 'using' ] ,
438+ 'storage.type.function' : [ 'function' , 'constructor' ] ,
439+ 'storage.type.class' : [ 'class' ] ,
440+ 'storage.modifier' : [ 'static' , 'async' , 'accessor' ] ,
441+ 'storage.type.property' : [ 'get' , 'set' ] ,
442+ 'keyword.other.extends' : [ 'extends' ] ,
443+ 'keyword.operator.expression' : [ 'instanceof' , 'new' , 'delete' , 'void' , 'typeof' ] ,
444+ 'keyword.operator.assignment' : [ '=' , '+=' , '-=' , '*=' , '/=' , '%=' , '**=' , '<<=' , '>>=' , '>>>=' , '&=' , '|=' , '^=' , '??=' , '||=' , '&&=' ] ,
445+ 'keyword.operator.comparison' : [ '==' , '!=' , '===' , '!==' ] ,
446+ 'keyword.operator.logical' : [ '||' , '&&' , '??' ] ,
447+ 'keyword.operator.arithmetic' : [ '+' , '-' , '*' , '/' , '%' , '**' ] ,
448+ 'keyword.operator.increment-decrement' : [ '++' , '--' ] ,
449+ 'keyword.operator.logical.prefix' : [ '!' , '~' ] ,
450+ 'keyword.operator.bitwise' : [ '|' , '&' , '^' ] ,
451+ 'keyword.operator.bitwise.shift' : [ '<<' , '>>' , '>>>' ] ,
452+ 'storage.type.function.arrow' : [ '=>' ] ,
453+ 'punctuation.bracket.round' : [ '(' , ')' ] ,
454+ 'punctuation.bracket.curly' : [ '{' , '}' ] ,
455+ 'punctuation.bracket.square' : [ '[' , ']' ] ,
456+ 'punctuation.accessor' : [ '.' ] ,
457+ 'punctuation.accessor.optional' : [ '?.' ] ,
458+ 'punctuation.terminator.statement' : [ ';' ] ,
459+ 'punctuation.separator.comma' : [ ',' ] ,
460+ 'constant.language.boolean' : [ 'true' , 'false' ] ,
461+ 'constant.language.null' : [ 'null' , 'undefined' ] ,
462+ 'variable.language' : [ 'this' , 'super' ] ,
463+ 'support.class' : [ 'Promise' , 'Array' , 'Map' , 'Set' , 'WeakMap' , 'WeakSet' , 'Error' , 'RegExp' , 'Date' , 'Object' , 'Function' , 'Symbol' ] ,
464+ 'support.variable' : [ 'console' , 'window' , 'document' , 'process' , 'module' , 'require' , 'exports' , 'global' , 'globalThis' ] ,
465+ 'support.variable.property' : [ '.length' , '.prototype' , '.constructor' ] ,
466+ } ;
467+
299468// ── Grammar ──
300469
301470export default defineGrammar ( {
302471 name : 'javascript' ,
303472 scopeName : 'source.js' ,
304473
305- tokens : ecmaTokens ,
474+ tokens : {
475+ // Comments must come before Regex_ to avoid /** ... */ being matched as regex
476+ Shebang, JSDoc, TripleSlash, LineComment, BlockComment,
477+ Ident, HexNumber, OctalNumber, BinaryNumber, BigInt : BigInt_ ,
478+ Number : Number_ , String : String_ , Template, Regex : Regex_ ,
479+ Decorator, PrivateField,
480+ } ,
306481
307482 prec : ecmaPrec ,
308483
@@ -317,47 +492,7 @@ export default defineGrammar({
317492 Program,
318493 } ,
319494
320- // JS subset of ecmaScopes: dropped storage.type.interface/type/enum/namespace,
321- // the type-operator keyword.operator.expression entries (keyof/as/satisfies/is/
322- // infer/asserts), and support.type.primitive (those names are plain identifiers
323- // in JS, not types). Kept everything that scopes real JS tokens.
324- scopes : {
325- 'keyword.control.conditional' : [ 'if' , 'else' , 'switch' , 'case' , 'default' ] ,
326- 'keyword.control.loop' : [ 'for' , 'while' , 'do' , 'in' , 'of' ] ,
327- 'keyword.control.flow' : [ 'return' , 'break' , 'continue' , 'await' , 'yield' ] ,
328- 'keyword.control.trycatch' : [ 'try' , 'catch' , 'finally' , 'throw' ] ,
329- 'keyword.control' : [ 'debugger' , 'with' ] ,
330- 'keyword.control.import' : [ 'import' , 'export' , 'from' ] ,
331- 'storage.type' : [ 'let' , 'const' , 'var' , 'using' ] ,
332- 'storage.type.function' : [ 'function' , 'constructor' ] ,
333- 'storage.type.class' : [ 'class' ] ,
334- 'storage.modifier' : [ 'static' , 'async' , 'accessor' ] ,
335- 'storage.type.property' : [ 'get' , 'set' ] ,
336- 'keyword.other.extends' : [ 'extends' ] ,
337- 'keyword.operator.expression' : [ 'instanceof' , 'new' , 'delete' , 'void' , 'typeof' ] ,
338- 'keyword.operator.assignment' : [ '=' , '+=' , '-=' , '*=' , '/=' , '%=' , '**=' , '<<=' , '>>=' , '>>>=' , '&=' , '|=' , '^=' , '??=' , '||=' , '&&=' ] ,
339- 'keyword.operator.comparison' : [ '==' , '!=' , '===' , '!==' ] ,
340- 'keyword.operator.logical' : [ '||' , '&&' , '??' ] ,
341- 'keyword.operator.arithmetic' : [ '+' , '-' , '*' , '/' , '%' , '**' ] ,
342- 'keyword.operator.increment-decrement' : [ '++' , '--' ] ,
343- 'keyword.operator.logical.prefix' : [ '!' , '~' ] ,
344- 'keyword.operator.bitwise' : [ '|' , '&' , '^' ] ,
345- 'keyword.operator.bitwise.shift' : [ '<<' , '>>' , '>>>' ] ,
346- 'storage.type.function.arrow' : [ '=>' ] ,
347- 'punctuation.bracket.round' : [ '(' , ')' ] ,
348- 'punctuation.bracket.curly' : [ '{' , '}' ] ,
349- 'punctuation.bracket.square' : [ '[' , ']' ] ,
350- 'punctuation.accessor' : [ '.' ] ,
351- 'punctuation.accessor.optional' : [ '?.' ] ,
352- 'punctuation.terminator.statement' : [ ';' ] ,
353- 'punctuation.separator.comma' : [ ',' ] ,
354- 'constant.language.boolean' : [ 'true' , 'false' ] ,
355- 'constant.language.null' : [ 'null' , 'undefined' ] ,
356- 'variable.language' : [ 'this' , 'super' ] ,
357- 'support.class' : [ 'Promise' , 'Array' , 'Map' , 'Set' , 'WeakMap' , 'WeakSet' , 'Error' , 'RegExp' , 'Date' , 'Object' , 'Function' , 'Symbol' ] ,
358- 'support.variable' : [ 'console' , 'window' , 'document' , 'process' , 'module' , 'require' , 'exports' , 'global' , 'globalThis' ] ,
359- 'support.variable.property' : [ '.length' , '.prototype' , '.constructor' ] ,
360- } ,
495+ scopes : jsScopes ,
361496
362497 entry : Program ,
363498} ) ;
0 commit comments