Skip to content

Commit 929847f

Browse files
committed
Update spec to match reality
1 parent 9bc39c9 commit 929847f

File tree

3 files changed

+201
-31
lines changed

3 files changed

+201
-31
lines changed

spec.md

Lines changed: 76 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -183,13 +183,15 @@ Template literals contain string parts (as literals) interleaved with expression
183183
['if', cond, then] if (cond) then
184184
['if', cond, then, else] if (cond) then else alt
185185
['while', cond, body] while (cond) body
186-
['for', init, cond, step, body] for (init; cond; step) body
187-
['for-of', 'x', iter, body] for (x of iter) body
188-
['for-of', 'x', iter, body, 'const'] for (const x of iter) body
189-
['block', body] { body }
186+
['for', head, body] for (...) body
187+
['for', ['of', 'x', iter], body] for (x of iter) body
188+
['for', ['of', ['const', 'x'], iter], body] for (const x of iter) body
189+
['for', ['in', 'x', obj], body] for (x in obj) body
190+
['for', [';', init, cond, step], body] for (init; cond; step) body
191+
['{}', body] { body } (block)
190192
['let', 'x'] let x
191-
['let', 'x', val] let x = val
192-
['const', 'x', val] const x = val
193+
['let', ['=', 'x', val]] let x = val
194+
['const', ['=', 'x', val]] const x = val
193195
['break'] break
194196
['continue'] continue
195197
['return'] return
@@ -198,16 +200,16 @@ Template literals contain string parts (as literals) interleaved with expression
198200

199201
### Exceptions (feature/throw.js, feature/try.js)
200202
```
201-
['throw', val] throw val
202-
['try', body, 'e', catch] try { body } catch (e) { catch }
203-
['try', body, null, null, finally] try { body } finally { finally }
204-
['try', body, 'e', catch, finally] try { body } catch (e) { catch } finally { finally }
203+
['throw', val] throw val
204+
['catch', ['try', body], 'e', handler] try { body } catch (e) { handler }
205+
['finally', ['try', body], handler] try { body } finally { handler }
206+
['finally', ['catch', ...], handler] try {...} catch {...} finally {...}
205207
```
206208

207209
### Function Declarations (feature/function.js)
208210
```
209-
['function', 'name', ['a', 'b'], body] function name(a, b) { body }
210-
['function', null, ['x'], body] function(x) { body }
211+
['function', 'name', [',', 'a', 'b'], body] function name(a, b) { body }
212+
['function', '', 'x', body] function(x) { body }
211213
```
212214

213215

@@ -218,15 +220,25 @@ Template literals contain string parts (as literals) interleaved with expression
218220

219221
Following McCarthy's [original Lisp](http://www-formal.stanford.edu/jmc/recursive.pdf) (1960), the operator occupies position zero. This enables uniform traversal: every node is processed identically regardless of arity.
220222

221-
### 2. Strings are References
223+
### 2. Strings are References or Tokens
222224

223-
An unwrapped string is always an identifier — a name to resolve from context. This prevents confusion between the *name* `"x"` and the *string value* `"x"`.
225+
An unwrapped string in operand position is interpreted by the operator:
226+
227+
- **Identifier**: resolved from context (most operators)
228+
- **Token**: used directly as name/data (operator-specific)
224229

225230
```
226-
"x" → resolve x from context
227-
[, "x"] → the literal string "x"
231+
"x" → resolve x from context
232+
[, "x"] → the literal string "x"
233+
['.', a, 'b'] → 'b' is property name (token)
234+
['//', 'abc', 'g']→ pattern/flags are tokens
235+
['n', '123'] → bigint digits are token
236+
['px', '100'] → unit value is token
237+
['function', 'f', ...] → 'f' is function name (token)
228238
```
229239

240+
Operators that use tokens: `.`, `?.`, `//`, `n`, `px`/units, `=>`, `function`, `let`, `const`, `try` (catch param).
241+
230242
### 3. Empty Slot for Literals
231243

232244
The pattern `[, value]` uses JavaScript's elision syntax. The empty first position signals: *this is data, not code*. No operator means no operation.
@@ -275,41 +287,74 @@ Custom operators are simply strings in position zero.
275287

276288
Literals (`[, value]`) hold JSON primitives only: number, string, boolean, null.
277289

278-
Non-JSON values use **constructor form** — an operator that constructs the value:
290+
Non-JSON values use **primitive operators** with token operands:
279291

