|
| 1 | +// JavaScript grammar for Monogram. |
| 2 | +// |
| 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 |
| 5 | +// annotations, type parameters/arguments, `interface`/`type`/`enum`/`namespace`/ |
| 6 | +// `declare`/`module`-as-type declarations, `as`/`satisfies`/`<T>`-cast/non-null |
| 7 | +// (`!`) expressions, and no type-optional `?` on params/members. Everything that |
| 8 | +// is real JavaScript is kept: functions/arrows/classes (static, async, accessor, |
| 9 | +// get/set, generators, private `#x`, static blocks), the full expression and |
| 10 | +// operator set (`??`, `?.`, `**`, optional chaining), destructuring, template |
| 11 | +// literals (tagged + interpolated), regex, every module import/export VALUE form, |
| 12 | +// 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. |
| 18 | + |
| 19 | +import { |
| 20 | + rule, defineGrammar, |
| 21 | + left, right, none, noUnaryLhs, |
| 22 | + op, prefix, postfix, sameLine, |
| 23 | + sep, opt, many, many1, alt, exclude, not, |
| 24 | +} from '../src/api.ts'; |
| 25 | +import { |
| 26 | + ecmaTokens, ecmaPrec, |
| 27 | + Ident, HexNumber, OctalNumber, BinaryNumber, BigInt_, |
| 28 | + Number_, String_, Template, Regex_, Decorator, PrivateField, |
| 29 | + notReserved, notReservedExpr, |
| 30 | +} from './typescript.ts'; |
| 31 | + |
| 32 | +// ── Decorators ── |
| 33 | +// Stage-3 / JS-real decorators: `@dec` and `@dec(args)`. The TS-only |
| 34 | +// `@dec<T>(args)` type-argument form is dropped. |
| 35 | + |
| 36 | +const DecoratorExpr = rule($ => [ |
| 37 | + [Decorator, opt('(', sep(Expr, ','), ')')], // @dec | @dec(args) |
| 38 | +]); |
| 39 | + |
| 40 | +// ── Expressions ── |
| 41 | + |
| 42 | +const Prop = rule($ => { |
| 43 | + const method = ['(', sep(Param, ','), ')', Block]; // ( … ) { … } |
| 44 | + return [ |
| 45 | + ['...', Expr], // spread |
| 46 | + // accessor (get/set) |
| 47 | + [alt('get', 'set'), MemberName, '(', opt(sep(Param, ',')), ')', Block], |
| 48 | + // method: async?/generator?, any member name (incl `#x`, computed `[e]`), then ( … ) { … } |
| 49 | + [opt('async'), opt('*'), MemberName, ...method], |
| 50 | + // value property — any member name incl computed `[e]: v` (MemberName covers `[Expr]`) |
| 51 | + [MemberName, ':', Expr], |
| 52 | + ['[', Expr, many(',', Expr), ']', ':', Expr], // computed comma list (lenient) |
| 53 | + // shorthand (Ident only): x | x = v — a reserved word here is invalid |
| 54 | + // (`var v = { class }`); a reserved word as a property KEY (`{ class: 1 }`) is fine, |
| 55 | + // already handled by the `[MemberName, ':', Expr]` branch above. |
| 56 | + [notReserved, Ident, alt(['=', Expr], [])], |
| 57 | + ]; |
| 58 | +}); |
| 59 | + |
| 60 | +const ClassHeritage = rule($ => [ |
| 61 | + Ident, |
| 62 | + [$, '.', Ident], |
| 63 | + [$, '(', sep(Expr, ','), ')'], |
| 64 | +]); |
| 65 | + |
| 66 | +const NewTarget = rule($ => [ |
| 67 | + Ident, |
| 68 | + [$, '.', Ident], |
| 69 | + [$, '[', Expr, ']'], |
| 70 | + ['(', Expr, ')'], |
| 71 | +]); |
| 72 | + |
| 73 | +const Expr = rule($ => [ |
| 74 | + // A standalone identifier expression, but never a reserved word that has no expression |
| 75 | + // role (see notReservedExpr). This kills the bare-identifier fallback for keywords like |
| 76 | + // `catch`/`throw` and the prefix operators `void`/`typeof`/`delete`, so `catch(x){}` |
| 77 | + // with no `try`, `void ;`, and `throw ;` are rejected as TS does. |
| 78 | + [notReservedExpr, Ident], |
| 79 | + Number_, |
| 80 | + String_, |
| 81 | + Template, |
| 82 | + Regex_, |
| 83 | + 'true', 'false', 'null', 'undefined', 'this', 'super', |
| 84 | + [$, op, $], |
| 85 | + [prefix, $], |
| 86 | + [$, postfix], |
| 87 | + ['...', $], |
| 88 | + [$, '(', sep($, ','), ')'], |
| 89 | + [$, '.', alt(Ident, PrivateField)], |
| 90 | + // optional chaining: ?.x | ?.#x | ?.(args) | ?.[i] | ?.`…` |
| 91 | + [$, '?.', alt(Ident, PrivateField, ['(', sep($, ','), ')'], ['[', $, ']'], Template)], |
| 92 | + [$, '[', $, ']'], |
| 93 | + [$, '?', $, ':', $], |
| 94 | + [$, 'instanceof', $], |
| 95 | + [$, 'in', $], |
| 96 | + [$, Template], |
| 97 | + // new T | new T(args) |
| 98 | + ['new', NewTarget, opt('(', sep($, ','), ')')], |
| 99 | + ['new', 'class', Ident, opt('extends', ClassHeritage), '{', many(ClassMember), '}', opt('(', sep($, ','), ')')], |
| 100 | + ['new', 'class', opt('extends', ClassHeritage), '{', many(ClassMember), '}', opt('(', sep($, ','), ')')], |
| 101 | + ['[', many(opt($), ','), opt($), ']'], |
| 102 | + ['{', sep(Prop, ','), '}'], |
| 103 | + [opt('async'), '(', sep(Param, ','), ')', '=>', alt($, Block)], |
| 104 | + [Ident, '=>', alt($, Block)], |
| 105 | + ['yield', alt(['*', $], [opt($)])], // yield e | yield* e (delegate) | yield |
| 106 | + ['(', $, many(',', $), ')'], |
| 107 | + ['import', alt(['(', $, ')'], ['.', 'meta'])], |
| 108 | + PrivateField, |
| 109 | + HexNumber, OctalNumber, BinaryNumber, BigInt_, |
| 110 | + [opt('async'), 'function', opt('*'), opt(Ident), '(', sep(Param, ','), ')', Block], |
| 111 | + // named vs anonymous kept separate (greedy opt(Ident) would eat a leading |
| 112 | + // `extends`); decorator dimension collapsed via opt(DecoratorExpr). |
| 113 | + [opt(DecoratorExpr), 'class', Ident, opt('extends', ClassHeritage), '{', many(ClassMember), '}'], |
| 114 | + [opt(DecoratorExpr), 'class', opt('extends', ClassHeritage), '{', many(ClassMember), '}'], |
| 115 | +]); |
| 116 | + |
| 117 | +// ── Statements ── |
| 118 | + |
| 119 | +const Block = rule($ => [ |
| 120 | + ['{', many(Stmt), '}'], |
| 121 | +]); |
| 122 | + |
| 123 | +// ── Destructuring Patterns ── |
| 124 | + |
| 125 | +const BindingProperty = rule($ => [ |
| 126 | + // `name: elem` — the KEY is a PropertyName, so a reserved word is allowed here |
| 127 | + // (`{ while: y }`); the bound name inside `elem` is guarded by BindingElement. |
| 128 | + [Ident, ':', BindingElement], |
| 129 | + // shorthand `a` / shorthand-with-default `a = 1` — the name is a BindingIdentifier, |
| 130 | + // so a reserved word is invalid (`{ while }`, `{ class }`). |
| 131 | + [notReserved, Ident, opt('=', Expr)], |
| 132 | + [alt(String_, Number_, ['[', Expr, ']']), ':', BindingElement], // "s"/0/[e]: elem |
| 133 | + ['...', alt([notReserved, Ident], BindingPattern)], // ...rest | ...{ a } |
| 134 | +]); |
| 135 | + |
| 136 | +const BindingElement = rule($ => [ |
| 137 | + [alt([notReserved, Ident], BindingPattern), opt('=', Expr)], // a | { a } (optionally = default) |
| 138 | +]); |
| 139 | + |
| 140 | +const ArrayBindingElement = rule($ => [ |
| 141 | + BindingElement, |
| 142 | + ['...', alt([notReserved, Ident], BindingPattern)], // [...rest] | [...{ a }] |
| 143 | +]); |
| 144 | + |
| 145 | +const BindingPattern = rule($ => [ |
| 146 | + ['{', sep(BindingProperty, ','), '}'], // { a, b: c, ...rest } |
| 147 | + ['[', opt(ArrayBindingElement), many(',', opt(ArrayBindingElement)), ']'], // [a, , b, ...rest] |
| 148 | +]); |
| 149 | + |
| 150 | +// ── Bindings & Parameters ── |
| 151 | + |
| 152 | +const Binding = rule($ => [ |
| 153 | + [alt([notReserved, Ident], BindingPattern), opt('=', Expr)], |
| 154 | +]); |
| 155 | + |
| 156 | +// A binding in a for-head: identical to Binding except the initializer is a |
| 157 | +// no-`in` expression, so `for (var a = 1 in xs)` reads `a = 1` then the for-in |
| 158 | +// `in` (TS's [~In] grammar), rather than greedily parsing `1 in xs`. |
| 159 | +const ForBinding = rule($ => [ |
| 160 | + [alt([notReserved, Ident], BindingPattern), opt('=', exclude('in', Expr))], |
| 161 | +]); |
| 162 | + |
| 163 | +const Param = rule($ => { |
| 164 | + const body = alt( |
| 165 | + [Ident, opt('=', Expr)], |
| 166 | + [BindingPattern, opt('=', Expr)], |
| 167 | + // a rest element can never validly be a reserved word (`...while`), so guarding it is FN-safe. |
| 168 | + ['...', alt([notReserved, Ident], BindingPattern)], // rest |
| 169 | + ); |
| 170 | + return [ |
| 171 | + [opt(DecoratorExpr), body], |
| 172 | + ]; |
| 173 | +}); |
| 174 | + |
| 175 | +const ForHead = rule($ => { |
| 176 | + const cTail = [';', opt(Expr, many(',', Expr)), ';', opt(Expr, many(',', Expr))]; // `; cond ; update` |
| 177 | + return [ |
| 178 | + // declared head: `let/const/var/using/await using <bindings>` then C-style or in/of. |
| 179 | + // ForBinding gives a no-`in` initializer so `for (var a = 1 in xs)` parses. |
| 180 | + [alt('let', 'const', 'var', 'using', ['await', 'using']), sep(ForBinding, ','), alt( |
| 181 | + cTail, |
| 182 | + [alt('in', 'of'), Expr], |
| 183 | + )], |
| 184 | + [opt(Expr, many(',', Expr)), ...cTail], // C-style, no declaration: `for (i=0; …; …)` / `for (;;)` |
| 185 | + [Expr, alt('in', 'of'), Expr], // for-in/of, no declaration: `for (x of xs)` |
| 186 | + ]; |
| 187 | +}); |
| 188 | + |
| 189 | +const SwitchCase = rule($ => [ |
| 190 | + ['case', Expr, many(',', Expr), ':'], |
| 191 | + ['default', ':'], |
| 192 | + Stmt, |
| 193 | +]); |
| 194 | + |
| 195 | +const Stmt = rule($ => [ |
| 196 | + Block, |
| 197 | + [alt('let', 'const', 'var'), sep(Binding, ','), opt(';')], |
| 198 | + ['if', '(', Expr, many(',', Expr), ')', $, opt('else', $)], |
| 199 | + ['for', opt('await'), '(', ForHead, ')', $], |
| 200 | + ['while', '(', Expr, many(',', Expr), ')', $], |
| 201 | + ['do', $, 'while', '(', Expr, many(',', Expr), ')', opt(';')], |
| 202 | + ['switch', '(', Expr, many(',', Expr), ')', '{', many(SwitchCase), '}'], |
| 203 | + ['return', opt(Expr, many(',', Expr)), opt(';')], |
| 204 | + ['throw', Expr, many(',', Expr), opt(';')], |
| 205 | + ['break', opt(Ident), opt(';')], |
| 206 | + ['continue', opt(Ident), opt(';')], |
| 207 | + ['try', Block, opt('catch', opt('(', alt(Param, BindingPattern), ')'), Block), opt('finally', Block)], |
| 208 | + [Ident, ':', $], |
| 209 | + ';', |
| 210 | + ['debugger', opt(';')], |
| 211 | + ['with', '(', Expr, ')', $], |
| 212 | + [opt('await'), 'using', sep(Binding, ','), opt(';')], |
| 213 | + Decl, |
| 214 | + [Expr, many(',', Expr), opt(';')], |
| 215 | +]); |
| 216 | + |
| 217 | +// ── Declarations ── |
| 218 | + |
| 219 | +const MemberName = rule($ => [ |
| 220 | + Ident, |
| 221 | + PrivateField, |
| 222 | + String_, |
| 223 | + Number_, |
| 224 | + HexNumber, |
| 225 | + OctalNumber, |
| 226 | + BinaryNumber, |
| 227 | + BigInt_, |
| 228 | + ['[', Expr, ']'], |
| 229 | +]); |
| 230 | + |
| 231 | +// Branched: parse the modifier list ONCE, then branch on the member kind, so a |
| 232 | +// member's shared `modifiers …` prefix isn't re-parsed per alternative. Inner |
| 233 | +// alt() is first-match, so branches are ordered specific-before-general |
| 234 | +// (generator/accessor before the MemberName method/field split). |
| 235 | +const Modifier = alt('static', 'accessor', 'async'); |
| 236 | +const callTail = ['(', sep(Param, ','), ')', opt(Block), opt(';')] as const; |
| 237 | +const ClassMember = rule($ => [ |
| 238 | + DecoratorExpr, |
| 239 | + ['constructor', '(', sep(Param, ','), ')', Block, opt(';')], |
| 240 | + ['static', Block], |
| 241 | + [ |
| 242 | + many(Modifier), |
| 243 | + alt( |
| 244 | + ['*', MemberName, ...callTail], // generator method |
| 245 | + [alt('get', 'set'), MemberName, '(', opt(sep(Param, ',')), ')', opt(Block), opt(';')], // accessor |
| 246 | + [MemberName, alt( |
| 247 | + [...callTail], // method (requires `(`) |
| 248 | + [opt('=', Expr), opt(';')], // field (all-optional → catch-all) |
| 249 | + )], |
| 250 | + ), |
| 251 | + ], |
| 252 | + // Fallbacks for a member NAMED like a modifier (`static = 1`, `get = 1`, `async() {}`): |
| 253 | + // many(Modifier) would eat the name, so the member kind alt fails and we land here. |
| 254 | + [MemberName, opt('=', Expr), opt(';')], |
| 255 | + [MemberName, '(', sep(Param, ','), ')', opt(Block), opt(';')], |
| 256 | +]); |
| 257 | + |
| 258 | +const ImportSpecifier = rule($ => [ |
| 259 | + [Ident, opt('as', Ident)], |
| 260 | +]); |
| 261 | + |
| 262 | +const ImportClause = rule($ => [ |
| 263 | + // default import, optionally followed by named `{…}` or namespace `* as x` |
| 264 | + [Ident, opt(',', alt(['{', sep(ImportSpecifier, ','), '}'], ['*', 'as', Ident]))], |
| 265 | + ['{', sep(ImportSpecifier, ','), '}'], |
| 266 | + ['*', 'as', Ident], |
| 267 | +]); |
| 268 | + |
| 269 | +const Decl = rule($ => [ |
| 270 | + // Function declarations live here (not in Stmt) so that at statement level a |
| 271 | + // leading `function` is preferred as a declaration over an IIFE expression- |
| 272 | + // statement: Program tries Decl before Stmt, so `function f(){}\n()=>{}` parses |
| 273 | + // as a declaration + arrow rather than longest-matching `function f(){}()` (IIFE). |
| 274 | + [opt('async'), 'function', opt('*'), Ident, '(', sep(Param, ','), ')', Block], |
| 275 | + // class decl: optional decorators. gen-tm expands the opt()/many() to recover |
| 276 | + // the `class Ident … { … }` shape for highlighting. |
| 277 | + [many(DecoratorExpr), 'class', Ident, opt('extends', ClassHeritage), '{', many(ClassMember), '}'], |
| 278 | + ['export', alt($, Stmt)], |
| 279 | + ['export', 'default', alt( |
| 280 | + [opt('async'), 'function', opt('*'), opt(Ident), '(', sep(Param, ','), ')', Block], // function |
| 281 | + [Expr, opt(';')], // catch-all: export default <expr> |
| 282 | + )], |
| 283 | + ['export', '*', alt(['from', String_, opt(';')], ['as', Ident, 'from', String_, opt(';')])], |
| 284 | + ['export', '{', sep(ImportSpecifier, ','), '}', opt('from', String_), opt(';')], |
| 285 | + ['import', alt( |
| 286 | + [ImportClause, 'from', String_, opt(';')], // import X from "m" |
| 287 | + [Ident, '=', Expr, opt(';')], // import x = expr |
| 288 | + [String_, opt(';')], // import "m" |
| 289 | + )], |
| 290 | + [many(DecoratorExpr), 'export', alt($, Stmt)], |
| 291 | +]); |
| 292 | + |
| 293 | +// ── Entry ── |
| 294 | + |
| 295 | +const Program = rule($ => [ |
| 296 | + many(alt(Decl, Stmt)), // Decl first: prefer declaration over IIFE expression-statement |
| 297 | +]); |
| 298 | + |
| 299 | +// ── Grammar ── |
| 300 | + |
| 301 | +export default defineGrammar({ |
| 302 | + name: 'javascript', |
| 303 | + scopeName: 'source.js', |
| 304 | + |
| 305 | + tokens: ecmaTokens, |
| 306 | + |
| 307 | + prec: ecmaPrec, |
| 308 | + |
| 309 | + rules: { |
| 310 | + DecoratorExpr, |
| 311 | + Expr, Prop, MemberName, NewTarget, ClassHeritage, |
| 312 | + Stmt, Block, |
| 313 | + BindingProperty, BindingElement, ArrayBindingElement, BindingPattern, |
| 314 | + Binding, ForBinding, Param, ForHead, SwitchCase, |
| 315 | + Decl, ClassMember, |
| 316 | + ImportClause, ImportSpecifier, |
| 317 | + Program, |
| 318 | + }, |
| 319 | + |
| 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 | + }, |
| 361 | + |
| 362 | + entry: Program, |
| 363 | +}); |
0 commit comments