Skip to content

Commit 77e94ab

Browse files
committed
Handle keywords as props
1 parent 3e766a1 commit 77e94ab

9 files changed

Lines changed: 62 additions & 44 deletions

File tree

feature/block.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
// Block parsing helpers
2-
import { expr, skip, space, lookup, err, parse, seek, cur, idx, parens, loc, operator, compile } from '../parse.js';
2+
import { expr, skip, space, lookup, err, parse, seek, cur, idx, parens, loc, operator, compile, peek } from '../parse.js';
33

44
const STATEMENT = 5, OBRACE = 123, CBRACE = 125;
55

6-
// keyword(op, prec, fn) - prefix-only word token
6+
// keyword(op, prec, fn) - prefix-only word token with object property support
77
// keyword('while', 6, () => ['while', parens(), body()])
88
// keyword('break', 6, () => ['break'])
9-
// attaches .loc to array results for source mapping
9+
// Allows property names: {while:1} won't match keyword, falls back to identifier
1010
export const keyword = (op, prec, map, c = op.charCodeAt(0), l = op.length, prev = lookup[c], r) =>
1111
lookup[c] = (a, curPrec, curOp, from = idx) =>
1212
!a &&
1313
(curOp ? op == curOp : (l < 2 || cur.substr(idx, l) == op) && (curOp = op)) &&
1414
curPrec < prec &&
1515
!parse.id(cur.charCodeAt(idx + l)) &&
16-
(seek(idx + l), (r = map()) ? loc(r, from) : (seek(from), !prev && err()), r) ||
16+
peek(idx + l) !== 58 && // allow {keyword:value}
17+
(seek(idx + l), (r = map()) ? loc(r, from) : seek(from), r) ||
1718
prev?.(a, curPrec, curOp);
1819

1920
// infix(op, prec, fn) - infix word token (requires left operand)

feature/class.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Class declarations and expressions
22
// class A extends B { ... }
3-
import { binary, unary, token, expr, space, next, parse, literal, word, operator, compile, skip } from '../parse.js';
3+
import { binary, unary, token, expr, space, next, parse, literal, word, operator, compile, skip, cur, idx } from '../parse.js';
44
import { keyword, block } from './block.js';
55

66
const TOKEN = 200, PREFIX = 140, COMP = 90;

feature/literal.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
* [, undefined] serializes to [null, null] which would compile to null.
88
* [] serializes to [] and compiles back to undefined.
99
*/
10-
import { literal, token } from '../parse.js';
10+
import { literal } from '../parse.js';
11+
import { keyword } from './block.js';
1112

1213
literal('true', true);
1314
literal('false', false);
1415
literal('null', null);
15-
token('undefined', 200, a => !a && []); // [] for JSON compatibility
16+
keyword('undefined', 200, () => []); // [] for JSON compatibility
1617
literal('NaN', NaN);
1718
literal('Infinity', Infinity);

feature/loop.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ keyword('for', STATEMENT + 1, () => {
1717
// for await (x of y)
1818
if (word('await')) {
1919
skip(5);
20-
space();
21-
return ['for await', parens(), body()];
20+
return (space(), ['for await', parens(), body()]);
2221
}
2322
return ['for', parens(), body()];
2423
});
@@ -55,8 +54,7 @@ keyword('continue', STATEMENT + 1, () => {
5554
});
5655
keyword('return', STATEMENT + 1, () => {
5756
parse.asi && (parse.newline = false);
58-
space();
59-
const c = cur.charCodeAt(idx);
57+
const c = space();
6058
return !c || c === CBRACE || c === SEMI || parse.newline ? ['return'] : ['return', expr(STATEMENT)];
6159
});
6260

feature/op/unary.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,18 @@
88
*
99
* JS-specific keywords
1010
*/
11-
import { unary, operator, compile, token, space, skip, expr, word } from '../../parse.js';
11+
import { unary, operator, compile, skip, expr, word } from '../../parse.js';
12+
import { keyword } from '../block.js';
1213

1314
const PREFIX = 140;
1415

1516
unary('typeof', PREFIX);
1617
unary('void', PREFIX);
1718
unary('delete', PREFIX);
18-
// new X() or new.target - can't use two unary() because both start with 'n' (lookup collision)
19-
token('new', PREFIX, a => !a && (
19+
// new X() or new.target
20+
keyword('new', PREFIX, () =>
2021
word('.target') ? (skip(7), ['new.target']) : ['new', expr(PREFIX)]
21-
));
22+
);
2223

