Skip to content

Commit 87505d5

Browse files
committed
Add JavaScript grammar (examples/javascript.ts) reusing the TS core
JavaScript is a syntactic subset of TypeScript, so the JS grammar is the TS grammar with every type construct stripped. Approach (chosen over a parameterized factory, which made the main grammar too noisy): SHARE the type-free vocabulary by import, COPY+strip the rules. - typescript.ts: hoist the inline tokens/prec/scopes into exported consts (ecmaTokens/ecmaPrec/ecmaScopes) + export the token consts. Behavior-preserving: no rule definition touched, typescript.tmLanguage.json byte-identical, all TS gates unchanged (100%/97.84%, coverage 99.3%, sanity 15/15, agnostic 5/5, guard 112/112). - javascript.ts: imports the vocabulary; copies the rules with all TS-only syntax removed -- Type/TypeParams/TypeMember/interface/enum/type-alias/declare/namespace, ': Type' annotations, '<T>' params/args + bare instantiation, as/satisfies/cast/ non-null '!', type-optional '?', accessibility modifiers/parameter-properties/ 'this:' param, import type. Keeps all real JS. scopeName source.js. (Rules can't be imported -- combinator rules bind references at definition time, so a JS rule must reference the JS rule consts; only the vocabulary layer is shared.) - test/js-conformance.ts: JS validation harness (ground truth = ts ScriptKind.JS). Validation: 61/61 curated valid-JS snippets parse; 0 type-free corpus files fail (every .js-valid file parses); TS-only syntax with a dedicated production is rejected (type anno, enum, f<T>, x!, cast, typed params/returns/bindings). Known leniency (same class TS itself has): contextual-keyword TS constructs (x as T, interface/type as statements) parse as adjacent identifier statements. The 45 corpus over-accepts are all inherited from the TS core -- no NEW over-acceptance. Follow-ups (not blocking): src/cli.ts writes shared-named tree-sitter/lezer files, so JS artifact generation clobbers TS's -- namespace per-language output before committing JS artifacts; then add a JS highlighter-coverage gate. (Implemented by a delegated agent; independently re-verified -- TS byte-identical, gates green.)
1 parent 5767717 commit 87505d5

3 files changed

Lines changed: 637 additions & 71 deletions

File tree

examples/javascript.ts

Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
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

Comments
 (0)