280-
| Value | Constructor Form |
281-
|-------|------------------|
292+
| Value | Form |
293+
|-------|------|
282294
| `undefined` | `[]` (empty array, JSON round-trip safe) |
283295
| `NaN` | `[, NaN]` (JS runtime only, serializes to `[null, null]`) |
284296
| `Infinity` | `[, Infinity]` (JS runtime only, serializes to `[null, null]`) |
285-
| `/abc/gi` | `['//', 'abc', 'gi']` |
297+
| `/abc/gi` | `['//', 'abc', 'gi']` (pattern/flags are tokens) |
286298
| `/abc/` | `['//', 'abc']` |
287-
| `10n` (BigInt) | `['n', '10']` |
299+
| `10n` (BigInt) | `['n', '10']` (digits are token) |
300+
| `100px` | `['px', '100']` (unit with value token) |
288301
| `Symbol('x')` | `['()', 'Symbol', [, 'x']]` (function call) |
289-
| `100px` | `['px', [, 100]]` (unit operator) |
290302
| `` `a${x}b` `` | `` ['`', [, 'a'], 'x', [, 'b']] `` (interpolation) |
291303

292-
**Regex** uses `//` as constructor operator (distinct from `/` division):
293-
- Division: `['/', a, b]`
294-
- Regex: `['//', pattern]` or `['//', pattern, flags]`
304+
**Regex** (`//`), **BigInt** (`n`), and **Units** (`px`, `em`, etc.) use tokens because they're syntactic parts of the primitive, like property names in `.` access.
295305

296306
**Template literals** must be operations — they contain sub-expressions to evaluate.
297307

298-
**Note**: For full JSON serialization portability, non-JSON primitives should use constructor form. JS-only values (`undefined`, `NaN`, `Infinity`) work in JS runtime but serialize to `null`.
299308

309+
### Examples
310+
311+
**Property access** — name is token:
312+
```
313+
a.b → ['.', 'a', 'b']
314+
a?.b → ['?.', 'a', 'b']
315+
```
316+
317+
**Primitives** — pattern/digits/unit are tokens:
318+
```
319+
/abc/gi → ['//', 'abc', 'gi']
320+
10n → ['n', '10']
321+
100px → ['px', '100']
322+
```
323+
324+
**Variables** — wrap assignment expression:
325+
```
326+
let x → ['let', 'x']
327+
let x = 1 → ['let', ['=', 'x', [, 1]]]
328+
const y = 2 → ['const', ['=', 'y', [, 2]]]
329+
```
300330

301-
### Operand Validity
331+
**For loops** — head is an expression:
332+
```
333+
for (x of arr) {} → ['for', ['of', 'x', 'arr'], body]
334+
for (const x of arr) {} → ['for', ['of', ['const', 'x'], 'arr'], body]
335+
for (let x of arr) {} → ['for', ['of', ['let', 'x'], 'arr'], body]
336+
for (x in obj) {} → ['for', ['in', 'x', 'obj'], body]
337+
for (;;) {} → ['for', [';', null, null, null], body]
338+
```
302339

303-
Operands must be valid tree nodes:
340+
**Try/catch** — uses `catch`/`finally` as operators:
341+
```
342+
try { a } catch (e) { b } → ['catch', ['try', 'a'], 'e', 'b']
343+
try { a } finally { c } → ['finally', ['try', 'a'], 'c']
344+
try { a } catch (e) { b } finally { c } → ['finally', ['catch', ['try', 'a'], 'e', 'b'], 'c']
345+
```
304346

347+
**Functions** — name and params are tokens:
305348
```
306-
['px', [, 100]] ✓ literal operand
307-
['px', 'x'] ✓ identifier operand
308-
['px', 100] ✗ raw number is not a tree node
349+
function f(a, b) { c } → ['function', 'f', [',', 'a', 'b'], 'c']
350+
function(x) { y } → ['function', '', 'x', 'y']
351+
x => x → ['=>', 'x', 'x']
352+
(a, b) => a + b → ['=>', ['()', [',', 'a', 'b']], ['+', 'a', 'b']]
309353
```
310354

311355

312356

357+
313358
## Serialization
314359

315360
The format is valid JSON when serialized with standard `JSON.stringify`. Elided array elements serialize as `null`:

test/jessie.js

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,124 @@ test('jessie: compile integration', () => {
7272
run('for (let i = 0; i < 5; i++) sum = sum + i', ctx)
7373
is(ctx.sum, 10)
7474
})
75+
76+
test('jessie: JSON roundtrip - variables', () => {
77+
// let x
78+
let ast = parse('let x')
79+
let restored = JSON.parse(JSON.stringify(ast))
80+
is(restored, ['let', 'x'])
81+
82+
// let x = 1
83+
ast = parse('let x = 1')
84+
restored = JSON.parse(JSON.stringify(ast))
85+
is(restored, ['let', ['=', 'x', [null, 1]]])
86+
run('let x = 5', {}) // should not throw
87+
88+
// const y = 2
89+
ast = parse('const y = 2')
90+
restored = JSON.parse(JSON.stringify(ast))
91+
is(restored, ['const', ['=', 'y', [null, 2]]])
92+
})
93+
94+
test('jessie: JSON roundtrip - for loops', () => {
95+
// for (x of arr)
96+
let ast = parse('for (x of arr) x')
97+
let restored = JSON.parse(JSON.stringify(ast))
98+
is(restored, ['for', ['of', 'x', 'arr'], 'x'])
99+
let result = []
100+
run('for (x of arr) result.push(x)', { arr: [1, 2, 3], result })
101+
is(result, [1, 2, 3])
102+
103+
// for (const x of arr)
104+
ast = parse('for (const x of arr) x')
105+
restored = JSON.parse(JSON.stringify(ast))
106+
is(restored, ['for', ['of', ['const', 'x'], 'arr'], 'x'])
107+
108+
// for (let x of arr)
109+
ast = parse('for (let x of arr) x')
110+
restored = JSON.parse(JSON.stringify(ast))
111+
is(restored, ['for', ['of', ['let', 'x'], 'arr'], 'x'])
112+
113+
// for (x in obj)
114+
ast = parse('for (x in obj) x')
115+
restored = JSON.parse(JSON.stringify(ast))
116+
is(restored, ['for', ['in', 'x', 'obj'], 'x'])
117+
118+
// for (;;)
119+
ast = parse('for (;;) break')
120+
restored = JSON.parse(JSON.stringify(ast))
121+
is(restored, ['for', [';', null, null, null], ['break']])
122+
})
123+
124+
test('jessie: JSON roundtrip - try/catch', () => {
125+
// try { a } catch (e) { b } — uses 'catch' operator wrapping try
126+
let ast = parse('try { a } catch (e) { b }')
127+
let restored = JSON.parse(JSON.stringify(ast))
128+
is(restored, ['catch', ['try', 'a'], 'e', 'b'])
129+
130+
// try { a } finally { c } — uses 'finally' operator wrapping try
131+
ast = parse('try { a } finally { c }')
132+
restored = JSON.parse(JSON.stringify(ast))
133+
is(restored, ['finally', ['try', 'a'], 'c'])
134+
135+
// try { a } catch (e) { b } finally { c } — finally wraps catch wraps try
136+
ast = parse('try { a } catch (e) { b } finally { c }')
137+
restored = JSON.parse(JSON.stringify(ast))
138+
is(restored, ['finally', ['catch', ['try', 'a'], 'e', 'b'], 'c'])
139+
})
140+
141+
test('jessie: JSON roundtrip - functions', () => {
142+
// function f(a, b) { c }
143+
let ast = parse('function f(a, b) { c }')
144+
let restored = JSON.parse(JSON.stringify(ast))
145+
is(restored, ['function', 'f', [',', 'a', 'b'], 'c'])
146+
147+
// function(x) { y } — empty string for anonymous
148+
ast = parse('function(x) { y }')
149+
restored = JSON.parse(JSON.stringify(ast))
150+
is(restored, ['function', '', 'x', 'y'])
151+
152+
// x => x (no parens)
153+
ast = parse('x => x')
154+
restored = JSON.parse(JSON.stringify(ast))
155+
is(restored, ['=>', 'x', 'x'])
156+
157+
// (a, b) => a + b — parens preserved in AST
158+
ast = parse('(a, b) => a + b')
159+
restored = JSON.parse(JSON.stringify(ast))
160+
is(restored, ['=>', ['()', [',', 'a', 'b']], ['+', 'a', 'b']])
161+
})
162+
163+
test('jessie: JSON roundtrip - property access', () => {
164+
// a.b - name is token
165+
let ast = parse('a.b')
166+
let restored = JSON.parse(JSON.stringify(ast))
167+
is(restored, ['.', 'a', 'b'])
168+
169+
// a?.b - name is token
170+
ast = parse('a?.b')
171+
restored = JSON.parse(JSON.stringify(ast))
172+
is(restored, ['?.', 'a', 'b'])
173+
})
174+
175+
test('jessie: compile from JSON-restored AST', () => {
176+
// Full roundtrip: parse → JSON → restore → compile → run
177+
const cases = [
178+
['let x = 5; x * 2', {}, 10],
179+
['x => x * 2', {}, fn => fn(5) === 10],
180+
['a.b', { a: { b: 42 } }, 42],
181+
['a?.b', { a: { b: 42 } }, 42],
182+
['a?.b', { a: null }, undefined],
183+
]
184+
185+
for (const [code, ctx, expected] of cases) {
186+
const ast = parse(code)
187+
const restored = JSON.parse(JSON.stringify(ast))
188+
const result = compile(restored)(ctx)
189+
if (typeof expected === 'function') {
190+
is(expected(result), true, code)
191+
} else {
192+
is(result, expected, code)
193+
}
194+
}
195+
})

test/justin.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ test('justin: JSON serialization', () => {
8686
// Compile from JSON-restored AST
8787
const ast = JSON.parse(JSON.stringify(parse('1 + 2')))
8888
is(compile(ast)({}), 3)
89+
90+
// Property access - name is token
91+
is(JSON.parse(JSON.stringify(parse('a.b'))), ['.', 'a', 'b'])
92+
is(JSON.parse(JSON.stringify(parse('a?.b'))), ['?.', 'a', 'b'])
8993
})
9094

9195
test('justin: logical', () => {

0 commit comments

Comments
 (0)