2324
// Compile
2425
operator('typeof', a => (a = compile(a), ctx => typeof a(ctx)));

feature/switch.js

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
// Switch/case/default
22
// AST: ['switch', val, ['case', test, body], ['default', body], ...]
3-
import { expr, skip, space, parens, operator, compile, idx, err, seek, parse, lookup, word } from '../parse.js';
3+
import { expr, skip, space, parens, operator, compile, idx, err, seek, parse, lookup, word, cur } from '../parse.js';
44
import { keyword } from './block.js';
55
import { BREAK } from './control.js';
66

77
const STATEMENT = 5, ASSIGN = 20, COLON = 58, SEMI = 59, CBRACE = 125;
88

9+
// Flag to track if we're inside switch body (case/default parsing)
10+
let inSwitch = 0;
11+
912
// Reserve 'case' and 'default' as keywords that fail outside switch body
10-
// This prevents them becoming identifiers for colon operator
11-
const reserve = (w, c = w.charCodeAt(0), prev = lookup[c]) =>
12-
lookup[c] = (a, prec, op) => (word(w) && !a && (parse.reserved = 1)) || prev?.(a, prec, op);
13+
// Allows property names like {case:1} ONLY when not in switch context
14+
const reserve = (w, l = w.length, c = w.charCodeAt(0), prev = lookup[c]) =>
15+
lookup[c] = (a, prec, op) => (word(w) && !a && inSwitch) || prev?.(a, prec, op);
1316
reserve('case');
1417
reserve('default');
1518

@@ -26,24 +29,27 @@ const caseBody = (c) => {
2629
// switchBody() - parse case/default statements
2730
const switchBody = () => {
2831
space() === 123 || err('Expected {'); skip();
32+
inSwitch++;
2933
const cases = [];
30-
while (space() !== CBRACE) {
31-
if (word('case')) {
32-
seek(idx + 4); space();
33-
const test = expr(ASSIGN - .5);
34-
space() === COLON && skip();
35-
cases.push(['case', test, caseBody()]);
36-
} else if (word('default')) {
37-
seek(idx + 7); space() === COLON && skip();
38-
cases.push(['default', caseBody()]);
39-
} else err('Expected case or default');
40-
}
34+
try {
35+
while (space() !== CBRACE) {
36+
if (word('case')) {
37+
seek(idx + 4); space();
38+
const test = expr(ASSIGN - .5);
39+
space() === COLON && skip();
40+
cases.push(['case', test, caseBody()]);
41+
} else if (word('default')) {
42+
seek(idx + 7); space() === COLON && skip();
43+
cases.push(['default', caseBody()]);
44+
} else err('Expected case or default');
45+
}
46+
} finally { inSwitch--; }
4147
skip();
4248
return cases;
4349
};
4450

4551
// switch (x) { ... }
46-
keyword('switch', STATEMENT + 1, () => (space(), ['switch', parens(), ...switchBody()]));
52+
keyword('switch', STATEMENT + 1, () => space() === 40 && ['switch', parens(), ...switchBody()]);
4753

4854
// Compile
4955
operator('switch', (val, ...cases) => {

feature/var.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* for (let x in o) → ['for', ['in', ['let', 'x'], 'o'], body]
99
* var x → ['var', 'x'] (acts as assignment target)
1010
*/
11-
import { token, expr, space, operator, compile } from '../parse.js';
11+
import { expr, space, operator, compile } from '../parse.js';
1212
import { keyword } from './block.js';
1313
import { destructure } from './destruct.js';
1414

@@ -28,8 +28,8 @@ const decl = keyword => {
2828
return [keyword, node];
2929
};
3030

31-
token('let', STATEMENT + 1, a => !a && decl('let'));
32-
token('const', STATEMENT + 1, a => !a && decl('const'));
31+
keyword('let', STATEMENT + 1, () => decl('let'));
32+
keyword('const', STATEMENT + 1, () => decl('const'));
3333

3434
// var: just declares identifier, assignment happens separately
3535
// var x = 5 → ['=', ['var', 'x'], 5]

parse.js

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,8 @@ export let idx, cur,
3737

3838
// a + b - c
3939
expr = (p = 0, end) => {
40-
let cc, token, newNode, fn, prevReserved = parse.reserved, nl;
40+
let cc, token, newNode, fn, nl;
4141
if (end) parse.asi && (parse.newline = false);
42-
parse.reserved = 0;
4342

4443
while (
4544
(cc = parse.space()) &&
@@ -48,10 +47,9 @@ export let idx, cur,
4847
(newNode =
4948
((fn = lookup[cc]) && fn(token, p)) ??
5049
(token && nl && parse.asi?.(token, p, expr)) ??
51-
(!token && !parse.reserved && next(parse.id))
50+
(!token && next(parse.id))
5251
)
53-
) token = newNode, parse.reserved = 0;
54-
parse.reserved = prevReserved;
52+
) token = newNode;
5553

5654
if (end) cc == end ? idx++ : err('Unclosed ' + String.fromCharCode(end - (end > 42 ? 2 : 1)));
5755

@@ -67,6 +65,9 @@ export let idx, cur,
6765
return cc
6866
},
6967

68+
// peek at next non-space char without modifying idx
69+
peek = (from = idx) => { while (cur.charCodeAt(from) <= SPACE) from++; return cur.charCodeAt(from); },
70+
7071
// is char an id?
7172
id = parse.id = c =>
7273
(c >= 48 && c <= 57) ||
@@ -87,7 +88,8 @@ export let idx, cur,
8788
// precedence registry - features register via token(), others can read
8889
prec = {},
8990

90-
// create operator checker/mapper
91+
// create operator checker/mapper - for symbols and special cases
92+
// For prefix word operators, prefer keyword() from block.js
9193
token = (
9294
op,
9395
p = SPACE,
@@ -104,17 +106,17 @@ export let idx, cur,
104106
(l < 2 || (op.charCodeAt(1) === cur.charCodeAt(idx + 1) && (l < 3 || cur.substr(idx, l) == op))) && (!word || !parse.id(cur.charCodeAt(idx + l))) && (matched = curOp = op)
105107
) &&
106108
curPrec < p &&
107-
(idx += l, (r = map(a)) ? loc(r, from) : (idx = from, matched = 0, word && r !== false && (parse.reserved = 1), !word && !prev && err()), r)
109+
(idx += l, (r = map(a)) ? loc(r, from) : (idx = from, matched = 0, !word && !prev && err()), r)
108110
) ||
109111
prev?.(a, curPrec, matched)),
110112

