diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 00000000..b3b9d5d8
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,8 @@
+Avoid writing to `lookup`, use token instead.
+Avoid ad-hocs - that signals inconsistency of design. High-level API is for a reason, we need to use it and if it doesn't fit, we need to improve it.
+`/features` represent generic language features, not only JS: avoid js-specific checks.
+For features you implement or any changes, if relevant please add tests, update spec.md, docs.md and README.md, as well as REPL and other relevant files. Make sure tests pass.
+Make sure API and feature code is intuitive and user-friendly: prefer `unary`/`binary`/`nary`/`group`/`token` calls in the right order ( eg. first `|`, then `||`, then `||=` ) rather than low-level parsing. Eg. feature should not use `cur`, `idx` and use `skip` instead.
+The project is planned to be built with jz - simple javascript subset compiling to wasm, so don't use complex structures like Proxy, classes etc.
+By introducing a change, think how would that scale to various dialects and compile targets. Also make sure it doesn't compromise performance and doesn't bloat the code. Justin must be faster than jsep.
+By writing parser feature, aim for raw tree shape parsed with minimal code rather than edge cases validation.
diff --git a/.gitignore b/.gitignore
index 4afbbd38..c59b76a1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -106,3 +106,4 @@ dist
# TernJS port file
.tern-port
.DS_Store
+test-results/
diff --git a/README.md b/README.md
index 29ef2cea..92cb3a71 100644
--- a/README.md
+++ b/README.md
@@ -1,98 +1,67 @@
-# subscript
+# sub ✦ script [](https://github.com/dy/subscript/actions/workflows/node.js.yml) [](http://npmjs.org/subscript) [](https://bundlephobia.com/package/subscript) [](https://dy.github.io/subscript/) [](http://microjs.com/#subscript)
-> _Subscript_ is fast, tiny & extensible parser / evaluator / microlanguage.
+> Tiny expression parser & evaluator.
-#### Used for:
+* **Safe** — sandboxed, blocks `__proto__`, `constructor`, no global access
+* **Fast** — Pratt parser engine, see [benchmarks](#performance)
+* **Portable** — universal expression format, any compile target
+* **Metacircular** — can parse and compile itself
+* **Extensible** — pluggable syntax for building custom DSL
-* expressions evaluators, calculators
-* subsets of languages
-* sandboxes, playgrounds, safe eval
-* custom DSL
-* preprocessors
-* templates
+## Usage
-_Subscript_ has [3.5kb](https://npmfs.com/package/subscript/7.4.3/subscript.min.js) footprint , good [performance](#performance) and extensive test coverage.
+```js
+import subscript from 'subscript'
+let fn = subscript('a + b * 2')
+fn({ a: 1, b: 3 }) // 7
+```
-## Usage
+## Presets
-```js
-import subscript from './subscript.js'
+### subscript
-// parse expression
-const fn = subscript('a.b + Math.sqrt(c - 1)')
+Common expressions:
-// evaluate with context
-fn({ a: { b:1 }, c: 5, Math })
-// 3
+`a.b a[b] a(b) + - * / % < > <= >= == != ! && || ~ & | ^ << >> ++ -- = += -= *= /=`
+```js
+import subscript from 'subscript'
+
+subscript('a.b + c * 2')({ a: { b: 1 }, c: 3 }) // 7
```
-## Operators
-
-_Subscript_ supports [common syntax](https://en.wikipedia.org/wiki/Comparison_of_programming_languages_(syntax)) (_JavaScript_, _C_, _C++_, _Java_, _C#_, _PHP_, _Swift_, _Objective-C_, _Kotlin_, _Perl_ etc.):
-
-* `a.b`, `a[b]`, `a(b)`
-* `a++`, `a--`, `++a`, `--a`
-* `a * b`, `a / b`, `a % b`
-* `+a`, `-a`, `a + b`, `a - b`
-* `a < b`, `a <= b`, `a > b`, `a >= b`, `a == b`, `a != b`
-* `~a`, `a & b`, `a ^ b`, `a | b`, `a << b`, `a >> b`
-* `!a`, `a && b`, `a || b`
-* `a = b`, `a += b`, `a -= b`, `a *= b`, `a /= b`, `a %= b`, `a <<= b`, `a >>= b`
-* `(a, (b))`, `a; b;`
-* `"abc"`, `'abc'`
-* `0.1`, `1.2e+3`
-
-### Justin
-
-_Just-in_ is no-keywords JS subset, _JSON_ + _expressions_ (see [thread](https://github.com/endojs/Jessie/issues/66)).
-It extends _subscript_ with:
-
-+ `a === b`, `a !== b`
-+ `a ** b`, `a **= b`
-+ `a ?? b`, `a ??= b`
-+ `a ||= b`, `a &&= b`
-+ `a >>> b`, `a >>>= b`
-+ `a ? b : c`, `a?.b`
-+ `...a`
-+ `[a, b]`
-+ `{a: b}`
-+ `(a, b) => c`
-+ `// foo`, `/* bar */`
-+ `true`, `false`, `null`, `NaN`, `undefined`
-+ `a in b`
+### justin
+
+JSON + expressions + templates + arrows:
+`` 'str' 0x 0b === !== ** ?? >>> ?. ? : => ... [] {} ` // /**/ true false null ``
```js
-import justin from 'subscript/justin'
+import justin from 'subscript/justin.js'
-let fn = justin('{ x: 1, "y": 2+2 }["x"]')
-fn() // 1
+justin('{ x: a?.b ?? 0, y: [1, ...rest] }')({ a: null, rest: [2, 3] })
+// { x: 0, y: [1, 2, 3] }
```
-### Extra
+### jessie
-+ `if (c) a`, `if (c) a else b`
-+ `while (c) body`
-+ `for (init; cond; step) body`
-+ `{ a; b }` — block scope
-+ `let x`, `const x = 1`
-+ `break`, `continue`, `return x`
-+ `` `a ${x} b` `` — template literals
-+ `/pattern/flags` — regex literals
-+ `5px`, `10rem` — unit suffixes
+JSON + expressions + statements, functions:
+`if else for while do let const var function class return throw try catch switch import export /regex/`
```js
-import subscript from 'subscript/justin'
-import 'subscript/feature/loop.js'
-
-let sum = subscript(`
- let sum = 0;
- for (i = 0; i < 10; i += 1) sum += i;
- sum
+import jessie from 'subscript/jessie.js'
+
+let fn = jessie(`
+ function factorial(n) {
+ if (n <= 1) return 1
+ return n * factorial(n - 1)
+ }
+ factorial(5)
`)
-sum() // 45
+fn({}) // 120
```
+Jessie can parse and compile its own source.
+
## Parse / Compile
@@ -110,157 +79,113 @@ fn = compile(tree)
fn({ a: {b: 1}, c: 2 }) // 2
```
-### Syntax Tree
-
-AST has simplified lispy tree structure (inspired by [frisk](https://ghub.io/frisk) / [nisp](https://github.com/ysmood/nisp)), opposed to [ESTree](https://github.com/estree/estree):
+## Extension
-* not limited to particular language (JS), can be compiled to different targets;
-* reflects execution sequence, rather than code layout;
-* has minimal overhead, directly maps to operators;
-* simplifies manual evaluation and debugging;
-* has conventional form and one-liner docs:
+```js
+import { binary, operator, compile } from 'subscript/justin.js'
+
+// add intersection operator
+binary('∩', 80) // register parser
+operator('∩', (a, b) => ( // register compiler
+ a = compile(a), b = compile(b),
+ ctx => a(ctx).filter(x => b(ctx).includes(x))
+))
+```
```js
-import { compile } from 'subscript.js'
-
-const fn = compile(['+', ['*', 'min', [,60]], [,'sec']])
-fn({min: 5}) // min*60 + "sec" == "300sec"
-
-// node kinds
-'a' // identifier — variable from scope
-[, value] // literal — [0] empty distinguishes from operator
-[op, a] // unary — prefix operator
-[op, a, null] // unary — postfix operator (null marks postfix)
-[op, a, b] // binary
-[op, a, b, c] // n-ary / ternary
-
-// operators
-['+', a, b] // a + b
-['.', a, 'b'] // a.b — property access
-['[]', a, b] // a[b] — bracket access
-['()', a] // (a) — grouping
-['()', a, b] // a(b) — function call
-['()', a, null] // a() — call with no args
-
-// literals & structures
-[, 1] // 1
-[, 'hello'] // "hello"
-['[]', [',', ...]] // [a, b] — array literal
-['{}', [':', ...]] // {a: b} — object literal
-
-// justin extensions
-['?', a, b, c] // a ? b : c — ternary
-['=>', params, x] // (a) => x — arrow function
-['...', a] // ...a — spread
-
-// control flow (extra)
-['if', cond, then, else]
-['while', cond, body]
-['for', init, cond, step, body]
-
-// postfix example
-['++', 'a'] // ++a
-['++', 'a', null] // a++
-['px', [,5]] // 5px (unit suffix)
+import justin from 'subscript/justin.js'
+justin('[1,2,3] ∩ [2,3,4]')({}) // [2, 3]
```
-### Stringify
+See [docs.md](./docs.md) for full API.
+
+
+## Syntax Tree
-To convert tree back to code, there's codegenerator function:
+Expressions parse to a minimal JSON-compatible AST:
```js
-import { stringify } from 'subscript.js'
+import { parse } from 'subscript'
-stringify(['+', ['*', 'min', [,60]], [,'sec']])
-// 'min * 60 + "sec"'
+parse('a + b * 2')
+// ['+', 'a', ['*', 'b', [, 2]]]
```
-## Extending
-
-_Subscript_ provides premade language [features](./feature) and API to customize syntax:
+AST has simplified lispy tree structure (inspired by [frisk](https://ghub.io/frisk) / [nisp](https://github.com/ysmood/nisp)), opposed to [ESTree](https://github.com/estree/estree):
-* `unary(str, precedence, postfix=false)` − register unary operator, either prefix `⚬a` or postfix `a⚬`.
-* `binary(str, precedence, rassoc=false)` − register binary operator `a ⚬ b`, optionally right-associative.
-* `nary(str, precedence)` − register n-ary (sequence) operator like `a; b;` or `a, b`, allows missing args.
-* `group(str, precedence)` - register group, like `[a]`, `{a}`, `(a)` etc.
-* `access(str, precedence)` - register access operator, like `a[b]`, `a(b)` etc.
-* `token(str, precedence, lnode => node)` − register custom token or literal. Callback takes left-side node and returns complete expression node.
-* `operator(str, (a, b) => ctx => value)` − register evaluator for an operator. Callback takes node arguments and returns evaluator function.
+* not limited to particular language (JS), can be compiled to different targets;
+* reflects execution sequence, rather than code layout;
+* has minimal overhead, directly maps to operators;
+* simplifies manual evaluation and debugging;
+* has conventional form and one-liner docs:
-Longer operators should be registered after shorter ones, eg. first `|`, then `||`, then `||=`.
+Three forms:
```js
-import script, { compile, operator, unary, binary, token } from './subscript.js'
+'x' // identifier — resolve from context
+[, value] // literal — return as-is (empty slot = data)
+[op, ...args] // operation — apply operator
+```
-// enable objects/arrays syntax
-import 'subscript/feature/array.js';
-import 'subscript/feature/object.js';
+See [spec.md](./spec.md).
-// add identity operators (precedence of comparison)
-binary('===', 9), binary('!==', 9)
-operator('===', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx)===b(ctx)))
-operator('!==', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx)!==b(ctx)))
-// add nullish coalescing (precedence of logical or)
-binary('??', 3)
-operator('??', (a, b) => b && (a = compile(a), b = compile(b), ctx => a(ctx) ?? b(ctx)))
+## Safety
-// add JS literals
-token('undefined', 20, a => a ? err() : [, undefined])
-token('NaN', 20, a => a ? err() : [, NaN])
+Blocked by default:
+- `__proto__`, `__defineGetter__`, `__defineSetter__`
+- `constructor`, `prototype`
+- Global access (only context is visible)
+
+```js
+subscript('constructor.constructor("alert(1)")()')({})
+// undefined (blocked)
```
-See [`./feature/*`](./feature) or [`./justin.js`](./justin.js) for examples.
+## Performance
+```
+Parse 30k: subscript 150ms · justin 183ms · jsep 270ms · expr-eval 480ms · jexl 1056ms
+Eval 30k: new Function 7ms · subscript 15ms · jsep+eval 30ms · expr-eval 72ms
+```
+## Utils
-## Performance
+### Codegen
-Subscript shows good performance within other evaluators. Example expression:
+Convert tree back to code:
-```
-1 + (a * b / c % d) - 2.0 + -3e-3 * +4.4e4 / f.g[0] - i.j(+k == 1)(0)
+```js
+import { codegen } from 'subscript/util/stringify.js'
+
+codegen(['+', ['*', 'min', [,60]], [,'sec']])
+// 'min * 60 + "sec"'
```
-Parse 30k times:
+### Bundle
-```
-subscript: ~150 ms 🥇
-justin: ~183 ms
-jsep: ~270 ms 🥈
-jexpr: ~297 ms 🥉
-mr-parser: ~420 ms
-expr-eval: ~480 ms
-math-parser: ~570 ms
-math-expression-evaluator: ~900ms
-jexl: ~1056 ms
-mathjs: ~1200 ms
-new Function: ~1154 ms
-```
+Create custom dialect as single file:
-Eval 30k times:
-```
-new Function: ~7 ms 🥇
-subscript: ~15 ms 🥈
-justin: ~17 ms
-jexpr: ~23 ms 🥉
-jsep (expression-eval): ~30 ms
-math-expression-evaluator: ~50ms
-expr-eval: ~72 ms
-jexl: ~110 ms
-mathjs: ~119 ms
-mr-parser: -
-math-parser: -
+```js
+import { bundle } from 'subscript/util/bundle.js'
+
+const code = await bundle('subscript/jessie.js')
+// → self-contained ES module
```
-
+* [jz](https://github.com/dy/jz) — JS subset → WASM compiler
+
+
+
+
+
+## Refs
-## Alternatives
+[jsep](https://github.com/EricSmekens/jsep), [jexl](https://github.com/TomFrost/Jexl), [expr-eval](https://github.com/silentmatt/expr-eval), [math.js](https://mathjs.org/).
-[jexpr](https://github.com/justinfagnani/jexpr), [jsep](https://github.com/EricSmekens/jsep), [jexl](https://github.com/TomFrost/Jexl), [mozjexl](https://github.com/mozilla/mozjexl), [expr-eval](https://github.com/silentmatt/expr-eval), [expression-eval](https://github.com/donmccurdy/expression-eval), [string-math](https://github.com/devrafalko/string-math), [nerdamer](https://github.com/jiggzson/nerdamer), [math-codegen](https://github.com/mauriciopoppe/math-codegen), [math-parser](https://www.npmjs.com/package/math-parser), [math.js](https://mathjs.org/docs/expressions/parsing.html), [nx-compile](https://github.com/nx-js/compiler-util), [built-in-math-eval](https://github.com/mauriciopoppe/built-in-math-eval)
+
-
🕉
diff --git a/feature/access.js b/feature/access.js index f492413b..a95406ec 100644 --- a/feature/access.js +++ b/feature/access.js @@ -1,11 +1,71 @@ -import { access, binary, group, err } from '../src/parse.js' -import { operator, compile } from '../src/compile.js' -import { PREC_ACCESS, unsafe } from '../src/const.js' +/** + * Property access: a.b, a[b], a(b), [1,2,3] + * For private fields (#x), see class.js + */ +import { access, binary, operator, compile } from '../parse.js'; + +// Block prototype chain attacks +export const unsafe = k => k?.[0] === '_' && k[1] === '_' || k === 'constructor' || k === 'prototype'; + +const ACCESS = 170; // a[b] -access('[]', PREC_ACCESS) -operator('[]', (a, b) => !b ? err() : (a = compile(a), b = compile(b), ctx => { const k = b(ctx); return unsafe(k) ? undefined : a(ctx)[k] })) +access('[]', ACCESS); // a.b -binary('.', PREC_ACCESS) -operator('.', (a, b) => (a = compile(a), b = !b[0] ? b[1] : b, unsafe(b) ? () => undefined : ctx => a(ctx)[b])) +binary('.', ACCESS); + +// a(b,c,d), a() +access('()', ACCESS); + +// Compile +const err = msg => { throw Error(msg) }; +operator('[]', (a, b) => { + // Array literal: [1,2,3] - b is strictly undefined (AST length 2) + if (b === undefined) { + a = !a ? [] : a[0] === ',' ? a.slice(1) : [a]; + a = a.map(a => a == null ? (() => undefined) : a[0] === '...' ? (a = compile(a[1]), ctx => a(ctx)) : (a = compile(a), ctx => [a(ctx)])); + return ctx => a.flatMap(a => a(ctx)); + } + // Member access: a[b] + if (b == null) err('Missing index'); + a = compile(a); b = compile(b); + return ctx => { const k = b(ctx); return unsafe(k) ? undefined : a(ctx)[k]; }; +}); +operator('.', (a, b) => (a = compile(a), b = !b[0] ? b[1] : b, unsafe(b) ? () => undefined : ctx => a(ctx)[b])); +operator('()', (a, b) => { + // Group: (expr) - no second argument means grouping, not call + if (b === undefined) return a == null ? err('Empty ()') : compile(a); + // Validate: no sparse arguments in calls + const hasSparse = n => n?.[0] === ',' && n.slice(1).some(a => a == null || hasSparse(a)); + if (hasSparse(b)) err('Empty argument'); + const args = !b ? () => [] : + b[0] === ',' ? (b = b.slice(1).map(compile), ctx => b.map(arg => arg(ctx))) : + (b = compile(b), ctx => [b(ctx)]); + return prop(a, (obj, path, ctx) => obj[path](...args(ctx)), true); +}); + +// Left-value check (valid assignment target) +export const isLval = n => + typeof n === 'string' || + (Array.isArray(n) && ( + n[0] === '.' || n[0] === '?.' || + (n[0] === '[]' && n.length === 3) || n[0] === '?.[]' || + (n[0] === '()' && n.length === 2 && isLval(n[1])) || + n[0] === '{}' + )); + +// Compile error helper +const compileErr = msg => { throw Error(msg) }; + +// Property accessor helper - compiles property access pattern to evaluator +export const prop = (a, fn, generic, obj, path) => ( + a == null ? compileErr('Empty ()') : + a[0] === '()' && a.length == 2 ? prop(a[1], fn, generic) : + typeof a === 'string' ? ctx => fn(ctx, a, ctx) : + a[0] === '.' ? (obj = compile(a[1]), path = a[2], ctx => fn(obj(ctx), path, ctx)) : + a[0] === '?.' ? (obj = compile(a[1]), path = a[2], ctx => { const o = obj(ctx); return o == null ? undefined : fn(o, path, ctx); }) : + a[0] === '[]' && a.length === 3 ? (obj = compile(a[1]), path = compile(a[2]), ctx => fn(obj(ctx), path(ctx), ctx)) : + a[0] === '?.[]' ? (obj = compile(a[1]), path = compile(a[2]), ctx => { const o = obj(ctx); return o == null ? undefined : fn(o, path(ctx), ctx); }) : + (a = compile(a), ctx => fn([a(ctx)], 0, ctx)) +); diff --git a/feature/accessor.js b/feature/accessor.js new file mode 100644 index 00000000..50c6a931 --- /dev/null +++ b/feature/accessor.js @@ -0,0 +1,49 @@ +/** + * Object accessor properties (getters/setters) + * + * AST: + * { get x() { body } } → ['{}', ['get', 'x', body]] + * { set x(v) { body } } → ['{}', ['set', 'x', 'v', body]] + */ +import { token, expr, skip, space, err, next, parse, cur, idx, operator, compile } from '../parse.js'; + +// Accessor marker for object property definitions +export const ACC = Symbol('accessor'); + +const ASSIGN = 20; +const OPAREN = 40, CPAREN = 41, OBRACE = 123, CBRACE = 125; + +// Shared parser for get/set — returns false if not valid accessor pattern (falls through to identifier) +// Returns false (not undefined) to signal "fall through without setting reserved" +const accessor = (kind) => a => { + if (a) return; // not prefix + space(); + const name = next(parse.id); + if (!name) return false; // no property name = not accessor (e.g. `{ get: 1 }`) + space(); + if (cur.charCodeAt(idx) !== OPAREN) return false; // not followed by ( = not accessor + skip(); + const params = expr(0, CPAREN); + space(); + if (cur.charCodeAt(idx) !== OBRACE) return false; + skip(); + return [kind, name, params, expr(0, CBRACE)]; +}; + +token('get', ASSIGN - 1, accessor('get')); +token('set', ASSIGN - 1, accessor('set')); + +// Compile +operator('get', (name, body) => { + body = body ? compile(body) : () => {}; + return ctx => [[ACC, name, { + get: function() { const s = Object.create(ctx || {}); s.this = this; return body(s); } + }]]; +}); + +operator('set', (name, param, body) => { + body = body ? compile(body) : () => {}; + return ctx => [[ACC, name, { + set: function(v) { const s = Object.create(ctx || {}); s.this = this; s[param] = v; body(s); } + }]]; +}); diff --git a/feature/add.js b/feature/add.js deleted file mode 100644 index 5aa851b2..00000000 --- a/feature/add.js +++ /dev/null @@ -1,22 +0,0 @@ - -import { binary, unary } from '../src/parse.js' -import { PREC_ADD, PREC_PREFIX, PREC_ASSIGN } from '../src/const.js' -import { compile, prop, operator } from '../src/compile.js' - -binary('+', PREC_ADD), operator('+', (a, b) => b && (a = compile(a), b = compile(b), ctx => a(ctx) + b(ctx))) -binary('-', PREC_ADD), operator('-', (a, b) => b && (a = compile(a), b = compile(b), ctx => a(ctx) - b(ctx))) - -unary('+', PREC_PREFIX), operator('+', (a, b) => !b && (a = compile(a), ctx => +a(ctx))) -unary('-', PREC_PREFIX), operator('-', (a, b) => !b && (a = compile(a), ctx => -a(ctx))) - -binary('+=', PREC_ASSIGN, true) -operator('+=', (a, b) => ( - b = compile(b), - prop(a, (container, path, ctx) => container[path] += b(ctx)) -)) - -binary('-=', PREC_ASSIGN, true) -operator('-=', (a, b) => ( - b = compile(b), - prop(a, (container, path, ctx) => (container[path] -= b(ctx))) -)) diff --git a/feature/array.js b/feature/array.js deleted file mode 100644 index 36cae6ed..00000000 --- a/feature/array.js +++ /dev/null @@ -1,11 +0,0 @@ -import { token, expr, group } from '../src/parse.js' -import { operator, compile } from '../src/compile.js' -import { PREC_TOKEN } from '../src/const.js' - -// [a,b,c] -group('[]', PREC_TOKEN) -operator('[]', (a, b) => b === undefined && ( - a = !a ? [] : a[0] === ',' ? a.slice(1) : [a], - a = a.map(a => a[0] === '...' ? (a = compile(a[1]), ctx => a(ctx)) : (a = compile(a), ctx => [a(ctx)])), - ctx => a.flatMap(a => (a(ctx)))) -) diff --git a/feature/arrow.js b/feature/arrow.js deleted file mode 100644 index dd033315..00000000 --- a/feature/arrow.js +++ /dev/null @@ -1,23 +0,0 @@ -import { binary, group } from "../src/parse.js" -import { compile, operator } from "../src/compile.js" -import { PREC_ASSIGN, PREC_TOKEN } from "../src/const.js" - -// arrow functions (useful for array methods) -binary('=>', PREC_ASSIGN, true) -operator('=>', - (a, b) => ( - a = a[0] === '()' ? a[1] : a, - a = !a ? [] : // () => - a[0] === ',' ? (a = a.slice(1)) : // (a,c) => - (a = [a]), // a => - - b = compile(b[0] === '{}' ? b[1] : b), // `=> {x}` -> `=> x` - - (ctx = null) => ( - ctx = Object.create(ctx), - (...args) => (a.map((a, i) => ctx[a] = args[i]), b(ctx)) - ) - ) -) - -binary('') diff --git a/feature/asi.js b/feature/asi.js index a94731ab..4799f229 100644 --- a/feature/asi.js +++ b/feature/asi.js @@ -1,69 +1,15 @@ -// ASI (Automatic Semicolon Insertion) preprocessor -// Wraps input to insert semicolons where JS would infer them -// Not a full JS spec implementation - covers common practical cases - -// Tokens that REQUIRE continuation (don't insert ; before these) -// Note: [ and { can START statements (array literal, block), so not included -const CONT_STARTS = /^[\.\(\+\-\*\/\%\&\|\^\~\?\:\,\<\>]|^&&|^\|\||^\.\.\.$/ - -// Lines that can't be statements by themselves (incomplete) -const INCOMPLETE = /[\+\-\*\/\%\&\|\^\~\?\:\,\=\<\>\(\{]$/ - -export function asi(src, { keepNewlines = false } = {}) { - const lines = src.split('\n') - const result = [] - - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim() - - // Skip empty lines - if (!line) { - if (keepNewlines) result.push(lines[i]) - continue - } - - // Skip comment lines - if (line.startsWith('//') || line.startsWith('/*')) { - if (keepNewlines) result.push(lines[i]) - continue - } - - result.push(lines[i]) - - // Already ends with semicolon, opening brace, comma, or closing brace - no insert - if (line.endsWith(';') || line.endsWith('{') || line.endsWith(',') || line.endsWith('}')) continue - - // Line is incomplete (ends with operator/opening paren) - no insert - if (INCOMPLETE.test(line)) continue - - // Find next non-empty, non-comment line - let nextLine = '' - for (let j = i + 1; j < lines.length; j++) { - const nl = lines[j].trim() - if (nl && !nl.startsWith('//') && !nl.startsWith('/*')) { - nextLine = nl - break - } - } - - // Next line starts with continuation token - no insert - const nextToken = nextLine.match(/^[^\s\w]*|^\w+/)?.[0] || '' - if (nextLine && CONT_STARTS.test(nextToken)) continue - - // If this is NOT the last code line, add semicolon - // (Don't add trailing semicolon - it makes result undefined) - if (nextLine) { - result[result.length - 1] = result[result.length - 1] + ';' - } - } - - // Join with space instead of newline for parser compatibility - return result.join(keepNewlines ? '\n' : ' ') -} - -// Wrap parse function with ASI preprocessing -export function withASI(parse) { - return (src, ...args) => parse(asi(src), ...args) -} - -export default asi +/** + * Automatic Semicolon Insertion (ASI) + * + * JS-style ASI: insert virtual ; when newline precedes illegal token at statement level + */ +import { parse } from '../parse.js'; + +const STATEMENT = 5; + +parse.asi = (token, prec, expr) => { + if (prec >= STATEMENT) return; // only at statement level + const next = expr(STATEMENT - .5); + if (!next) return; + return token?.[0] !== ';' ? [';', token, next] : (token.push(next), token); +}; diff --git a/feature/assign.js b/feature/assign.js deleted file mode 100644 index 3dab2673..00000000 --- a/feature/assign.js +++ /dev/null @@ -1,11 +0,0 @@ -import { binary, err } from "../src/parse.js"; -import { compile, operator, operators, prop } from "../src/compile.js"; -import { PREC_ASSIGN } from "../src/const.js"; - -// assignments -binary('=', PREC_ASSIGN, true) -operator('=', (a, b) => ( - b = compile(b), - // a = x, ((a)) = x, a.b = x, a['b'] = x - prop(a, (container, path, ctx) => container[path] = b(ctx)) -)) diff --git a/feature/async.js b/feature/async.js new file mode 100644 index 00000000..b750eb83 --- /dev/null +++ b/feature/async.js @@ -0,0 +1,45 @@ +// Async/await/yield: async function, async arrow, await, yield expressions +import { unary, expr, skip, space, cur, idx, word, operator, compile } from '../parse.js'; +import { keyword } from './block.js'; + +const PREFIX = 140, ASSIGN = 20; + +// await expr → ['await', expr] +unary('await', PREFIX); + +// yield expr → ['yield', expr] +// yield* expr → ['yield*', expr] +keyword('yield', PREFIX, () => { + space(); + if (cur[idx] === '*') { + skip(); + space(); + return ['yield*', expr(ASSIGN)]; + } + return ['yield', expr(ASSIGN)]; +}); + +// async function name() {} → ['async', ['function', name, params, body]] +// async () => {} → ['async', ['=>', params, body]] +// async x => {} → ['async', ['=>', x, body]] +keyword('async', PREFIX, () => { + space(); + // async function - check for 'function' word + if (word('function')) return ['async', expr(PREFIX)]; + // async arrow: async () => or async x => + // Parse at assign precedence to catch => operator + const params = expr(ASSIGN - .5); + return params && ['async', params]; +}); + +// Compile +operator('async', fn => { + const inner = compile(fn); + return ctx => { + const f = inner(ctx); + return async function(...args) { return f(...args); }; + }; +}); +operator('await', a => (a = compile(a), async ctx => await a(ctx))); +operator('yield', a => (a = a ? compile(a) : null, ctx => { throw { __yield__: a ? a(ctx) : undefined }; })); +operator('yield*', a => (a = compile(a), ctx => { throw { __yield_all__: a(ctx) }; })); diff --git a/feature/bitwise.js b/feature/bitwise.js deleted file mode 100644 index 5adc1a64..00000000 --- a/feature/bitwise.js +++ /dev/null @@ -1,11 +0,0 @@ -import { PREC_OR, PREC_AND, PREC_SHIFT, PREC_XOR, PREC_PREFIX } from "../src/const.js" -import { unary, binary } from "../src/parse.js" -import { operator, compile } from "../src/compile.js" - -unary('~', PREC_PREFIX), operator('~', (a, b) => !b && (a = compile(a), ctx => ~a(ctx))) - -binary('|', PREC_OR), operator('|', (a, b) => b && (a = compile(a), b = compile(b), ctx => a(ctx) | b(ctx))) - -binary('&', PREC_AND), operator('&', (a, b) => b && (a = compile(a), b = compile(b), ctx => a(ctx) & b(ctx))) - -binary('^', PREC_XOR), operator('^', (a, b) => b && (a = compile(a), b = compile(b), ctx => a(ctx) ^ b(ctx))) diff --git a/feature/block.js b/feature/block.js index 69023c12..b06f2fcc 100644 --- a/feature/block.js +++ b/feature/block.js @@ -1,46 +1,41 @@ -/** - * Block scope and control flow infrastructure - * - * AST: - * { a; b } → ['block', [';', a, b]] - * - * Shared by: if.js, loop.js, control.js - */ -import * as P from '../src/parse.js' -import { operator, compile } from '../src/compile.js' -import { OBRACE, CBRACE } from '../src/const.js' +// Block parsing helpers +import { expr, skip, space, lookup, err, parse, seek, cur, idx, parens, loc, operator, compile } from '../parse.js'; -const { expr, skip, space } = P +const STATEMENT = 5, OBRACE = 123, CBRACE = 125; -// Control signals for break/continue/return -class Break {} -class Continue {} -class Return { constructor(v) { this.value = v } } -export const BREAK = new Break(), CONTINUE = new Continue(), Return_ = Return +// keyword(op, prec, fn) - prefix-only word token +// keyword('while', 6, () => ['while', parens(), body()]) +// keyword('break', 6, () => ['break']) +// attaches .loc to array results for source mapping +export const keyword = (op, prec, map, c = op.charCodeAt(0), l = op.length, prev = lookup[c], r) => + lookup[c] = (a, curPrec, curOp, from = idx) => + !a && + (curOp ? op == curOp : (l < 2 || cur.substr(idx, l) == op) && (curOp = op)) && + curPrec < prec && + !parse.id(cur.charCodeAt(idx + l)) && + (seek(idx + l), (r = map()) ? loc(r, from) : (seek(from), !prev && err()), r) || + prev?.(a, curPrec, curOp); -// Shared loop body executor - handles control flow signals -export const loop = (body, ctx) => { - try { return { val: body(ctx) } } - catch (e) { - if (e === BREAK) return { brk: 1 } - if (e === CONTINUE) return { cnt: 1 } - if (e instanceof Return) return { ret: 1, val: e.value } - throw e - } -} +// infix(op, prec, fn) - infix word token (requires left operand) +// infix('catch', 6, a => ['catch', a, parens(), block()]) +// infix('finally', 6, a => ['finally', a, block()]) +// attaches .loc to array results for source mapping +export const infix = (op, prec, map, c = op.charCodeAt(0), l = op.length, prev = lookup[c], r) => + lookup[c] = (a, curPrec, curOp, from = idx) => + a && + (curOp ? op == curOp : (l < 2 || cur.substr(idx, l) == op) && (curOp = op)) && + curPrec < prec && + !parse.id(cur.charCodeAt(idx + l)) && + (seek(idx + l), loc(r = map(a), from), r) || + prev?.(a, curPrec, curOp); -// Block parsing helper - parses { body } or single expression -export const parseBody = () => { - if (space() === OBRACE) { - skip() - return ['block', expr(0, CBRACE)] - } - return expr(0) -} +// block() - parse required { body } +export const block = () => + (space() === OBRACE || err('Expected {'), skip(), expr(STATEMENT - .5, CBRACE) || null); -// Block operator - creates new scope -operator('block', body => { - if (body === undefined) return () => {} - body = compile(body) - return ctx => body(Object.create(ctx)) -}) +// body() - parse { body } or single statement +export const body = () => + space() !== OBRACE ? expr(STATEMENT + .5) : (skip(), ['block', expr(STATEMENT - .5, CBRACE) || null]); + +// Compile +operator('block', body => body === undefined ? () => {} : (body = compile(body), ctx => body(ctx))); diff --git a/feature/bool.js b/feature/bool.js deleted file mode 100644 index 7e998890..00000000 --- a/feature/bool.js +++ /dev/null @@ -1,5 +0,0 @@ -import { token } from "../src/parse.js" -import { PREC_TOKEN } from "../src/const.js" - -token('true', PREC_TOKEN, a => a ? err() : [, true]) -token('false', PREC_TOKEN, a => a ? err() : [, false]) diff --git a/feature/call.js b/feature/call.js deleted file mode 100644 index 499e4922..00000000 --- a/feature/call.js +++ /dev/null @@ -1,15 +0,0 @@ -import { access } from '../src/parse.js' -import { operator, compile, prop } from '../src/compile.js' -import { PREC_ACCESS } from '../src/const.js' - -// a(b,c,d), a() -access('()', PREC_ACCESS) -operator('()', (a, b, args) => b !== undefined && ( - args = !b ? () => [] : // a() - b[0] === ',' ? (b = b.slice(1).map(b => !b ? err() : compile(b)), ctx => b.map(arg => arg(ctx))) : // a(b,c) - (b = compile(b), ctx => [b(ctx)]), // a(b) - - // a(...args), a.b(...args), a[b](...args) - prop(a, (obj, path, ctx) => obj[path](...args(ctx)), true) -) -) diff --git a/feature/class.js b/feature/class.js new file mode 100644 index 00000000..b674b06e --- /dev/null +++ b/feature/class.js @@ -0,0 +1,69 @@ +// Class declarations and expressions +// class A extends B { ... } +import { binary, unary, token, expr, space, next, parse, literal, word, operator, compile } from '../parse.js'; +import { keyword, block } from './block.js'; + +const TOKEN = 200, PREFIX = 140, COMP = 90; +const STATIC = Symbol('static'); + +// super → literal +literal('super', Symbol.for('super')); + +// static member → ['static', member] +unary('static', PREFIX); + +// instanceof: object instanceof Constructor +binary('instanceof', COMP); + +// #private fields: #x → '#x' (identifier starting with #) +token('#', TOKEN, a => { + if (a) return; + const id = next(parse.id); + return id ? '#' + id : void 0; +}); + +// class [Name] [extends Base] { body } +keyword('class', TOKEN, () => { + space(); + let name = next(parse.id) || null; + // 'extends' parsed as name? → anonymous class + if (name === 'extends') name = null; + else { + space(); + if (!word('extends')) return ['class', name, null, block()]; + } + space(); + return ['class', name, expr(TOKEN), block()]; +}); + +// Compile +const err = msg => { throw Error(msg) }; +operator('instanceof', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) instanceof b(ctx))); +operator('class', (name, base, body) => { + base = base ? compile(base) : null; + body = body ? compile(body) : null; + return ctx => { + const Parent = base ? base(ctx) : Object; + const cls = function(...args) { + if (!(this instanceof cls)) return err('Class constructor must be called with new'); + const instance = base ? Reflect.construct(Parent, args, cls) : this; + if (cls.prototype.__constructor__) cls.prototype.__constructor__.apply(instance, args); + return instance; + }; + Object.setPrototypeOf(cls.prototype, Parent.prototype); + Object.setPrototypeOf(cls, Parent); + if (body) { + const methods = Object.create(ctx); + methods['super'] = Parent; + const entries = body(methods); + const items = Array.isArray(entries) && typeof entries[0]?.[0] === 'string' ? entries : []; + for (const [k, v] of items) { + if (k === 'constructor') cls.prototype.__constructor__ = v; + else cls.prototype[k] = v; + } + } + if (name) ctx[name] = cls; + return cls; + }; +}); +operator('static', a => (a = compile(a), ctx => [[STATIC, a(ctx)]])); diff --git a/feature/collection.js b/feature/collection.js new file mode 100644 index 00000000..4ba1a13a --- /dev/null +++ b/feature/collection.js @@ -0,0 +1,40 @@ +/** + * Collection literals: arrays and objects (Justin feature) + * + * [a, b, c] + * {a: 1, b: 2} + * {a, b} (shorthand) + */ +import { group, binary, operator, compile } from '../parse.js'; +import { ACC } from './accessor.js'; + +const ASSIGN = 20, TOKEN = 200; + +// [a,b,c] +group('[]', TOKEN); + +// {a:1, b:2, c:3} +group('{}', TOKEN); + +// a: b (colon operator for object properties) +binary(':', ASSIGN - 1, true); + +// Compile +operator('{}', (a, b) => { + if (b !== undefined) return; + a = !a ? [] : a[0] !== ',' ? [a] : a.slice(1); + const props = a.map(p => compile(typeof p === 'string' ? [':', p, p] : p)); + return ctx => { + const obj = {}, acc = {}; + for (const e of props.flatMap(f => f(ctx))) { + if (e[0] === ACC) { + const [, n, desc] = e; + acc[n] = { ...acc[n], ...desc, configurable: true, enumerable: true }; + } else obj[e[0]] = e[1]; + } + for (const n in acc) Object.defineProperty(obj, n, acc[n]); + return obj; + }; +}); +operator(':', (a, b) => (b = compile(b), Array.isArray(a) ? + (a = compile(a), ctx => [[a(ctx), b(ctx)]]) : ctx => [[a, b(ctx)]])); diff --git a/feature/comment.js b/feature/comment.js index ca40737a..6c4daa09 100644 --- a/feature/comment.js +++ b/feature/comment.js @@ -1,6 +1,26 @@ -import { SPACE, STAR, PREC_TOKEN } from "../src/const.js" -import { token, skip, next, cur, idx, expr } from "../src/parse.js" +/** Configurable comments via parse.comment = { start: end } */ +import { parse, cur, idx, seek } from '../parse.js'; -// /**/, // -token('/*', PREC_TOKEN, (a, prec) => (next(c => c !== STAR && cur.charCodeAt(idx + 1) !== 47), skip(),skip(), a || expr(prec) || [])) -token('//', PREC_TOKEN, (a, prec) => (next(c => c >= SPACE), a || expr(prec) || [])) +const SPACE = 32, space = parse.space; + +// Default C-style comments +parse.comment ??= { '//': '\n', '/*': '*/' }; + +// Cached array: [[start, end, firstCharCode], ...] +let comments; + +parse.space = () => { + if (!comments) comments = Object.entries(parse.comment).map(([s, e]) => [s, e, s.charCodeAt(0)]); + for (var cc; (cc = space()); ) { + for (var j = 0, c; c = comments[j++]; ) { + if (cc === c[2] && cur.substr(idx, c[0].length) === c[0]) { + var i = idx + c[0].length; + if (c[1] === '\n') while (cur.charCodeAt(i) >= SPACE) i++; + else { while (cur[i] && cur.substr(i, c[1].length) !== c[1]) i++; if (cur[i]) i += c[1].length; } + seek(i); cc = 0; break; + } + } + if (cc) return cc; + } + return cc; +}; diff --git a/feature/compare.js b/feature/compare.js deleted file mode 100644 index 5297fdf5..00000000 --- a/feature/compare.js +++ /dev/null @@ -1,11 +0,0 @@ -import { PREC_EQ, PREC_COMP } from '../src/const.js' -import { unary, binary } from "../src/parse.js" -import { operator, compile } from "../src/compile.js" - - -binary('==', PREC_EQ), operator('==', (a, b) => b && (a = compile(a), b = compile(b), ctx => a(ctx) == b(ctx))) -binary('!=', PREC_EQ), operator('!=', (a, b) => b && (a = compile(a), b = compile(b), ctx => a(ctx) != b(ctx))) -binary('>', PREC_COMP), operator('>', (a, b) => b && (a = compile(a), b = compile(b), ctx => a(ctx) > b(ctx))) -binary('<', PREC_COMP), operator('<', (a, b) => b && (a = compile(a), b = compile(b), ctx => a(ctx) < b(ctx))) -binary('>=', PREC_COMP), operator('>=', (a, b) => b && (a = compile(a), b = compile(b), ctx => a(ctx) >= b(ctx))) -binary('<=', PREC_COMP), operator('<=', (a, b) => b && (a = compile(a), b = compile(b), ctx => a(ctx) <= b(ctx))) diff --git a/feature/destruct.js b/feature/destruct.js new file mode 100644 index 00000000..5de34504 --- /dev/null +++ b/feature/destruct.js @@ -0,0 +1,33 @@ +/** + * Destructuring patterns and binding + * + * Handles: [a, b] = arr, {x, y} = obj, [a, ...rest] = arr, {x = default} = obj + */ +import { compile } from '../parse.js'; + +// Destructure value into context +export const destructure = (pattern, value, ctx) => { + if (typeof pattern === 'string') { ctx[pattern] = value; return; } + const [op, ...items] = pattern; + if (op === '{}') { + for (const item of items) { + let key, binding, def; + if (item[0] === '=') [, [, key, binding], def] = item; + else [, key, binding] = item; + let val = value[key]; + if (val === undefined && def) val = compile(def)(ctx); + destructure(binding, val, ctx); + } + } else if (op === '[]') { + let i = 0; + for (const item of items) { + if (item === null) { i++; continue; } + if (Array.isArray(item) && item[0] === '...') { ctx[item[1]] = value.slice(i); break; } + let binding = item, def; + if (Array.isArray(item) && item[0] === '=') [, binding, def] = item; + let val = value[i++]; + if (val === undefined && def) val = compile(def)(ctx); + destructure(binding, val, ctx); + } + } +}; diff --git a/feature/function.js b/feature/function.js new file mode 100644 index 00000000..bd2eee45 --- /dev/null +++ b/feature/function.js @@ -0,0 +1,44 @@ +// Function declarations and expressions +import { space, next, parse, parens, expr, operator, compile } from '../parse.js'; +import { RETURN } from './loop.js'; +import { keyword, block } from './block.js'; + +const TOKEN = 200; + +keyword('function', TOKEN, () => { + space(); + const name = next(parse.id); + name && space(); + return ['function', name, parens() || null, block()]; +}); + +// Compile +operator('function', (name, params, body) => { + body = body ? compile(body) : () => undefined; + // Normalize params: null → [], 'x' → ['x'], [',', 'a', 'b'] → ['a', 'b'] + const ps = !params ? [] : params[0] === ',' ? params.slice(1) : [params]; + // Check for rest param + let restName = null, restIdx = -1; + const last = ps[ps.length - 1]; + if (Array.isArray(last) && last[0] === '...') { + restIdx = ps.length - 1; + restName = last[1]; + ps.length--; + } + return ctx => { + const fn = (...args) => { + const l = {}; + ps.forEach((p, i) => l[p] = args[i]); + if (restName) l[restName] = args.slice(restIdx); + const fnCtx = new Proxy(l, { + get: (l, k) => k in l ? l[k] : ctx[k], + set: (l, k, v) => ((k in l ? l : ctx)[k] = v, true), + has: (l, k) => k in l || k in ctx + }); + try { return body(fnCtx); } + catch (e) { if (e?.type === RETURN) return e.value; throw e; } + }; + if (name) ctx[name] = fn; + return fn; + }; +}); diff --git a/feature/group.js b/feature/group.js index 8392a3e3..9e19aa60 100644 --- a/feature/group.js +++ b/feature/group.js @@ -1,11 +1,41 @@ -import { err, nary, group } from '../src/parse.js' -import { compile, operator } from '../src/compile.js' -import { PREC_ACCESS, PREC_GROUP, PREC_SEQ, PREC_STATEMENT } from '../src/const.js' +import { nary, group, operator, compile } from '../parse.js'; +import { BREAK, CONTINUE } from './loop.js'; +import { prop } from './access.js'; -// (a,b,c), (a) — uses PREC_ACCESS to avoid conflict with ?. -group('()', PREC_ACCESS) -operator('()', (a, b) => b === undefined && (!a && err('Empty ()'), compile(a))) +const STATEMENT = 5, SEQ = 10, ACCESS = 170; -const last = (...args) => (args = args.map(compile), ctx => args.map(arg => arg(ctx)).pop()) -nary(',', PREC_SEQ), operator(',', last) -nary(';', PREC_STATEMENT, true), operator(';', last) +// (a,b,c), (a) — uses ACCESS to avoid conflict with ?. +group('()', ACCESS); + +// Sequences +nary(',', SEQ); +nary(';', STATEMENT, true); // right-assoc to allow same-prec statements + +// Compile +const err = msg => { throw Error(msg) }; +operator('()', (a, b) => { + // Group: (expr) - no second argument means grouping, not call + if (b === undefined) return a == null ? err('Empty ()') : compile(a); + // Validate: no sparse arguments in calls + const hasSparse = n => n?.[0] === ',' && n.slice(1).some(a => a == null || hasSparse(a)); + if (hasSparse(b)) err('Empty argument'); + const args = !b ? () => [] : + b[0] === ',' ? (b = b.slice(1).map(compile), ctx => b.map(arg => arg(ctx))) : + (b = compile(b), ctx => [b(ctx)]); + return prop(a, (obj, path, ctx) => obj[path](...args(ctx)), true); +}); + +// sequence returns last evaluated value; catches BREAK/CONTINUE and attaches result +const seq = (...args) => (args = args.map(compile), ctx => { + let r; + for (const arg of args) { + try { r = arg(ctx); } + catch (e) { + if (e?.type === BREAK || e?.type === CONTINUE) { e.value = r; throw e; } + throw e; + } + } + return r; +}); +operator(',', seq); +operator(';', seq); diff --git a/feature/if.js b/feature/if.js index ac6d4e81..003a3467 100644 --- a/feature/if.js +++ b/feature/if.js @@ -1,29 +1,28 @@ -/** - * Conditionals: if/else - * - * AST: - * if (c) a else b → ['if', c, a, b?] - */ -import * as P from '../src/parse.js' -import { operator, compile } from '../src/compile.js' -import { PREC_STATEMENT, OPAREN, CPAREN } from '../src/const.js' -import { parseBody } from './block.js' +// If/else statement - else consumed internally +import { space, skip, parens, word, idx, seek, operator, compile } from '../parse.js'; +import { body, keyword } from './block.js'; -const { token, expr, skip, space, err, parse } = P +const STATEMENT = 5, SEMI = 59; -// if (cond) body [else alt] -token('if', PREC_STATEMENT, a => { - if (a) return - space() === OPAREN || err('Expected (') - skip() - const cond = expr(0, CPAREN), body = parseBody() - space() - const alt = P.cur.substr(P.idx, 4) === 'else' && !parse.id(P.cur.charCodeAt(P.idx + 4)) - ? (skip(), skip(), skip(), skip(), parseBody()) : undefined - return alt !== undefined ? ['if', cond, body, alt] : ['if', cond, body] -}) +// Check for `else` after optional semicolon +const checkElse = () => { + const from = idx; + if (space() === SEMI) skip(); + space(); + if (word('else')) return skip(4), true; + return seek(from), false; +}; +// if (cond) body [else body] - self-contained +keyword('if', STATEMENT + 1, () => { + space(); + const node = ['if', parens(), body()]; + if (checkElse()) node.push(body()); + return node; +}); + +// Compile operator('if', (cond, body, alt) => { - cond = compile(cond); body = compile(body); alt = alt !== undefined ? compile(alt) : null - return ctx => cond(ctx) ? body(ctx) : alt?.(ctx) -}) + cond = compile(cond); body = compile(body); alt = alt !== undefined ? compile(alt) : null; + return ctx => cond(ctx) ? body(ctx) : alt?.(ctx); +}); diff --git a/feature/increment.js b/feature/increment.js deleted file mode 100644 index 5e9958cf..00000000 --- a/feature/increment.js +++ /dev/null @@ -1,11 +0,0 @@ -import { token, expr } from "../src/parse.js" -import { operator, compile, prop } from "../src/compile.js" -import { PREC_POSTFIX } from "../src/const.js" - -token('++', PREC_POSTFIX, a => a ? ['++', a, null,] : ['++', expr(PREC_POSTFIX - 1)]) -// ++a, ++((a)), ++a.b, ++a[b] -operator('++', (a,b) => prop(a, b === null ? (obj, path) => obj[path]++ : (obj, path) => ++obj[path])) - -token('--', PREC_POSTFIX, a => a ? ['--', a, null,] : ['--', expr(PREC_POSTFIX - 1)]) -// --a, --a.b, --a[b] -operator('--', (a, b) => prop(a, b === null ? (obj, path) => obj[path]-- : (obj, path) => --obj[path])) diff --git a/feature/literal.js b/feature/literal.js new file mode 100644 index 00000000..c9299c06 --- /dev/null +++ b/feature/literal.js @@ -0,0 +1,13 @@ +/** + * Literal values + * + * true, false, null, undefined, NaN, Infinity + */ +import { literal } from '../parse.js'; + +literal('true', true); +literal('false', false); +literal('null', null); +literal('undefined', undefined); +literal('NaN', NaN); +literal('Infinity', Infinity); diff --git a/feature/logic.js b/feature/logic.js deleted file mode 100644 index 4584f485..00000000 --- a/feature/logic.js +++ /dev/null @@ -1,11 +0,0 @@ -import { PREC_LOR, PREC_LAND, PREC_PREFIX, PREC_ASSIGN } from '../src/const.js'; -import { unary, binary } from "../src/parse.js" -import { operator, compile } from "../src/compile.js" - -unary('!', PREC_PREFIX), operator('!', (a, b) => !b && (a = compile(a), ctx => !a(ctx))) - -binary('||', PREC_LOR) -operator('||', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) || b(ctx))) - -binary('&&', PREC_LAND) -operator('&&', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) && b(ctx))) diff --git a/feature/loop.js b/feature/loop.js index 11efdb54..65d61bfc 100644 --- a/feature/loop.js +++ b/feature/loop.js @@ -1,116 +1,123 @@ -/** - * Loops: while, for, break, continue, return - * - * AST: - * while (c) a → ['while', c, a] - * for (i;c;s) a → ['for', i, c, s, a] - * break/continue → ['break'] / ['continue'] - * return x → ['return', x?] - */ -import * as P from '../src/parse.js' -import { operator, compile } from '../src/compile.js' -import { PREC_STATEMENT, OPAREN, CPAREN, CBRACE, PREC_SEQ, PREC_TOKEN } from '../src/const.js' -import { parseBody, loop, BREAK, CONTINUE, Return_ as Return } from './block.js' -export { BREAK, CONTINUE } from './block.js' +// Loops: while, do-while, for, for await, break, continue, return +import { expr, skip, space, parse, word, parens, cur, idx, operator, compile } from '../parse.js'; +import { body, keyword } from './block.js'; +import { destructure } from './destruct.js'; -const { token, expr, skip, space, err, next, parse } = P -const SEMI = 59 +// Control flow symbols +export const BREAK = Symbol('break'), CONTINUE = Symbol('continue'), RETURN = Symbol('return'); -// while (cond) body -token('while', PREC_STATEMENT, a => { - if (a) return - space() === OPAREN || err('Expected (') - skip() - return ['while', expr(0, CPAREN), parseBody()] -}) +// Loop body executor - catches control flow and returns status +export const loop = (body, ctx) => { + try { return { v: body(ctx) }; } + catch (e) { + if (e?.type === BREAK) return { b: 1 }; + if (e?.type === CONTINUE) return { c: 1 }; + if (e?.type === RETURN) return { r: 1, v: e.value }; + throw e; + } +}; -operator('while', (cond, body) => { - cond = compile(cond); body = compile(body) - return ctx => { - let r, res - while (cond(ctx)) { - r = loop(body, ctx) - if (r.brk) break - if (r.cnt) continue - if (r.ret) return r.val - res = r.val - } - return res +const STATEMENT = 5, CBRACE = 125, SEMI = 59; + +keyword('while', STATEMENT + 1, () => (space(), ['while', parens(), body()])); +keyword('do', STATEMENT + 1, () => (b => (space(), skip(5), space(), ['do', b, parens()]))(body())); + +// for / for await +keyword('for', STATEMENT + 1, () => { + space(); + // for await (x of y) + if (word('await')) { + skip(5); + space(); + return ['for await', parens(), body()]; } -}) + return ['for', parens(), body()]; +}); -// for (init; cond; step) body -// Note: init supports both expressions AND let/const declarations -token('for', PREC_STATEMENT, a => { - if (a) return - space() === OPAREN || err('Expected (') - skip() - // Parse init: can be expression (PREC_SEQ) or let/const declaration (PREC_STATEMENT) - // Using expr(PREC_SEQ) excludes statement-level tokens like let/const - // So we detect let/const keywords and parse the declaration inline - let init - const cc = space() - if (cc === SEMI) init = null - else if (cc === 108 && P.cur.substr(P.idx, 3) === 'let' && !parse.id(P.cur.charCodeAt(P.idx + 3))) { - skip(); skip(); skip() // skip 'let' - space() - const name = next(parse.id) - if (!name) err('Expected identifier') - space() - if (P.cur.charCodeAt(P.idx) === 61 && P.cur.charCodeAt(P.idx + 1) !== 61) { - skip(); init = ['let', name, expr(PREC_SEQ)] - } else init = ['let', name] - } else if (cc === 99 && P.cur.substr(P.idx, 5) === 'const' && !parse.id(P.cur.charCodeAt(P.idx + 5))) { - skip(); skip(); skip(); skip(); skip() // skip 'const' - space() - const name = next(parse.id) - if (!name) err('Expected identifier') - space() - P.cur.charCodeAt(P.idx) === 61 && P.cur.charCodeAt(P.idx + 1) !== 61 || err('Expected =') - skip(); init = ['const', name, expr(PREC_SEQ)] - } else init = expr(PREC_SEQ) - space() === SEMI ? skip() : err('Expected ;') - const cond = space() === SEMI ? null : expr(PREC_SEQ) - space() === SEMI ? skip() : err('Expected ;') - const step = space() === CPAREN ? null : expr(PREC_SEQ) - space() === CPAREN ? skip() : err('Expected )') - return ['for', init, cond, step, parseBody()] -}) +keyword('break', STATEMENT + 1, () => ['break']); +keyword('continue', STATEMENT + 1, () => ['continue']); +keyword('return', STATEMENT + 1, () => { + parse.asi && (parse.newline = false); + space(); + const c = cur.charCodeAt(idx); + return !c || c === CBRACE || c === SEMI || parse.newline ? ['return'] : ['return', expr(STATEMENT)]; +}); -operator('for', (init, cond, step, body) => { - init = init ? compile(init) : null - cond = cond ? compile(cond) : () => true - step = step ? compile(step) : null - body = compile(body) +// Compile +operator('while', (cond, body) => { + cond = compile(cond); body = compile(body); return ctx => { - let r, res - for (init?.(ctx); cond(ctx); step?.(ctx)) { - r = loop(body, ctx) - if (r.brk) break - if (r.cnt) continue - if (r.ret) return r.val - res = r.val - } - return res - } -}) + let r, res; + while (cond(ctx)) if ((r = loop(body, ctx)).b) break; else if (r.r) return r.v; else if (!r.c) res = r.v; + return res; + }; +}); -// break / continue / return -token('break', PREC_TOKEN, a => a ? null : ['break']) -operator('break', () => () => { throw BREAK }) +operator('do', (body, cond) => { + body = compile(body); cond = compile(cond); + return ctx => { + let r, res; + do { if ((r = loop(body, ctx)).b) break; else if (r.r) return r.v; else if (!r.c) res = r.v; } while (cond(ctx)); + return res; + }; +}); + +operator('for', (head, body) => { + // Normalize head: [';', init, cond, step] or single expr (for-in/of) + if (Array.isArray(head) && head[0] === ';') { + let [, init, cond, step] = head; + init = init ? compile(init) : null; + cond = cond ? compile(cond) : () => true; + step = step ? compile(step) : null; + body = compile(body); + return ctx => { + let r, res; + for (init?.(ctx); cond(ctx); step?.(ctx)) + if ((r = loop(body, ctx)).b) break; else if (r.r) return r.v; else if (!r.c) res = r.v; + return res; + }; + } + // For-in/of: head is ['in', lhs, rhs] or ['of', lhs, rhs] + if (Array.isArray(head) && (head[0] === 'in' || head[0] === 'of')) { + let [op, lhs, rhs] = head; + // Extract name from declaration: ['let', 'x'] → 'x' + if (Array.isArray(lhs) && (lhs[0] === 'let' || lhs[0] === 'const' || lhs[0] === 'var')) lhs = lhs[1]; + if (op === 'in') return forIn(lhs, rhs, body); + if (op === 'of') return forOf(lhs, rhs, body); + } +}); -token('continue', PREC_TOKEN, a => a ? null : ['continue']) -operator('continue', () => () => { throw CONTINUE }) +const forOf = (name, iterable, body) => { + iterable = compile(iterable); body = compile(body); + const isPattern = Array.isArray(name); + return ctx => { + let r, res; + const prev = isPattern ? null : ctx[name]; + for (const val of iterable(ctx)) { + if (isPattern) destructure(name, val, ctx); else ctx[name] = val; + if ((r = loop(body, ctx)).b) break; else if (r.r) return r.v; else if (!r.c) res = r.v; + } + if (!isPattern) ctx[name] = prev; + return res; + }; +}; -token('return', PREC_STATEMENT, a => { - if (a) return - space() - const c = P.cur.charCodeAt(P.idx) - if (!c || c === CBRACE || c === SEMI) return ['return'] - return ['return', expr(PREC_STATEMENT)] -}) +const forIn = (name, obj, body) => { + obj = compile(obj); body = compile(body); + const isPattern = Array.isArray(name); + return ctx => { + let r, res; + const prev = isPattern ? null : ctx[name]; + for (const key in obj(ctx)) { + if (isPattern) destructure(name, key, ctx); else ctx[name] = key; + if ((r = loop(body, ctx)).b) break; else if (r.r) return r.v; else if (!r.c) res = r.v; + } + if (!isPattern) ctx[name] = prev; + return res; + }; +}; -operator('return', val => { - val = val !== undefined ? compile(val) : null - return ctx => { throw new Return(val?.(ctx)) } -}) +operator('break', () => () => { throw { type: BREAK }; }); +operator('continue', () => () => { throw { type: CONTINUE }; }); +operator('return', val => (val = val !== undefined ? compile(val) : null, + ctx => { throw { type: RETURN, value: val?.(ctx) }; })); diff --git a/feature/module.js b/feature/module.js new file mode 100644 index 00000000..1f522c74 --- /dev/null +++ b/feature/module.js @@ -0,0 +1,42 @@ +/** + * Import/Export with contextual 'from' operator + * + * AST: + * import './x.js' → ['import', path] + * import X from './x.js' → ['import', ['from', 'X', path]] + * import { a, b } from './x' → ['import', ['from', ['{}', ...], path]] + * import * as X from './x.js' → ['import', ['from', ['as', '*', X], path]] + * export { a } from './x' → ['export', ['from', ['{}', ...], path]] + * export const x = 1 → ['export', decl] + */ +import { token, expr, space, lookup, skip } from '../parse.js'; +import { keyword } from './block.js'; + +const STATEMENT = 5, SEQ = 10, STAR = 42; + +// * as prefix in import context (import * as X) +const prevStar = lookup[STAR]; +lookup[STAR] = (a, prec) => !a ? (skip(), '*') : prevStar?.(a, prec); + +// 'from' as contextual binary - only after import-like LHS (not = or ,), false in prefix for identifier fallback +token('from', SEQ + 1, a => !a ? false : a[0] !== '=' && a[0] !== ',' && (space(), ['from', a, expr(SEQ + 1)])); + +// 'as' for aliasing: * as X, { a as b }. False in prefix for identifier fallback +token('as', SEQ + 2, a => !a ? false : (space(), ['as', a, expr(SEQ + 2)])); + +// import: prefix that parses specifiers + from + path +keyword('import', STATEMENT, () => (space(), ['import', expr(SEQ)])); + +// export: prefix for declarations or re-exports (use STATEMENT to capture const/let/function) +keyword('export', STATEMENT, () => (space(), ['export', expr(STATEMENT)])); + +// default: prefix for export default +keyword('default', SEQ + 1, () => (space(), ['default', expr(SEQ)])); + +// Compile stubs - import/export are parse-only (no runtime semantics) +import { operator, compile } from '../parse.js'; +operator('import', () => () => undefined); +operator('export', () => () => undefined); +operator('from', (a, b) => () => undefined); +operator('as', (a, b) => () => undefined); +operator('default', (a) => compile(a)); diff --git a/feature/mult.js b/feature/mult.js deleted file mode 100644 index 5378799d..00000000 --- a/feature/mult.js +++ /dev/null @@ -1,25 +0,0 @@ -import { binary } from '../src/parse.js' -import { operator, compile, prop } from '../src/compile.js' -import { PREC_MULT, PREC_ASSIGN } from '../src/const.js' - -binary('*', PREC_MULT), operator('*', (a, b) => b && (a = compile(a), b = compile(b), ctx => a(ctx) * b(ctx))) -binary('/', PREC_MULT), operator('/', (a, b) => b && (a = compile(a), b = compile(b), ctx => a(ctx) / b(ctx))) -binary('%', PREC_MULT), operator('%', (a, b) => b && (a = compile(a), b = compile(b), ctx => a(ctx) % b(ctx))) - -binary('*=', PREC_ASSIGN, true) -operator('*=', (a, b) => ( - b = compile(b), - prop(a, (container, path, ctx) => container[path] *= b(ctx)) -)) - -binary('/=', PREC_ASSIGN, true) -operator('/=', (a, b) => ( - b = compile(b), - prop(a, (container, path, ctx) => container[path] /= b(ctx)) -)) - -binary('%=', PREC_ASSIGN, true) -operator('%=', (a, b) => ( - b = compile(b), - prop(a, (container, path, ctx) => container[path] %= b(ctx)) -)) diff --git a/feature/number.js b/feature/number.js index eb95d060..2f0f0ae0 100644 --- a/feature/number.js +++ b/feature/number.js @@ -1,52 +1,48 @@ -import { lookup, next, err, skip, cur, idx } from "../src/parse.js" -import { PERIOD, _0, _E, _e, _9 } from "../src/const.js" +/** + * Numbers with configurable prefix notation + * + * Configurable via parse.number: { '0x': 16, '0b': 2, '0o': 8 } + */ +import { parse, lookup, next, err, skip, idx, cur } from '../parse.js'; -// Char codes for prefixes -const _b = 98, _B = 66, _o = 111, _O = 79, _x = 120, _X = 88 -const _a = 97, _f = 102, _A = 65, _F = 70 -const PLUS = 43, MINUS = 45 +const PERIOD = 46, _0 = 48, _9 = 57, _E = 69, _e = 101, PLUS = 43, MINUS = 45; +const _a = 97, _f = 102, _A = 65, _F = 70; -// Check if char at offset is digit or sign (valid after 'e') -const isExpFollow = off => { - const c = cur.charCodeAt(idx + off) - return (c >= _0 && c <= _9) || c === PLUS || c === MINUS -} +// Decimal number - check for .. range operator (don't consume . if followed by .) +const num = a => [, ( + a = +next(c => + // . is decimal only if NOT followed by another . (range operator) + (c === PERIOD && cur.charCodeAt(idx + 1) !== PERIOD) || + (c >= _0 && c <= _9) || + ((c === _E || c === _e) && ((c = cur.charCodeAt(idx + 1)) >= _0 && c <= _9 || c === PLUS || c === MINUS) ? 2 : 0) + ) +) != a ? err() : a]; -// parse decimal number (with optional exponent) -// Only consume 'e' if followed by digit or +/- -const num = (a, _) => [, ( - a = +next(c => (c === PERIOD) || (c >= _0 && c <= _9) || ((c === _E || c === _e) && isExpFollow(1) ? 2 : 0)) -) != a ? err() : a] +// Char test for prefix base +const charTest = { + 2: c => c === 48 || c === 49, + 8: c => c >= 48 && c <= 55, + 16: c => (c >= _0 && c <= _9) || (c >= _a && c <= _f) || (c >= _A && c <= _F) +}; -// .1 -lookup[PERIOD] = a => !a && num() +// Default: no prefixes +parse.number = null; -// 1-9 (non-zero starts decimal) -for (let i = _0 + 1; i <= _9; i++) lookup[i] = a => a ? err() : num() +// .1 (but not .. range) +lookup[PERIOD] = a => !a && cur.charCodeAt(idx + 1) !== PERIOD && num(); -// 0 - check for prefix (0b, 0o, 0x) or plain decimal +// 0-9: check parse.number for prefix config +for (let i = _0; i <= _9; i++) lookup[i] = a => a ? void 0 : num(); lookup[_0] = a => { - if (a) return err() - const nextChar = cur.charCodeAt(idx + 1) - - // Binary: 0b - if (nextChar === _b || nextChar === _B) { - skip(); skip() // consume '0b' - const s = next(c => c === 48 || c === 49) // 0 or 1 - return [, parseInt(s, 2)] + if (a) return; + const cfg = parse.number; + if (cfg) { + for (const [pre, base] of Object.entries(cfg)) { + if (pre[0] === '0' && cur[idx + 1]?.toLowerCase() === pre[1]) { + skip(2); + return [, parseInt(next(charTest[base]), base)]; + } + } } - // Octal: 0o - if (nextChar === _o || nextChar === _O) { - skip(); skip() // consume '0o' - const s = next(c => c >= 48 && c <= 55) // 0-7 - return [, parseInt(s, 8)] - } - // Hex: 0x - if (nextChar === _x || nextChar === _X) { - skip(); skip() // consume '0x' - const s = next(c => (c >= _0 && c <= _9) || (c >= _a && c <= _f) || (c >= _A && c <= _F)) - return [, parseInt(s, 16)] - } - - return num() -} + return num(); +}; diff --git a/feature/object.js b/feature/object.js deleted file mode 100644 index 02edfcfe..00000000 --- a/feature/object.js +++ /dev/null @@ -1,17 +0,0 @@ -import { token, expr, group, binary } from '../src/parse.js' -import { operator, compile } from '../src/compile.js' -import { PREC_ASSIGN, PREC_SEQ, PREC_TOKEN } from '../src/const.js' - - -// {a:1, b:2, c:3} -group('{}', PREC_TOKEN) -operator('{}', (a, b) => b === undefined && ( - // {}, {a:b}, {a}, {a, b} - a = (!a ? [] : a[0] !== ',' ? [a] : a.slice(1)), - a = a.map(p => compile(typeof p === 'string' ? [':', p, p] : p)), - ctx => Object.fromEntries(a.flatMap(frag => frag(ctx))) -)) - -binary(':', PREC_ASSIGN - 1, true) -// "a": a, a: a -operator(':', (a, b) => (b = compile(b), Array.isArray(a) ? (a = compile(a), ctx => [[a(ctx), b(ctx)]]) : ctx => [[a, b(ctx)]])) diff --git a/feature/op/arithmetic.js b/feature/op/arithmetic.js new file mode 100644 index 00000000..4730e11e --- /dev/null +++ b/feature/op/arithmetic.js @@ -0,0 +1,29 @@ +/** + * Arithmetic operators + * + * + - * / % + * Unary: + - + */ +import { binary, unary, operator, compile } from '../../parse.js'; + +const ADD = 110, MULT = 120, PREFIX = 140; + +binary('+', ADD); +binary('-', ADD); +binary('*', MULT); +binary('/', MULT); +binary('%', MULT); + +unary('+', PREFIX); +unary('-', PREFIX); + +// Compile +operator('+', (a, b) => b !== undefined ? + (a = compile(a), b = compile(b), ctx => a(ctx) + b(ctx)) : + (a = compile(a), ctx => +a(ctx))); +operator('-', (a, b) => b !== undefined ? + (a = compile(a), b = compile(b), ctx => a(ctx) - b(ctx)) : + (a = compile(a), ctx => -a(ctx))); +operator('*', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) * b(ctx))); +operator('/', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) / b(ctx))); +operator('%', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) % b(ctx))); diff --git a/feature/op/arrow.js b/feature/op/arrow.js new file mode 100644 index 00000000..ee497248 --- /dev/null +++ b/feature/op/arrow.js @@ -0,0 +1,33 @@ +/** + * Arrow function operator + * + * (a, b) => expr → arrow function + * + * Common in: JS, TS, Java, C#, Kotlin, Scala + */ +import { binary, operator, compile } from '../../parse.js'; + +const ASSIGN = 20; + +binary('=>', ASSIGN, true); + +// Compile +operator('=>', (a, b) => { + a = a[0] === '()' ? a[1] : a; + a = !a ? [] : a[0] === ',' ? a.slice(1) : [a]; + let restIdx = -1, restName = null; + if (a.length && Array.isArray(a[a.length - 1]) && a[a.length - 1][0] === '...') { + restIdx = a.length - 1; + restName = a[restIdx][1]; + a = a.slice(0, -1); + } + b = compile(b[0] === '{}' ? b[1] : b); + return (ctx = null) => { + ctx = Object.create(ctx); + return (...args) => { + a.forEach((p, i) => ctx[p] = args[i]); + if (restName) ctx[restName] = args.slice(restIdx); + return b(ctx); + }; + }; +}); diff --git a/feature/op/assign-logical.js b/feature/op/assign-logical.js new file mode 100644 index 00000000..e5cd76dc --- /dev/null +++ b/feature/op/assign-logical.js @@ -0,0 +1,33 @@ +/** + * Logical/nullish assignment operators + destructuring + * + * ||= &&= ??= + destructuring support for let/const/var + */ +import { binary, operator, compile } from '../../parse.js'; +import { destructure } from '../destruct.js'; +import { isLval, prop } from '../access.js'; + +const ASSIGN = 20; +const err = msg => { throw Error(msg) }; + +binary('||=', ASSIGN, true); +binary('&&=', ASSIGN, true); +binary('??=', ASSIGN, true); + +// Override = to support destructuring +operator('=', (a, b) => { + // Handle let/const/var declarations: ['=', ['let', pattern], value] + if (Array.isArray(a) && (a[0] === 'let' || a[0] === 'const' || a[0] === 'var')) { + const pattern = a[1]; + b = compile(b); + if (typeof pattern === 'string') return ctx => { ctx[pattern] = b(ctx); }; + return ctx => destructure(pattern, b(ctx), ctx); + } + isLval(a) || err('Invalid assignment target'); + return (b = compile(b), prop(a, (obj, path, ctx) => obj[path] = b(ctx))); +}); + +// Compile +operator('||=', (a, b) => (isLval(a) || err('Invalid assignment target'), b = compile(b), prop(a, (o, k, ctx) => o[k] ||= b(ctx)))); +operator('&&=', (a, b) => (isLval(a) || err('Invalid assignment target'), b = compile(b), prop(a, (o, k, ctx) => o[k] &&= b(ctx)))); +operator('??=', (a, b) => (isLval(a) || err('Invalid assignment target'), b = compile(b), prop(a, (o, k, ctx) => o[k] ??= b(ctx)))); diff --git a/feature/op/assignment.js b/feature/op/assignment.js new file mode 100644 index 00000000..e5b21634 --- /dev/null +++ b/feature/op/assignment.js @@ -0,0 +1,47 @@ +/** + * Assignment operators (C-family) + * + * = += -= *= /= %= |= &= ^= >>= <<= + * Note: **= is in pow.js, >>>= ||= &&= ??= are JS-specific (in assignment-js.js) + */ +import { binary, operator, compile } from '../../parse.js'; + +const ASSIGN = 20; + +// Base assignment +binary('=', ASSIGN, true); + +// Compound arithmetic +binary('+=', ASSIGN, true); +binary('-=', ASSIGN, true); +binary('*=', ASSIGN, true); +binary('/=', ASSIGN, true); +binary('%=', ASSIGN, true); + +// Compound bitwise +binary('|=', ASSIGN, true); +binary('&=', ASSIGN, true); +binary('^=', ASSIGN, true); +binary('>>=', ASSIGN, true); +binary('<<=', ASSIGN, true); + +// Simple assign helper for x, a.b, a[b], (x) +const assign = (a, fn, obj, key) => + typeof a === 'string' ? ctx => fn(ctx, a, ctx) : + a[0] === '.' ? (obj = compile(a[1]), key = a[2], ctx => fn(obj(ctx), key, ctx)) : + a[0] === '[]' && a.length === 3 ? (obj = compile(a[1]), key = compile(a[2]), ctx => fn(obj(ctx), key(ctx), ctx)) : + a[0] === '()' && a.length === 2 ? assign(a[1], fn) : // unwrap parens: (x) = 1 + (() => { throw Error('Invalid assignment target') })(); + +// Compile +operator('=', (a, b) => (b = compile(b), assign(a, (o, k, ctx) => o[k] = b(ctx)))); +operator('+=', (a, b) => (b = compile(b), assign(a, (o, k, ctx) => o[k] += b(ctx)))); +operator('-=', (a, b) => (b = compile(b), assign(a, (o, k, ctx) => o[k] -= b(ctx)))); +operator('*=', (a, b) => (b = compile(b), assign(a, (o, k, ctx) => o[k] *= b(ctx)))); +operator('/=', (a, b) => (b = compile(b), assign(a, (o, k, ctx) => o[k] /= b(ctx)))); +operator('%=', (a, b) => (b = compile(b), assign(a, (o, k, ctx) => o[k] %= b(ctx)))); +operator('|=', (a, b) => (b = compile(b), assign(a, (o, k, ctx) => o[k] |= b(ctx)))); +operator('&=', (a, b) => (b = compile(b), assign(a, (o, k, ctx) => o[k] &= b(ctx)))); +operator('^=', (a, b) => (b = compile(b), assign(a, (o, k, ctx) => o[k] ^= b(ctx)))); +operator('>>=', (a, b) => (b = compile(b), assign(a, (o, k, ctx) => o[k] >>= b(ctx)))); +operator('<<=', (a, b) => (b = compile(b), assign(a, (o, k, ctx) => o[k] <<= b(ctx)))); diff --git a/feature/op/bitwise-unsigned.js b/feature/op/bitwise-unsigned.js new file mode 100644 index 00000000..c6eb1a7f --- /dev/null +++ b/feature/op/bitwise-unsigned.js @@ -0,0 +1,17 @@ +/** + * Unsigned right shift operators + * + * >>> >>>= + */ +import { binary, operator, compile } from '../../parse.js'; +import { isLval, prop } from '../access.js'; + +const ASSIGN = 20, SHIFT = 100; +const err = msg => { throw Error(msg) }; + +binary('>>>', SHIFT); +binary('>>>=', ASSIGN, true); + +// Compile +operator('>>>', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) >>> b(ctx))); +operator('>>>=', (a, b) => (isLval(a) || err('Invalid assignment target'), b = compile(b), prop(a, (o, k, ctx) => o[k] >>>= b(ctx)))); diff --git a/feature/op/bitwise.js b/feature/op/bitwise.js new file mode 100644 index 00000000..f9f16666 --- /dev/null +++ b/feature/op/bitwise.js @@ -0,0 +1,29 @@ +/** + * Bitwise operators (C-family) + * + * | & ^ ~ >> << + * Note: >>> is JS-specific (in bitwise-js.js) + */ +import { binary, unary, operator, compile } from '../../parse.js'; + +const OR = 50, XOR = 60, AND = 70, SHIFT = 100, PREFIX = 140; + +// Base operators first (tried last in chain) +binary('|', OR); +binary('&', AND); +binary('^', XOR); + +// Shifts (after < >) +binary('>>', SHIFT); +binary('<<', SHIFT); + +// Unary +unary('~', PREFIX); + +// Compile +operator('~', a => (a = compile(a), ctx => ~a(ctx))); +operator('|', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) | b(ctx))); +operator('&', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) & b(ctx))); +operator('^', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) ^ b(ctx))); +operator('>>', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) >> b(ctx))); +operator('<<', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) << b(ctx))); diff --git a/feature/op/comparison.js b/feature/op/comparison.js new file mode 100644 index 00000000..a9e24203 --- /dev/null +++ b/feature/op/comparison.js @@ -0,0 +1,19 @@ +/** + * Comparison operators + * + * < > <= >= + */ +import { binary, operator, compile } from '../../parse.js'; + +const COMP = 90; + +binary('<', COMP); +binary('>', COMP); +binary('<=', COMP); +binary('>=', COMP); + +// Compile +operator('>', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) > b(ctx))); +operator('<', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) < b(ctx))); +operator('>=', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) >= b(ctx))); +operator('<=', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) <= b(ctx))); diff --git a/feature/op/defer.js b/feature/op/defer.js new file mode 100644 index 00000000..496f0242 --- /dev/null +++ b/feature/op/defer.js @@ -0,0 +1,15 @@ +/** + * Defer operator + * + * defer expr: registers cleanup to run at scope exit + * + * Common in: Go, Swift, Zig + */ +import { unary, operator, compile } from '../../parse.js'; + +const PREFIX = 140; + +unary('defer', PREFIX); + +// Compile +operator('defer', a => (a = compile(a), ctx => { ctx.__deferred__ = ctx.__deferred__ || []; ctx.__deferred__.push(a); })); diff --git a/feature/op/equality.js b/feature/op/equality.js new file mode 100644 index 00000000..580dc2f9 --- /dev/null +++ b/feature/op/equality.js @@ -0,0 +1,16 @@ +/** + * Equality operators (base) + * + * == != + * For === !== see equality-strict.js + */ +import { binary, operator, compile } from '../../parse.js'; + +const EQ = 80; + +binary('==', EQ); +binary('!=', EQ); + +// Compile +operator('==', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) == b(ctx))); +operator('!=', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) != b(ctx))); diff --git a/feature/op/identity.js b/feature/op/identity.js new file mode 100644 index 00000000..6f3aecfe --- /dev/null +++ b/feature/op/identity.js @@ -0,0 +1,15 @@ +/** + * Identity operators + * + * === !== + */ +import { binary, operator, compile } from '../../parse.js'; + +const EQ = 80; + +binary('===', EQ); +binary('!==', EQ); + +// Compile +operator('===', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) === b(ctx))); +operator('!==', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) !== b(ctx))); diff --git a/feature/op/increment.js b/feature/op/increment.js new file mode 100644 index 00000000..fb8f23b2 --- /dev/null +++ b/feature/op/increment.js @@ -0,0 +1,23 @@ +/** + * Increment/decrement operators + * + * ++ -- (prefix and postfix) + */ +import { token, expr, operator, compile } from '../../parse.js'; + +const POSTFIX = 150; + +token('++', POSTFIX, a => a ? ['++', a, null] : ['++', expr(POSTFIX - 1)]); +token('--', POSTFIX, a => a ? ['--', a, null] : ['--', expr(POSTFIX - 1)]); + +// Compile (b=null means postfix, b=undefined means prefix) +// Simple prop helper for increment - handles x, a.b, a[b], (x) +const inc = (a, fn, obj, key) => + typeof a === 'string' ? ctx => fn(ctx, a) : + a[0] === '.' ? (obj = compile(a[1]), key = a[2], ctx => fn(obj(ctx), key)) : + a[0] === '[]' && a.length === 3 ? (obj = compile(a[1]), key = compile(a[2]), ctx => fn(obj(ctx), key(ctx))) : + a[0] === '()' && a.length === 2 ? inc(a[1], fn) : // unwrap parens: (x)++ + (() => { throw Error('Invalid increment target') })(); + +operator('++', (a, b) => inc(a, b === null ? (o, k) => o[k]++ : (o, k) => ++o[k])); +operator('--', (a, b) => inc(a, b === null ? (o, k) => o[k]-- : (o, k) => --o[k])); diff --git a/feature/op/logical.js b/feature/op/logical.js new file mode 100644 index 00000000..b52430e8 --- /dev/null +++ b/feature/op/logical.js @@ -0,0 +1,21 @@ +/** + * Logical operators (base) + * + * ! && || + * For ?? see nullish.js + */ +import { binary, unary, operator, compile } from '../../parse.js'; + +const LOR = 30, LAND = 40, PREFIX = 140; + +// ! must be registered before != and !== +binary('!', PREFIX); +unary('!', PREFIX); + +binary('||', LOR); +binary('&&', LAND); + +// Compile +operator('!', a => (a = compile(a), ctx => !a(ctx))); +operator('||', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) || b(ctx))); +operator('&&', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) && b(ctx))); diff --git a/feature/op/membership.js b/feature/op/membership.js new file mode 100644 index 00000000..e6f37e3b --- /dev/null +++ b/feature/op/membership.js @@ -0,0 +1,17 @@ +/** + * Membership operator + * + * in: key in object + * of: for-of iteration (parsed as binary in for head) + * + * Note: instanceof is in class.js (jessie feature) + */ +import { binary, operator, compile } from '../../parse.js'; + +const COMP = 90; + +binary('in', COMP); +binary('of', COMP); + +// Compile +operator('in', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) in b(ctx))); diff --git a/feature/op/nullish.js b/feature/op/nullish.js new file mode 100644 index 00000000..87152a03 --- /dev/null +++ b/feature/op/nullish.js @@ -0,0 +1,13 @@ +/** + * Nullish coalescing operator (JS-specific) + * + * ?? + */ +import { binary, operator, compile } from '../../parse.js'; + +const LOR = 30; + +binary('??', LOR); + +// Compile +operator('??', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) ?? b(ctx))); diff --git a/feature/op/optional.js b/feature/op/optional.js new file mode 100644 index 00000000..5ef1943d --- /dev/null +++ b/feature/op/optional.js @@ -0,0 +1,61 @@ +/** + * Optional chaining operators + * + * a?.b → optional member access + * a?.[x] → optional computed access + * a?.() → optional call + * + * Common in: JS, TS, Swift, Kotlin, C# + */ +import { token, expr, skip, space, operator, compile } from '../../parse.js'; +import { unsafe } from '../access.js'; + +const ACCESS = 170; + +token('?.', ACCESS, (a, b) => { + if (!a) return; + const cc = space(); + // Optional call: a?.() + if (cc === 40) { skip(); return ['?.()', a, expr(0, 41) || null]; } + // Optional computed: a?.[x] + if (cc === 91) { skip(); return ['?.[]', a, expr(0, 93)]; } + // Optional member: a?.b + b = expr(ACCESS); + return b ? ['?.', a, b] : void 0; +}); + +// Compile +operator('?.', (a, b) => (a = compile(a), unsafe(b) ? () => undefined : ctx => a(ctx)?.[b])); +operator('?.[]', (a, b) => (a = compile(a), b = compile(b), ctx => { const k = b(ctx); return unsafe(k) ? undefined : a(ctx)?.[k]; })); +operator('?.()', (a, b) => { + const args = !b ? () => [] : + b[0] === ',' ? (b = b.slice(1).map(compile), ctx => b.map(arg => arg(ctx))) : + (b = compile(b), ctx => [b(ctx)]); + + // Handle nested optional chain: a?.method?.() or a?.["method"]?.() + if (a[0] === '?.') { + const container = compile(a[1]); + const prop = a[2]; + return unsafe(prop) ? () => undefined : + ctx => { const c = container(ctx); return c?.[prop]?.(...args(ctx)); }; + } + if (a[0] === '?.[]') { + const container = compile(a[1]); + const prop = compile(a[2]); + return ctx => { const c = container(ctx); const p = prop(ctx); return unsafe(p) ? undefined : c?.[p]?.(...args(ctx)); }; + } + // Handle a?.() where a is a.method or a[method] - need to bind this + if (a[0] === '.') { + const obj = compile(a[1]); + const prop = a[2]; + return unsafe(prop) ? () => undefined : + ctx => { const o = obj(ctx); return o?.[prop]?.(...args(ctx)); }; + } + if (a[0] === '[]' && a.length === 3) { + const obj = compile(a[1]); + const prop = compile(a[2]); + return ctx => { const o = obj(ctx); const p = prop(ctx); return unsafe(p) ? undefined : o?.[p]?.(...args(ctx)); }; + } + const fn = compile(a); + return ctx => fn(ctx)?.(...args(ctx)); +}); diff --git a/feature/op/pow.js b/feature/op/pow.js new file mode 100644 index 00000000..25be0549 --- /dev/null +++ b/feature/op/pow.js @@ -0,0 +1,19 @@ +/** + * Exponentiation operator + * + * ** **= + * + * ES2016+, not in classic JS/C + */ +import { binary, operator, compile } from '../../parse.js'; +import { isLval, prop } from '../access.js'; + +const EXP = 130, ASSIGN = 20; + +binary('**', EXP, true); +binary('**=', ASSIGN, true); + +// Compile +operator('**', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) ** b(ctx))); +const err = msg => { throw Error(msg) }; +operator('**=', (a, b) => (isLval(a) || err('Invalid assignment target'), b = compile(b), prop(a, (obj, path, ctx) => obj[path] **= b(ctx)))); diff --git a/feature/op/range.js b/feature/op/range.js new file mode 100644 index 00000000..cc2c3f03 --- /dev/null +++ b/feature/op/range.js @@ -0,0 +1,26 @@ +/** + * Range operators + * + * .. (inclusive range): 1..5 → [1,2,3,4,5] + * ..< (exclusive range): 1..<5 → [1,2,3,4] + * + * Common in: Swift, Kotlin, Rust, Ruby + */ +import { binary, operator, compile } from '../../parse.js'; + +const COMP = 90; + +binary('..', COMP); +binary('..<', COMP); + +// Compile +operator('..', (a, b) => (a = compile(a), b = compile(b), ctx => { + const start = a(ctx), end = b(ctx), arr = []; + for (let i = start; i <= end; i++) arr.push(i); + return arr; +})); +operator('..<', (a, b) => (a = compile(a), b = compile(b), ctx => { + const start = a(ctx), end = b(ctx), arr = []; + for (let i = start; i < end; i++) arr.push(i); + return arr; +})); diff --git a/feature/op/spread.js b/feature/op/spread.js new file mode 100644 index 00000000..ad39827a --- /dev/null +++ b/feature/op/spread.js @@ -0,0 +1,15 @@ +/** + * Spread/rest operator + * + * ...x → spread in arrays/calls, rest in params + * + * Common in: JS, TS, Python (*args), Ruby (*splat) + */ +import { unary, operator, compile } from '../../parse.js'; + +const PREFIX = 140; + +unary('...', PREFIX); + +// Compile (for arrays/objects spread) +operator('...', a => (a = compile(a), ctx => Object.entries(a(ctx)))); diff --git a/feature/op/ternary.js b/feature/op/ternary.js new file mode 100644 index 00000000..61c1a88a --- /dev/null +++ b/feature/op/ternary.js @@ -0,0 +1,15 @@ +/** + * Ternary conditional operator + * + * a ? b : c → conditional expression + * + * Common in: C, JS, Java, PHP, etc. + */ +import { token, expr, next, operator, compile } from '../../parse.js'; + +const ASSIGN = 20; + +token('?', ASSIGN, (a, b, c) => a && (b = expr(ASSIGN - 1)) && next(c => c === 58) && (c = expr(ASSIGN - 1), ['?', a, b, c])); + +// Compile +operator('?', (a, b, c) => (a = compile(a), b = compile(b), c = compile(c), ctx => a(ctx) ? b(ctx) : c(ctx))); diff --git a/feature/op/type.js b/feature/op/type.js new file mode 100644 index 00000000..10f2717c --- /dev/null +++ b/feature/op/type.js @@ -0,0 +1,18 @@ +/** + * Type operators + * + * as: type cast/assertion (identity in JS) + * is: type check (instanceof in JS) + * + * Common in: TypeScript, Kotlin, Swift, C# + */ +import { binary, operator, compile } from '../../parse.js'; + +const COMP = 90; + +binary('as', COMP); +binary('is', COMP); + +// Compile (identity in JS) +operator('as', (a, b) => (a = compile(a), ctx => a(ctx))); +operator('is', (a, b) => (a = compile(a), b = compile(b), ctx => a(ctx) instanceof b(ctx))); diff --git a/feature/op/unary.js b/feature/op/unary.js new file mode 100644 index 00000000..3c9565af --- /dev/null +++ b/feature/op/unary.js @@ -0,0 +1,41 @@ +/** + * Unary keyword operators + * + * typeof x → type string + * void x → undefined + * delete x → remove property + * new X() → construct instance + * + * JS-specific keywords + */ +import { unary, operator, compile } from '../../parse.js'; + +const PREFIX = 140; + +unary('typeof', PREFIX); +unary('void', PREFIX); +unary('delete', PREFIX); +unary('new', PREFIX); + +// Compile +operator('typeof', a => (a = compile(a), ctx => typeof a(ctx))); +operator('void', a => (a = compile(a), ctx => (a(ctx), undefined))); +operator('delete', a => { + if (a[0] === '.') { + const obj = compile(a[1]), key = a[2]; + return ctx => delete obj(ctx)[key]; + } + if (a[0] === '[]') { + const obj = compile(a[1]), key = compile(a[2]); + return ctx => delete obj(ctx)[key(ctx)]; + } + return () => true; +}); +operator('new', (call) => { + const target = compile(call?.[0] === '()' ? call[1] : call); + const args = call?.[0] === '()' ? call[2] : null; + const argList = !args ? () => [] : + args[0] === ',' ? (a => ctx => a.map(f => f(ctx)))(args.slice(1).map(compile)) : + (a => ctx => [a(ctx)])(compile(args)); + return ctx => new (target(ctx))(...argList(ctx)); +}); diff --git a/feature/optional.js b/feature/optional.js deleted file mode 100644 index 6731a682..00000000 --- a/feature/optional.js +++ /dev/null @@ -1,23 +0,0 @@ -import { token, expr } from '../src/parse.js' -import { operator, compile } from '../src/compile.js' -import { PREC_ACCESS, unsafe } from '../src/const.js' - -// a?.[, a?.( - postfix operator -token('?.', PREC_ACCESS, a => a && ['?.', a]) -operator('?.', a => (a = compile(a), ctx => a(ctx) || (() => { }))) - -// a?.b, a?.() - optional chain operator -token('?.', PREC_ACCESS, (a, b) => a && (b = expr(PREC_ACCESS), !b?.map) && ['?.', a, b]) -operator('?.', (a, b) => b && (a = compile(a), unsafe(b) ? () => undefined : ctx => a(ctx)?.[b])) - -// a?.x() - keep context, but watch out a?.() -operator('()', (a, b, container, args, path, optional) => b !== undefined && (a[0] === '?.') && (a[2] || Array.isArray(a[1])) && ( - args = !b ? () => [] : - b[0] === ',' ? (b = b.slice(1).map(compile), ctx => b.map(a => a(ctx))) : - (b = compile(b), ctx => [b(ctx)]), - !a[2] && (optional = true, a = a[1]), - a[0] === '[]' && a.length === 3 ? (path = compile(a[2])) : (path = () => a[2]), - container = compile(a[1]), optional ? - ctx => { const p = path(ctx); return unsafe(p) ? undefined : container(ctx)?.[p]?.(...args(ctx)) } : - ctx => { const p = path(ctx); return unsafe(p) ? undefined : container(ctx)?.[p](...args(ctx)) } -)) diff --git a/feature/pow.js b/feature/pow.js deleted file mode 100644 index eb3a2295..00000000 --- a/feature/pow.js +++ /dev/null @@ -1,5 +0,0 @@ -import { binary } from "../src/parse.js"; -import { compile, operator } from "../src/compile.js"; -import { PREC_EXP } from "../src/const.js"; - -binary('**', PREC_EXP, true), operator('**', (a, b) => b && (a = compile(a), b = compile(b), ctx => a(ctx) ** b(ctx))) diff --git a/feature/prop.js b/feature/prop.js new file mode 100644 index 00000000..8c5587fd --- /dev/null +++ b/feature/prop.js @@ -0,0 +1,34 @@ +/** + * Minimal property access: a.b, a[b], f() + * For array literals, private fields, see member.js + */ +import { access, binary, group, operator, compile } from '../parse.js'; + +const ACCESS = 170; + +// a[b] - computed member access only (no array literal support) +access('[]', ACCESS); + +// a.b - dot member access +binary('.', ACCESS); + +// (a) - grouping only (no sequences) +group('()', ACCESS); + +// a(b,c,d), a() - function calls +access('()', ACCESS); + +// Compile +const err = msg => { throw Error(msg) }; +operator('[]', (a, b) => (b == null && err('Missing index'), a = compile(a), b = compile(b), ctx => a(ctx)[b(ctx)])); +operator('.', (a, b) => (a = compile(a), b = !b[0] ? b[1] : b, ctx => a(ctx)[b])); +operator('()', (a, b) => { + // Group: (expr) - no second argument means grouping, not call + if (b === undefined) return a == null ? err('Empty ()') : compile(a); + // Function call: a(b,c) + const args = !b ? () => [] : + b[0] === ',' ? (b = b.slice(1).map(compile), ctx => b.map(arg => arg(ctx))) : + (b = compile(b), ctx => [b(ctx)]); + a = compile(a); + return ctx => a(ctx)(...args(ctx)); +}); diff --git a/feature/regex.js b/feature/regex.js index cd65cdb3..287f99b6 100644 --- a/feature/regex.js +++ b/feature/regex.js @@ -1,55 +1,31 @@ /** * Regex literals: /pattern/flags - * + * * AST: * /abc/gi → [, /abc/gi] - * + * * Note: Disambiguates from division by context: - * - `/` after value = division + * - `/` after value = division (falls through to prev) * - `/` at start or after operator = regex */ -import * as P from '../src/parse.js' +import { token, skip, err, next, idx, cur } from '../parse.js'; -const { lookup, skip, err, next } = P -const SLASH = 47, BSLASH = 92 +const PREFIX = 140, SLASH = 47, BSLASH = 92; -const regexFlags = c => c === 103 || c === 105 || c === 109 || c === 115 || c === 117 || c === 121 // g i m s u y +const regexChar = c => c === BSLASH ? 2 : c && c !== SLASH; // \x = 2 chars, else 1 until / +const regexFlag = c => c === 103 || c === 105 || c === 109 || c === 115 || c === 117 || c === 121; // g i m s u y -// Store original division handler -const divHandler = lookup[SLASH] +token('/', PREFIX, a => { + if (a) return; // has left operand = division, fall through -// Override / to detect regex vs division -lookup[SLASH] = (a, prec) => { - // If there's a left operand, it's division - if (a) return divHandler?.(a, prec) - - // No left operand = regex literal - skip() // consume opening / - - let pattern = '', c - while ((c = P.cur.charCodeAt(P.idx)) && c !== SLASH) { - if (c === BSLASH) { - // Escape sequence - include both chars - pattern += P.cur[P.idx] - skip() - if (!P.cur[P.idx]) err('Unterminated regex') - pattern += P.cur[P.idx] - skip() - } else { - pattern += P.cur[P.idx] - skip() - } - } - - if (!P.cur[P.idx]) err('Unterminated regex') - skip() // consume closing / - - // Parse flags - const flags = next(regexFlags) - - try { - return [, new RegExp(pattern, flags)] - } catch (e) { - err('Invalid regex: ' + e.message) - } -} + // Invalid regex start (quantifiers) or /= - fall through + const first = cur.charCodeAt(idx); + if (first === SLASH || first === 42 || first === 43 || first === 63 || first === 61) return; + + const pattern = next(regexChar); + cur.charCodeAt(idx) === SLASH || err('Unterminated regex'); + skip(); // consume closing / + + try { return [, new RegExp(pattern, next(regexFlag))]; } + catch (e) { err('Invalid regex: ' + e.message); } +}); diff --git a/feature/seq.js b/feature/seq.js new file mode 100644 index 00000000..42e5c328 --- /dev/null +++ b/feature/seq.js @@ -0,0 +1,21 @@ +/** + * Sequence operators (C-family) + * + * , ; — returns last evaluated value + */ +import { nary, operator, compile } from '../parse.js'; + +const STATEMENT = 5, SEQ = 10; + +// Sequences +nary(',', SEQ); +nary(';', STATEMENT, true); // right-assoc to allow same-prec statements + +// Compile - returns last evaluated value +const seq = (...args) => (args = args.map(compile), ctx => { + let r; + for (const arg of args) r = arg(ctx); + return r; +}); +operator(',', seq); +operator(';', seq); diff --git a/feature/shift.js b/feature/shift.js deleted file mode 100644 index c32a076f..00000000 --- a/feature/shift.js +++ /dev/null @@ -1,12 +0,0 @@ -import { PREC_OR, PREC_AND, PREC_SHIFT, PREC_XOR, PREC_PREFIX, PREC_ASSIGN } from "../src/const.js" -import { unary, binary } from "../src/parse.js" -import { operator, compile } from "../src/compile.js" - - -binary('>>', PREC_SHIFT), operator('>>', (a, b) => b && (a = compile(a), b = compile(b), ctx => a(ctx) >> b(ctx))) -binary('<<', PREC_SHIFT), operator('<<', (a, b) => b && (a = compile(a), b = compile(b), ctx => a(ctx) << b(ctx))) - -binary('>>=', PREC_ASSIGN, true) -operator('>>=', (a, b) => (b = compile(b), prop(a, (obj, path, ctx) => (obj[path] >>= b(ctx))))) -binary('<<=', PREC_ASSIGN, true) -operator('<<=', (a, b) => (b = compile(b), prop(a, (obj, path, ctx) => (obj[path] <<= b(ctx))))) diff --git a/feature/spread.js b/feature/spread.js deleted file mode 100644 index 70c9d9c9..00000000 --- a/feature/spread.js +++ /dev/null @@ -1,6 +0,0 @@ -import { unary } from "../src/parse.js" -import { PREC_PREFIX } from "../src/const.js" -import { operator, compile } from "../src/compile.js" - -unary('...', PREC_PREFIX) -operator('...', (a) => (a = compile(a), ctx => Object.entries(a(ctx)))) diff --git a/feature/string.js b/feature/string.js index b59fc06e..a9a7307c 100644 --- a/feature/string.js +++ b/feature/string.js @@ -1,20 +1,27 @@ -import { skip, err, next, cur, idx, lookup } from '../src/parse.js' -import { DQUOTE, QUOTE, BSLASH } from '../src/const.js' +/** + * Strings with escape sequences + * + * Configurable via parse.string: { '"': true } or { '"': true, "'": true } + */ +import { parse, lookup, next, err, skip, idx, cur } from '../parse.js'; -const escape = { n: '\n', r: '\r', t: '\t', b: '\b', f: '\f', v: '\v' }, - string = q => (qc, prec, str = '') => { - qc && err('Unexpected string') // must not follow another token - skip() - // while (c = cur.charCodeAt(idx), c - q) { - // if (c === BSLASH) skip(), c = cur[idx], skip(), str += escape[c] || c - // else str += cur[idx], skip() - // } - next(c => c - q && (c === BSLASH ? (str += escape[cur[idx+1]] || cur[idx+1], 2 ) : (str += cur[idx], 1))) - skip() || err('Bad string') - return [, str] - } +const BSLASH = 92, DQUOTE = 34, SQUOTE = 39; +const esc = { n: '\n', r: '\r', t: '\t', b: '\b', f: '\f', v: '\v' }; +// Parse string with given quote char code +const parseString = q => (a, _, s = '') => { + if (a || !parse.string?.[String.fromCharCode(q)]) return; + skip(); + next(c => c - q && (c === BSLASH ? (s += esc[cur[idx + 1]] || cur[idx + 1], 2) : (s += cur[idx], 1))); + cur[idx] === String.fromCharCode(q) ? skip() : err('Bad string'); + return [, s]; +}; -// "' with / -lookup[DQUOTE] = string(DQUOTE) -lookup[QUOTE] = string(QUOTE) +// Register both quote chars (enabled via parse.string config) +lookup[DQUOTE] = parseString(DQUOTE); +lookup[SQUOTE] = parseString(SQUOTE); + +// Default: double quotes only +parse.string = { '"': true }; + +export { esc }; diff --git a/feature/switch.js b/feature/switch.js new file mode 100644 index 00000000..adf4a0f2 --- /dev/null +++ b/feature/switch.js @@ -0,0 +1,48 @@ +// Switch/case/default +// AST: ['switch', val, [';', ['case', test], stmts..., ['default'], stmts...]] +import { expr, skip, space, parens, operator, compile } from '../parse.js'; +import { keyword, block } from './block.js'; +import { BREAK } from './loop.js'; + +const STATEMENT = 5, ASSIGN = 20, COLON = 58; + +keyword('switch', STATEMENT + 1, () => (space(), ['switch', parens(), block()])); +keyword('case', STATEMENT + 1, () => (space(), (c => (space() === COLON && skip(), ['case', c]))(expr(ASSIGN)))); +keyword('default', STATEMENT + 1, () => (space() === COLON && skip(), ['default'])); + +// Compile +operator('switch', (val, cases) => { + val = compile(val); + // Parse cases body: [';', ['case', test], stmts..., ['default'], stmts...] + if (!cases) return ctx => val(ctx); + const parsed = []; + const items = cases[0] === ';' ? cases.slice(1) : [cases]; + let current = null; + for (const item of items) { + if (Array.isArray(item) && (item[0] === 'case' || item[0] === 'default')) { + if (current) parsed.push(current); + current = [item[0] === 'case' ? compile(item[1]) : null, []]; + } else if (current) { + current[1].push(compile(item)); + } + } + if (current) parsed.push(current); + + return ctx => { + const v = val(ctx); + let matched = false, result; + for (const [test, stmts] of parsed) { + if (matched || test === null || test(ctx) === v) { + matched = true; + for (const stmt of stmts) { + try { result = stmt(ctx); } + catch (e) { + if (e?.type === BREAK) return e.value !== undefined ? e.value : result; + throw e; + } + } + } + } + return result; + }; +}); diff --git a/feature/template.js b/feature/template.js index ee9d402b..5267397c 100644 --- a/feature/template.js +++ b/feature/template.js @@ -1,62 +1,39 @@ /** - * Template string interpolation: `a ${expr} b` - * - * AST: - * `a ${x} b` → ['`', [,'a '], 'x', [,' b']] + * Template literals: `a ${x} b` → ['`', [,'a '], 'x', [,' b']] + * Tagged templates: tag`...` → ['``', 'tag', ...] */ -import * as P from '../src/parse.js' -import { operator, compile } from '../src/compile.js' +import { parse, skip, err, expr, lookup, cur, idx, operator, compile } from '../parse.js'; -const { lookup, skip, err, next, expr } = P -const BACKTICK = 96, DOLLAR = 36, OBRACE = 123, CBRACE = 125, BSLASH = 92 +const ACCESS = 170, BACKTICK = 96, DOLLAR = 36, OBRACE = 123, BSLASH = 92; +const esc = { n: '\n', r: '\r', t: '\t', b: '\b', f: '\f', v: '\v' }; -const escape = { n: '\n', r: '\r', t: '\t', b: '\b', f: '\f', v: '\v' } +// Parse template body after opening ` +const parseBody = () => { + const parts = []; + for (let s = '', c; (c = cur.charCodeAt(idx)) !== BACKTICK; ) + !c ? err('Unterminated template') : + c === BSLASH ? (skip(), s += esc[cur[idx]] || cur[idx], skip()) : + c === DOLLAR && cur.charCodeAt(idx + 1) === OBRACE ? (s && parts.push([, s]), s = '', skip(2), parts.push(expr(0, 125))) : + (s += cur[idx], skip(), c = cur.charCodeAt(idx), c === BACKTICK && s && parts.push([, s])); + return skip(), parts; +}; -// Parse template literal -lookup[BACKTICK] = a => { - a && err('Unexpected template') - skip() // consume opening ` - - const parts = [] - let str = '' - - while (P.cur.charCodeAt(P.idx) !== BACKTICK) { - const c = P.cur.charCodeAt(P.idx) - if (!c) err('Unterminated template') - - // Escape sequence - if (c === BSLASH) { - skip() - const ec = P.cur[P.idx] - str += escape[ec] || ec - skip() - } - // Interpolation ${...} - else if (c === DOLLAR && P.cur.charCodeAt(P.idx + 1) === OBRACE) { - if (str) parts.push([, str]) - str = '' - skip(); skip() // consume ${ - parts.push(expr(0, CBRACE)) - } - // Regular character - else { - str += P.cur[P.idx] - skip() - } - } - - skip() // consume closing ` - if (str) parts.push([, str]) - - // Optimize: if no interpolations, return plain string - if (parts.length === 0) return [, ''] - if (parts.length === 1 && parts[0][0] === undefined) return parts[0] - - return ['`', ...parts] -} +const prev = lookup[BACKTICK]; +// Tagged templates: decline when ASI with newline (return undefined to let ASI handle) +lookup[BACKTICK] = (a, prec) => + a && prec < ACCESS ? (parse.asi && parse.newline ? void 0 : (skip(), ['``', a, ...parseBody()])) : // tagged + !a ? (skip(), (p => p.length < 2 && p[0]?.[0] === undefined ? p[0] || [,''] : ['`', ...p])(parseBody())) : // plain + prev?.(a, prec); -// Compile template: concatenate parts -operator('`', (...parts) => { - parts = parts.map(p => compile(p)) - return ctx => parts.map(p => p(ctx)).join('') -}) +// Compile +operator('`', (...parts) => (parts = parts.map(compile), ctx => parts.map(p => p(ctx)).join(''))); +operator('``', (tag, ...parts) => { + tag = compile(tag); + const strings = [], exprs = []; + for (const p of parts) { + if (Array.isArray(p) && p[0] === undefined) strings.push(p[1]); + else exprs.push(compile(p)); + } + const strs = Object.assign([...strings], { raw: strings }); + return ctx => tag(ctx)(strs, ...exprs.map(e => e(ctx))); +}); diff --git a/feature/ternary.js b/feature/ternary.js deleted file mode 100644 index 88770811..00000000 --- a/feature/ternary.js +++ /dev/null @@ -1,10 +0,0 @@ -import { token, expr, next } from '../src/parse.js' -import { operator, compile } from '../src/compile.js' -import { PREC_ASSIGN, COLON } from '../src/const.js' - -// ?: -// token('?', PREC_ASSIGN, (a, b, c) => a && (b = expr(PREC_ASSIGN - 1, COLON)) && (c = expr(PREC_ASSIGN - 1), ['?', a, b, c])) -// ALT: not throwing -token('?', PREC_ASSIGN, (a, b, c) => a && (b = expr(PREC_ASSIGN - 1)) && next(c => c === COLON) && (c = expr(PREC_ASSIGN - 1), ['?', a, b, c])) - -operator('?', (a, b, c) => (a = compile(a), b = compile(b), c = compile(c), ctx => a(ctx) ? b(ctx) : c(ctx))) diff --git a/feature/try.js b/feature/try.js new file mode 100644 index 00000000..83057a28 --- /dev/null +++ b/feature/try.js @@ -0,0 +1,57 @@ +// try/catch/finally/throw statements +// AST: ['catch', ['try', body], param, catchBody] or ['finally', inner, body] +import { space, parse, parens, expr, operator, compile } from '../parse.js'; +import { keyword, infix, block } from './block.js'; +import { BREAK, CONTINUE, RETURN } from './loop.js'; + +const STATEMENT = 5; + +keyword('try', STATEMENT + 1, () => ['try', block()]); +infix('catch', STATEMENT + 1, a => (space(), ['catch', a, parens(), block()])); +infix('finally', STATEMENT + 1, a => ['finally', a, block()]); + +keyword('throw', STATEMENT + 1, () => { + parse.asi && (parse.newline = false); + space(); + if (parse.newline) throw SyntaxError('Unexpected newline after throw'); + return ['throw', expr(STATEMENT)]; +}); + +// Compile +operator('try', tryBody => { + tryBody = tryBody ? compile(tryBody) : null; + return ctx => tryBody?.(ctx); +}); + +operator('catch', (tryNode, catchName, catchBody) => { + const tryBody = tryNode?.[1] ? compile(tryNode[1]) : null; + catchBody = catchBody ? compile(catchBody) : null; + return ctx => { + let result; + try { + result = tryBody?.(ctx); + } catch (e) { + if (e?.type === BREAK || e?.type === CONTINUE || e?.type === RETURN) throw e; + if (catchName !== null && catchBody) { + const had = catchName in ctx, orig = ctx[catchName]; + ctx[catchName] = e; + try { result = catchBody(ctx); } + finally { had ? ctx[catchName] = orig : delete ctx[catchName]; } + } else if (catchName === null) throw e; + } + return result; + }; +}); + +operator('finally', (inner, finallyBody) => { + inner = inner ? compile(inner) : null; + finallyBody = finallyBody ? compile(finallyBody) : null; + return ctx => { + let result; + try { result = inner?.(ctx); } + finally { finallyBody?.(ctx); } + return result; + }; +}); + +operator('throw', val => (val = compile(val), ctx => { throw val(ctx); })); diff --git a/feature/unit.js b/feature/unit.js index 819763d4..533b45b8 100644 --- a/feature/unit.js +++ b/feature/unit.js @@ -5,55 +5,31 @@ * 5px → ['px', [,5]] * 2.5s → ['s', [,2.5]] * - * Units are postfix operators — idiomatic to subscript's design. - * Inspired by piezo: https://github.com/dy/piezo - * * Usage: * import { unit } from 'subscript/feature/unit.js' * unit('px', 'em', 'rem', 's', 'ms') */ -import * as P from '../src/parse.js' -import { operator, compile } from '../src/compile.js' - -const { lookup, next, parse } = P +import { lookup, next, parse, idx, seek, operator, compile } from '../parse.js'; -// Unit registry -const units = new Set +const units = {}; -// Register units with default evaluator export const unit = (...names) => names.forEach(name => { - units.add(name) - // Default: return { value, unit } object - operator(name, val => (val = compile(val), ctx => ({ value: val(ctx), unit: name }))) -}) + units[name] = 1; + operator(name, val => (val = compile(val), ctx => ({ value: val(ctx), unit: name }))); +}); // Wrap number handler to check for unit suffix -const wrapHandler = (charCode) => { - const original = lookup[charCode] - if (!original) return - - lookup[charCode] = (a, prec) => { - const result = original(a, prec) - if (!result) return result - - // Only numeric literals (not identifiers) - if (!Array.isArray(result) || result[0] !== undefined) return result - - // Try to consume unit suffix - const startIdx = P.idx - const u = next(c => parse.id(c) && !(c >= 48 && c <= 57)) - - if (u && units.has(u)) return [u, result] - - // Not a unit - backtrack - if (u) P.idx = startIdx - - return result - } -} - -// Wrap all number entry points (0-9 and .) -// PERIOD, _0, _9 are from src/const.js -import { PERIOD, _0, _9 } from '../src/const.js' -wrapHandler(PERIOD) -for (let i = _0; i <= _9; i++) wrapHandler(i) +const wrapNum = cc => { + const orig = lookup[cc]; + if (!orig) return; + lookup[cc] = (a, prec) => { + const r = orig(a, prec); + if (!r || r[0] !== undefined) return r; + const start = idx, u = next(c => parse.id(c) && !(c >= 48 && c <= 57)); + return u && units[u] ? [u, r] : (u && seek(start), r); + }; +}; + +// Wrap digit and period handlers +for (let i = 48; i <= 57; i++) wrapNum(i); +wrapNum(46); diff --git a/feature/var.js b/feature/var.js index 90607bb5..769571f0 100644 --- a/feature/var.js +++ b/feature/var.js @@ -1,49 +1,59 @@ /** - * Variable declarations: let, const - * + * Variable declarations: let, const, var + * * AST: - * let x → ['let', 'x'] - * let x = 1 → ['let', 'x', val] - * const x = 1 → ['const', 'x', val] + * let x = 1 → ['let', ['=', 'x', 1]] + * let x = 1, y = 2 → ['let', ['=', 'x', 1], ['=', 'y', 2]] + * const {a} = x → ['const', ['=', ['{}', 'a'], 'x']] + * for (let x in o) → ['for', ['in', ['let', 'x'], 'o'], body] + * var x → ['var', 'x'] (acts as assignment target) */ -import * as P from '../src/parse.js' -import { operator, compile } from '../src/compile.js' -import { PREC_STATEMENT } from '../src/const.js' +import { token, expr, space, operator, compile } from '../parse.js'; +import { keyword } from './block.js'; +import { destructure } from './destruct.js'; -const { token, expr, skip, space, err, parse, next } = P +const STATEMENT = 5, SEQ = 10, ASSIGN = 20; -// let x [= val] -token('let', PREC_STATEMENT, a => { - if (a) return - space() - const name = next(parse.id) - if (!name) err('Expected identifier') - space() - if (P.cur.charCodeAt(P.idx) === 61 && P.cur.charCodeAt(P.idx + 1) !== 61) { - skip() - return ['let', name, expr(PREC_STATEMENT)] - } - return ['let', name] -}) +// let/const: expr(SEQ-1) consumes assignment, stops before comma +// For for-in/of, return ['in/of', ['let', x], iterable] not ['let', ['in', x, it]] +// For comma, return ['let', decl1, decl2, ...] not ['let', [',', ...]] +const decl = keyword => { + let node = expr(SEQ - 1); + // for (let x in obj) - restructure so for-loop sees in/of at top + if (node?.[0] === 'in' || node?.[0] === 'of') + return [node[0], [keyword, node[1]], node[2]]; + // let x = 1, y = 2 - flatten comma into nary let + if (node?.[0] === ',') + return [keyword, ...node.slice(1)]; + return [keyword, node]; +}; -operator('let', (name, val) => { - val = val !== undefined ? compile(val) : null - return ctx => { ctx[name] = val ? val(ctx) : undefined } -}) +token('let', STATEMENT + 1, a => !a && decl('let')); +token('const', STATEMENT + 1, a => !a && decl('const')); -// const x = val -token('const', PREC_STATEMENT, a => { - if (a) return - space() - const name = next(parse.id) - if (!name) err('Expected identifier') - space() - P.cur.charCodeAt(P.idx) === 61 && P.cur.charCodeAt(P.idx + 1) !== 61 || err('Expected =') - skip() - return ['const', name, expr(PREC_STATEMENT)] -}) +// var: just declares identifier, assignment happens separately +// var x = 5 → ['=', ['var', 'x'], 5] +keyword('var', STATEMENT, () => (space(), ['var', expr(ASSIGN)])); -operator('const', (name, val) => { - val = compile(val) - return ctx => { ctx[name] = val(ctx) } -}) +// Compile +const varOp = (...decls) => { + decls = decls.map(d => { + // Just identifier: let x + if (typeof d === 'string') return ctx => { ctx[d] = undefined; }; + // Assignment: let x = 1 + if (d[0] === '=') { + const [, pattern, val] = d; + const v = compile(val); + if (typeof pattern === 'string') return ctx => { ctx[pattern] = v(ctx); }; + return ctx => destructure(pattern, v(ctx), ctx); + } + return compile(d); + }); + return ctx => { for (const d of decls) d(ctx); }; +}; +operator('let', varOp); +operator('const', varOp); +// var just declares the variable (assignment handled by = operator) +operator('var', name => (typeof name === 'string' + ? ctx => { ctx[name] = undefined; } + : () => {})); diff --git a/repl.html b/index.html similarity index 71% rename from repl.html rename to index.html index b94cc035..51fe1188 100644 --- a/repl.html +++ b/index.html @@ -34,10 +34,10 @@ .toggle-sidebar svg { width: 0.875rem; height: 0.875rem; } /* Code area - editor only */ -.code-area { display: flex; gap: 1rem; align-items: flex-start; } +.code-area { display: flex; gap: 1rem; align-items: flex-start; margin-top: 1rem; } /* Editor with line numbers */ -.editor { display: flex; position: relative; overflow: auto; max-height: 50vh; min-width: 200px; flex: 1; max-width: 80ch; } +.editor { display: flex; position: relative; overflow: auto; max-height: 50vh; min-width: 200px; flex: 1; } .line-nums { padding: 0.5rem; color: var(--ln); font-size: 0.875rem; line-height: 1.6; text-align: right; user-select: none; white-space: pre; min-width: 1.75rem; @@ -57,7 +57,7 @@ .info-bar .error { color: var(--err); flex: 1; } /* Output section - tabs */ -.output-section { display: flex; flex-direction: column; max-width: 80ch; } +.output-section { display: flex; flex-direction: column; } .output-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); } .output-tab { background: none; border: none; color: var(--muted); padding: 0.375rem 0.75rem; font: 0.875rem inherit; cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; } .output-tab:hover { color: var(--fg); } @@ -260,7 +260,8 @@