Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions feature/accessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,21 +47,25 @@ const accessor = (kind) => a => {
token('get', ASSIGN - 1, accessor('get'));
token('set', ASSIGN - 1, accessor('set'));

// Method shorthand: { foo() {} } / { "foo"() {} } / class { static foo() {} }
// Method shorthand: { foo() {} } / { async foo() {} } / class { static foo() {} }
// → [':', key, ['=>', ['()', params], body]]
// Accepts identifier, string-literal node [, "..."], or ['static', key] from unary('static').
// Accepts identifier, string-literal node [, "..."], ['async', key] from
// async.js, or ['static', key] from unary('static').
token('(', ASSIGN - 1, a => {
if (!a) return;
// ['static', key] from unary('static'): unwrap, re-wrap the resulting method node.
let wrap;
let wrap, isAsync;
if (Array.isArray(a) && a[0] === 'static') wrap = 'static', a = a[1];
if (Array.isArray(a) && a[0] === 'async') isAsync = true, a = a[1];
// Accept identifier or string-literal node as key
if (!isMethodKey(a)) return;
const params = expr(0, CPAREN) || null;
parse.space();
// Not followed by { - not method shorthand, fall through
if (cur.charCodeAt(idx) !== OBRACE) return;
skip();
const node = [':', a, ['=>', ['()', params], expr(0, CBRACE) || null]];
let value = ['=>', ['()', params], expr(0, CBRACE) || null];
if (isAsync) value = ['async', value];
const node = [':', a, value];
return wrap ? [wrap, node] : node;
});
12 changes: 11 additions & 1 deletion feature/async.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Async/await/yield: async function, async arrow, await, yield expressions - parse half
import { parse, unary, expr, skip, keyword, cur, idx, word } from '../parse.js';
import { parse, unary, expr, skip, seek, keyword, cur, idx, word } from '../parse.js';

const PREFIX = 140, ASSIGN = 20;
const OPAREN = 40, OBRACE = 123;

// await expr → ['await', expr]
unary('await', PREFIX);
Expand All @@ -27,6 +28,15 @@ keyword('async', PREFIX, () => {
if (word('function')) return ['async', expr(PREFIX)];
// async arrow: async () => or async x =>
// Parse at assign precedence to catch => operator
const from = idx;
const params = expr(ASSIGN - .5);
if (params?.[0] === '()' && typeof params[1] === 'string' && parse.space() === OBRACE) {
let at = from;
while (cur.charCodeAt(at) <= 32) at++;
while (parse.id(cur.charCodeAt(at))) at++;
while (cur.charCodeAt(at) <= 32) at++;
if (cur.charCodeAt(at) === OPAREN) seek(at);
return ['async', params[1]];
}
return params && ['async', params];
});
35 changes: 26 additions & 9 deletions feature/number.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,41 @@
*
* Configurable via parse.number: { '0x': 16, '0b': 2, '0o': 8 }
*/
import { parse, lookup, next, err, skip, idx, cur } from '../parse.js';
import { parse, lookup, next, err, skip, seek, idx, cur } from '../parse.js';

const PERIOD = 46, _0 = 48, _9 = 57, _E = 69, _e = 101, PLUS = 43, MINUS = 45, UNDERSCORE = 95, _n = 110;
const _a = 97, _f = 102, _A = 65, _F = 70;

// Strip underscores only if present (avoid allocation for common case)
const strip = s => s.indexOf('_') < 0 ? s : s.replaceAll('_', '');

const digit = c => c >= _0 && c <= _9;
const digitOrSep = c => digit(c) || c === UNDERSCORE;

// Decimal number - check for .. range operator (don't consume . if followed by .)
// Supports numeric separators: 1_000_000 and BigInt suffix: 123n
const num = a => {
let str = strip(next(c =>
// . is decimal only if NOT followed by another . (range operator)
(c === PERIOD && cur.charCodeAt(idx + 1) !== PERIOD) ||
(c >= _0 && c <= _9) ||
c === UNDERSCORE ||
((c === _E || c === _e) && ((c = cur.charCodeAt(idx + 1)) >= _0 && c <= _9 || c === PLUS || c === MINUS) ? 2 : 0)
));
const from = idx;

if (cur.charCodeAt(idx) === PERIOD) skip();
next(digitOrSep);

if (cur.charCodeAt(idx) === PERIOD && cur.charCodeAt(idx + 1) !== PERIOD) {
skip();
next(digitOrSep);
}

const exp = cur.charCodeAt(idx);
if (exp === _E || exp === _e) {
let at = idx + 1, sign = cur.charCodeAt(at);
if (sign === PLUS || sign === MINUS) at++;
if (digit(cur.charCodeAt(at))) {
seek(at + 1);
next(digitOrSep);
}
}

let str = strip(cur.slice(from, idx));
// BigInt suffix
if (cur.charCodeAt(idx) === _n) { skip(); return [, BigInt(str)]; }
return (a = +str) != a ? err() : [, a];
Expand All @@ -37,7 +54,7 @@ const charTest = {
parse.number = null;

// .1 (but not .. range)
lookup[PERIOD] = a => !a && cur.charCodeAt(idx + 1) !== PERIOD && num();
lookup[PERIOD] = a => !a && digit(cur.charCodeAt(idx + 1)) && num();

// 0-9: check parse.number for prefix config
for (let i = _0; i <= _9; i++) lookup[i] = a => a ? void 0 : num();
Expand Down
6 changes: 3 additions & 3 deletions feature/switch.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ const STATEMENT = 5, ASSIGN = 20, COLON = 58, SEMI = 59, CBRACE = 125;
// Flag to track if we're inside switch body (case/default parsing)
let inSwitch = 0;

// Reserve 'case' and 'default' as keywords that fail outside switch body
// Allows property names like {case:1} ONLY when not in switch context
// Reserve 'case' and 'default' as case-body boundaries, while still allowing
// object properties like {case: 1} and {default: 1} inside switch bodies.
const reserve = (w, l = w.length, c = w.charCodeAt(0), prev = lookup[c]) =>
lookup[c] = (a, prec, op) => (word(w) && !a && inSwitch) || prev?.(a, prec, op);
lookup[c] = (a, prec, op) => (word(w) && !a && inSwitch && (!parse.prop || parse.prop(idx + l))) || prev?.(a, prec, op);
reserve('case');
reserve('default');

Expand Down
15 changes: 15 additions & 0 deletions test/feature/async-class.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ test('async/class: async arrow', () => {
is(parse('async (a, b) => a + b'), ['async', ['=>', ['()', [',', 'a', 'b']], ['+', 'a', 'b']]]);
});

test('async/class: async method shorthand', () => {
is(parse('class A { async m(a) { await a } }'), [
'class', 'A', null,
[':', 'm', ['async', ['=>', ['()', 'a'], ['await', 'a']]]]
]);
is(parse('{ async m(a) { await a } }'), [
'{}',
[':', 'm', ['async', ['=>', ['()', 'a'], ['await', 'a']]]]
]);
});

test('async/class: await', () => {
is(parse('await x'), ['await', 'x']);
is(parse('await f()'), ['await', ['()', 'f', null]]);
Expand Down Expand Up @@ -138,6 +149,10 @@ test('numbers: numeric separators', () => {
is(compile(parse('1_000 + 2_000'))(), 3000);
});

test('numbers: decimal literal member access', () => {
is(parse('0.95.toFixed(2)'), ['()', ['.', [, 0.95], 'toFixed'], [, 2]]);
});

test('numbers: bigint', () => {
is(parse('123n'), [, 123n]);
is(parse('1_000n'), [, 1000n]);
Expand Down
2 changes: 2 additions & 0 deletions test/feature/control.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ test('control: switch basic', t => {
is(parse('switch (x) { case 1: a }')[2][2], 'a')
is(parse('switch (x) { default: a }')[2][0], 'default')
is(parse('switch (x) { default: a }')[2][1], 'a')
is(parse('switch (x) { case 1: ({ default: a }) }')[2][2],
['()', ['{}', [':', 'default', 'a']]])

// Multiple cases
const ast = parse('switch (x) { case 1: a; case 2: b }')
Expand Down
Loading