111-
binary = (op, p, right = false) => token(op, p, (a, b) => a && (b = expr(p - (right ? .5 : 0))) && [op, a, b]),
113+
binary = (op, p, right = false) => token(op, p, a => a && (b => b && [op, a, b])(expr(p - (right ? .5 : 0)))),
112114

113115
unary = (op, p, post) => token(op, p, a => post ? (a && [op, a]) : (!a && (a = expr(p - .5)) && [op, a])),
114116

115117
literal = (op, val) => token(op, 200, a => !a && [, val]),
116118

117-
nary = (op, p, right) => {
119+
nary = (op, p, right) =>
118120
token(op, p,
119121
(a, b) => (
120122
b = expr(p - (right ? .5 : 0)),
@@ -124,7 +126,7 @@ export let idx, cur,
124126
a
125127
))
126128
)
127-
},
129+
,
128130

129131
group = (op, p) => token(op[0], p, a => (!a && [op, expr(0, op.charCodeAt(1)) || null])),
130132

test/jessie.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,15 @@ test('jessie: block vs object detection', () => {
7676
is(parse('{a:1}'), ['{}', [':', 'a', [, 1]]])
7777
is(parse('{a,b}'), ['{}', [',', 'a', 'b']])
7878

79+
// Objects - keyword property names
80+
is(parse('{for:1}'), ['{}', [':', 'for', [, 1]]])
81+
is(parse('{for:1, title:2}'), ['{}', [',', [':', 'for', [, 1]], [':', 'title', [, 2]]]])
82+
is(parse('{let:1}'), ['{}', [':', 'let', [, 1]]])
83+
is(parse('{new:1}'), ['{}', [':', 'new', [, 1]]])
84+
is(parse('{undefined:1}'), ['{}', [':', 'undefined', [, 1]]])
85+
is(parse('{if:1, while:2, for:3}'), ['{}', [',', [':', 'if', [, 1]], [':', 'while', [, 2]], [':', 'for', [, 3]]]])
86+
is(run('{for:1, title:2}'), {for: 1, title: 2})
87+
7988
// Block evaluation - creates scope, returns last value
8089
is(run('{let x=1; x+1}'), 2)
8190
is(run('{let x=1}; x', {x: 5}), 5) // block scope doesn't leak

0 commit comments

Comments
 (0)