diff --git a/feature/accessor.js b/feature/accessor.js index 8bbfd32..c7c6faf 100644 --- a/feature/accessor.js +++ b/feature/accessor.js @@ -47,14 +47,16 @@ 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; @@ -62,6 +64,8 @@ token('(', ASSIGN - 1, a => { // 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; }); diff --git a/feature/async.js b/feature/async.js index 6d11b9f..6c465ab 100644 --- a/feature/async.js +++ b/feature/async.js @@ -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); @@ -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]; }); diff --git a/feature/number.js b/feature/number.js index eba49cd..6eac142 100644 --- a/feature/number.js +++ b/feature/number.js @@ -3,7 +3,7 @@ * * 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; @@ -11,16 +11,33 @@ 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]; @@ -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(); diff --git a/feature/switch.js b/feature/switch.js index c11bddb..5f297c3 100644 --- a/feature/switch.js +++ b/feature/switch.js @@ -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'); diff --git a/test/feature/async-class.js b/test/feature/async-class.js index d44af2e..442cac4 100644 --- a/test/feature/async-class.js +++ b/test/feature/async-class.js @@ -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]]); @@ -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]); diff --git a/test/feature/control.js b/test/feature/control.js index 3546a09..edd00fb 100644 --- a/test/feature/control.js +++ b/test/feature/control.js @@ -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 }')