Skip to content

Commit d048fca

Browse files
committed
Swap order for catch operator
1 parent 801d237 commit d048fca

File tree

8 files changed

+220
-68
lines changed

8 files changed

+220
-68
lines changed

.github/copilot-instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
Avoid writing to `lookup`, use token instead.
22
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.
33
`/features` represent generic language features, not only JS: avoid js-specific checks.
4+
AST should be faithful to source tokens - don't normalize or assume C-like semantics. If a language has `elif`/`elsif` tokens, preserve them in AST rather than converting to nested `else if`. Parse what's written, don't invent structure.
45
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.
56
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.
67
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.

.work/research.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
## AST Structure Decisions
2+
3+
### AST vs CST (2026-02-01)
4+
5+
**Decision:** Subscript produces AST (Abstract Syntax Tree), not CST (Concrete Syntax Tree).
6+
7+
| Type | Purpose | Preserves | Use case |
8+
|------|---------|-----------|----------|
9+
| CST | Source-faithful | All tokens, whitespace | Formatters, refactoring |
10+
| AST | Semantic structure | Meaning only | Compilers, evaluators |
11+
12+
**Implications:**
13+
- Delimiters like `()` `{}` stripped when purely syntactic
14+
- Keywords preserved when they carry meaning (`catch`, `finally`)
15+
- Normalization happens in parser, not compiler
16+
- Lisp tradition: `(if cond then else)` not `(if cond then (else body))`
17+
18+
---
19+
20+
### Delimiter Handling (2026-02-01)
21+
22+
**Principle:** Strip delimiters when purely syntactic, keep when semantic.
23+
24+
| Construct | Parens meaning | Result |
25+
|-----------|---------------|--------|
26+
| `if (cond)` | Required syntax | Strip → `['if', cond, ...]` |
27+
| `while (cond)` | Required syntax | Strip → `['while', cond, ...]` |
28+
| `f(a, b)` | Call operator | Keep → `['()', 'f', ...]` |
29+
| `(a, b) => x` | Grouping | Keep → `['=>', ['()', ...], ...]` |
30+
31+
**Why arrow keeps `['()']`:** Distinguishes `a => x` from `(a) => x`. Also, Python would need this to distinguish tuple `(a, b)` from grouping.
32+
33+
**Core helper:** `parens()` in parse.js strips and returns content. Features use it when parens are syntactic.
34+
35+
---
36+
37+
### Try/catch structure (2026-02-01)
38+
39+
**Decision:** Keywords as operators: `['try', body, ['catch', param, handler]?, ['finally', cleanup]?]`
40+
41+
**Rationale:**
42+
- `catch` and `finally` are distinct keywords deserving operator status
43+
- Clean optional structure - just omit the clause
44+
- No null padding needed
45+
- Dialect-friendly: Python `except`, Ruby `rescue` become their own operators
46+
47+
**Examples:**
48+
```
49+
try { a } catch (e) { b } → ['try', 'a', ['catch', 'e', 'b']]
50+
try { a } finally { c } → ['try', 'a', ['finally', 'c']]
51+
try { a } catch (e) { b } finally { c } → ['try', 'a', ['catch', 'e', 'b'], ['finally', 'c']]
52+
```
53+
54+
---
55+
56+
### If-else-if chains (2026-02-01)
57+
58+
**Decision:** `['if', cond, then, ['if', cond2, then2, else]]` for C-family
59+
60+
**Rejected alternatives:**
61+
1. `['if', c, t, ['else', ['if', ...]]]``else` isn't an operator, just positional marker
62+
2. `['if', c1, t1, c2, t2, else]` — flat chain loses structure, hard to delimit
63+
64+
**Rationale:**
65+
- JS/C have no `else if` token — else branch contains if statement
66+
- Position 4 = else branch, which can be any expression including another if
67+
- Minimal structure: no invented wrappers
68+
- Compiles trivially: `c(ctx) ? t(ctx) : e?.(ctx)`
69+
70+
**Dialect handling:**
71+
- Python `elif` IS a keyword → `['if', c, t, ['elif', c2, t2, ['else', e]]]`
72+
- Ruby `elsif` IS a keyword → `['if', c, t, ['elsif', c2, t2, ['else', e]]]`
73+
- The operator name reflects the source token
74+
75+
**Principle:** Use C tokens as universal semantic structure. Dialects that have distinct keywords (elif, elsif) preserve them. Dialects that don't (JS else-if) don't invent them.
76+
77+
### Try-catch structure (2026-02-01)
78+
79+
**Decision:** Flat `['try', body, param, catch, finally?]`
80+
81+
**Rejected:** Wrapped `['finally', ['catch', ['try', body], ...], ...]`
82+
83+
**Rationale:**
84+
- Consistent with if: flat positional args
85+
- One node per statement, not nested operators
86+
- `null` for missing parts: `['try', body, null, null, finally]`
87+
88+
**Dialect handling:**
89+
- Python: `['try', body, 'e', except, ['else', e], finally]` (try-except has else!)
90+
- Ruby: `['begin', body, 'e', rescue, ensure]`
91+
92+
## Design Principles
93+
94+
### Token Customization
95+
96+
Just as precedence is customizable via `prec()`, tokens should be customizable per dialect.
97+
98+
Current: Features hardcode keywords (`keyword('try')`)
99+
100+
Future possibility:
101+
```js
102+
// Feature exports factory
103+
export default (tokens = {try:'try', catch:'catch', finally:'finally'}) => {
104+
keyword(tokens.try, ...)
105+
}
106+
```
107+
108+
This allows:
109+
- **subscript.js**: default C/JS tokens
110+
- **worm.js** (Python): `{try:'try', catch:'except', finally:'finally', throw:'raise'}`
111+
- **subruby.js**: `{try:'begin', catch:'rescue', finally:'ensure', throw:'raise'}`
112+
113+
### C as Universal Foundation
114+
115+
C-like tokens serve as **canonical semantic representation**, not because C is superior, but because:
116+
1. Widely understood baseline
117+
2. Minimal syntax (no significant whitespace)
118+
3. Most languages have C-family equivalents
119+
120+
Other dialects parse TO this structure (with their tokens), enabling:
121+
- Cross-compilation: Python AST → JS output
122+
- Universal tooling: one AST walker works for all
123+
- Round-trip within dialect: preserves source tokens
File renamed without changes.

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ fn({ a: 1, b: 3 }) // 7
2424

2525
## Presets
2626

27-
**Subscript** – common expressions:
27+
[**Subscript**]() – common expressions:
2828

2929
```js
3030
import subscript from 'subscript'
3131

3232
subscript('a.b + c * 2')({ a: { b: 1 }, c: 3 }) // 7
3333
```
3434

35-
**Justin** – JSON + expressions + templates + arrows:
35+
[**Justin**]() – JSON + expressions + templates + arrows:
3636

3737
```js
3838
import justin from 'subscript/justin.js'
@@ -41,7 +41,7 @@ justin('{ x: a?.b ?? 0, y: [1, ...rest] }')({ a: null, rest: [2, 3] })
4141
// { x: 0, y: [1, 2, 3] }
4242
```
4343

44-
**Jessie** – JSON + expressions + statements, functions (JS subset):
44+
[**Jessie**]() – JSON + expressions + statements, functions (JS subset):
4545

4646
```js
4747
import jessie from 'subscript/jessie.js'

feature/try.js

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,27 @@
11
// try/catch/finally/throw statements
2-
// AST: ['catch', ['try', body], param, catchBody] or ['finally', inner, body]
3-
import { space, parse, parens, expr, operator, compile } from '../parse.js';
4-
import { keyword, infix, block } from './block.js';
2+
// AST (faithful): ['try', body, ['catch', param, handler]?, ['finally', cleanup]?]
3+
// Note: body/handler are raw block results, param is raw parens result
4+
import { space, parse, parens, expr, word, skip, operator, compile } from '../parse.js';
5+
import { keyword, block } from './block.js';
56
import { BREAK, CONTINUE, RETURN } from './control.js';
67

78
const STATEMENT = 5;
89

9-
keyword('try', STATEMENT + 1, () => ['try', block()]);
10-
infix('catch', STATEMENT + 1, a => (space(), ['catch', a, parens(), block()]));
11-
infix('finally', STATEMENT + 1, a => ['finally', a, block()]);
10+
// try { body } [catch (param) { handler }] [finally { cleanup }]
11+
keyword('try', STATEMENT + 1, () => {
12+
const node = ['try', block()];
13+
space();
14+
if (word('catch')) {
15+
skip(5); space();
16+
node.push(['catch', parens(), block()]);
17+
}
18+
space();
19+
if (word('finally')) {
20+
skip(7);
21+
node.push(['finally', block()]);
22+
}
23+
return node;
24+
});
1225

1326
keyword('throw', STATEMENT + 1, () => {
1427
parse.asi && (parse.newline = false);
@@ -17,39 +30,33 @@ keyword('throw', STATEMENT + 1, () => {
1730
return ['throw', expr(STATEMENT)];
1831
});
1932

20-
// Compile
21-
operator('try', tryBody => {
33+
// Compile try - normalize in compiler, not parser
34+
operator('try', (tryBody, ...clauses) => {
2235
tryBody = tryBody ? compile(tryBody) : null;
23-
return ctx => tryBody?.(ctx);
24-
});
36+
37+
let catchClause = clauses.find(c => c?.[0] === 'catch');
38+
let finallyClause = clauses.find(c => c?.[0] === 'finally');
39+
40+
const catchParam = catchClause?.[1];
41+
const catchBody = catchClause?.[2] ? compile(catchClause[2]) : null;
42+
const finallyBody = finallyClause?.[1] ? compile(finallyClause[1]) : null;
2543

26-
operator('catch', (tryNode, catchName, catchBody) => {
27-
const tryBody = tryNode?.[1] ? compile(tryNode[1]) : null;
28-
catchBody = catchBody ? compile(catchBody) : null;
2944
return ctx => {
3045
let result;
3146
try {
3247
result = tryBody?.(ctx);
3348
} catch (e) {
3449
if (e === BREAK || e === CONTINUE || e === RETURN) throw e;
35-
if (catchName !== null && catchBody) {
36-
const had = catchName in ctx, orig = ctx[catchName];
37-
ctx[catchName] = e;
50+
if (catchParam !== null && catchParam !== undefined && catchBody) {
51+
const had = catchParam in ctx, orig = ctx[catchParam];
52+
ctx[catchParam] = e;
3853
try { result = catchBody(ctx); }
39-
finally { had ? ctx[catchName] = orig : delete ctx[catchName]; }
40-
} else if (catchName === null) throw e;
54+
finally { had ? ctx[catchParam] = orig : delete ctx[catchParam]; }
55+
} else if (!catchBody) throw e;
56+
}
57+
finally {
58+
finallyBody?.(ctx);
4159
}
42-
return result;
43-
};
44-
});
45-
46-
operator('finally', (inner, finallyBody) => {
47-
inner = inner ? compile(inner) : null;
48-
finallyBody = finallyBody ? compile(finallyBody) : null;
49-
return ctx => {
50-
let result;
51-
try { result = inner?.(ctx); }
52-
finally { finallyBody?.(ctx); }
5360
return result;
5461
};
5562
});

spec.md

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -174,15 +174,7 @@ Template literals contain string parts (as literals) interleaved with expression
174174

175175
### Statements (jessie)
176176

177-
ASI (Automatic Semicolon Insertion) treats newlines as statement separators, producing flat `;` arrays:
178-
```
179-
a; b → [';', 'a', 'b']
180-
a\nb → [';', 'a', 'b'] (ASI inserts ;)
181-
a; b; c → [';', 'a', 'b', 'c']
182-
a\nb\nc → [';', 'a', 'b', 'c'] (flat, not nested)
183-
```
184-
185-
Control flow:
177+
### Control flow
186178
```
187179
['if', cond, then] if (cond) then
188180
['if', cond, then, else] if (cond) then else alt
@@ -203,10 +195,10 @@ Control flow:
203195

204196
### Exceptions (feature/throw.js, feature/try.js)
205197
```
206-
['throw', val] throw val
207-
['catch', ['try', body], 'e', handler] try { body } catch (e) { handler }
208-
['finally', ['try', body], handler] try { body } finally { handler }
209-
['finally', ['catch', ...], handler] try {...} catch {...} finally {...}
198+
['throw', val] throw val
199+
['try', body, ['catch', 'e', handler]] try { body } catch (e) { handler }
200+
['try', body, ['finally', cleanup]] try { body } finally { cleanup }
201+
['try', body, ['catch', 'e', h], ['finally', c]] try {...} catch {...} finally {...}
210202
```
211203

212204
### Function Declarations (feature/function.js)
@@ -256,7 +248,20 @@ Postfix operators use `null` to mark the absent second operand:
256248

257249
This preserves structural consistency without inventing new node types.
258250

259-
### 5. Flat Sequences
251+
252+
### 5. AST, not CST
253+
254+
This is an Abstract, not Concrete Syntax Tree.
255+
The parser normalizes syntax to semantic structure.
256+
Delimiters like `()` `{}` are stripped when purely syntactic; operators preserve meaning.
257+
258+
| Type | Purpose | Example |
259+
|------|---------|---------|
260+
| CST | Preserve source exactly | Formatters, refactoring tools |
261+
| AST | Semantic structure | Compilers, evaluators |
262+
263+
264+
### 6. Flat Sequences
260265

261266
Associative operators flatten:
262267
```
@@ -265,7 +270,7 @@ a + b + c → ['+', 'a', 'b', 'c'] // not ['+', ['+', 'a', 'b'], 'c']
265270

266271
This reflects execution semantics and enables SIMD-style optimization.
267272

268-
### 6. Location Metadata
273+
### 7. Location Metadata
269274

270275
Nodes may carry a `.loc` property indicating source position:
271276
```js
@@ -340,11 +345,11 @@ for (x in obj) {} → ['for', ['in', 'x', 'obj'], body]
340345
for (;;) {} → ['for', [';', null, null, null], body]
341346
```
342347

343-
**Try/catch**uses `catch`/`finally` as operators:
348+
**Try/catch**keywords preserved as operators:
344349
```
345-
try { a } catch (e) { b } → ['catch', ['try', 'a'], 'e', 'b']
346-
try { a } finally { c } → ['finally', ['try', 'a'], 'c']
347-
try { a } catch (e) { b } finally { c } → ['finally', ['catch', ['try', 'a'], 'e', 'b'], 'c']
350+
try { a } catch (e) { b } → ['try', 'a', ['catch', 'e', 'b']]
351+
try { a } finally { c } → ['try', 'a', ['finally', 'c']]
352+
try { a } catch (e) { b } finally { c } → ['try', 'a', ['catch', 'e', 'b'], ['finally', 'c']]
348353
```
349354

350355
**Functions** — name and params are tokens:

test/jessie.js

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -141,24 +141,30 @@ test('jessie: try basic', () => {
141141

142142
test('jessie: try-catch', () => {
143143
const ast = parse('try { x } catch (e) { y }')
144-
is(ast[0], 'catch')
145-
is(ast[1][0], 'try')
146-
is(ast[2], 'e')
147-
is(ast[3], 'y')
144+
is(ast[0], 'try')
145+
is(ast[1], 'x')
146+
is(ast[2][0], 'catch')
147+
is(ast[2][1], 'e')
148+
is(ast[2][2], 'y')
148149
})
149150

150151
test('jessie: try-finally', () => {
151152
const ast = parse('try { x } finally { y }')
152-
is(ast[0], 'finally')
153-
is(ast[1][0], 'try')
154-
is(ast[2], 'y')
153+
is(ast[0], 'try')
154+
is(ast[1], 'x')
155+
is(ast[2][0], 'finally')
156+
is(ast[2][1], 'y')
155157
})
156158

157159
test('jessie: try-catch-finally', () => {
158160
const ast = parse('try { a } catch (e) { b } finally { c }')
159-
is(ast[0], 'finally')
160-
is(ast[1][0], 'catch')
161-
is(ast[2], 'c')
161+
is(ast[0], 'try')
162+
is(ast[1], 'a')
163+
is(ast[2][0], 'catch')
164+
is(ast[2][1], 'e')
165+
is(ast[2][2], 'b')
166+
is(ast[3][0], 'finally')
167+
is(ast[3][1], 'c')
162168
})
163169

164170
test('jessie: throw', () => {
@@ -519,15 +525,15 @@ test('jessie: JSON roundtrip - for loops', () => {
519525
test('jessie: JSON roundtrip - try/catch', () => {
520526
let ast = parse('try { a } catch (e) { b }')
521527
let restored = JSON.parse(JSON.stringify(ast))
522-
is(restored, ['catch', ['try', 'a'], 'e', 'b'])
528+
is(restored, ['try', 'a', ['catch', 'e', 'b']])
523529

524530
ast = parse('try { a } finally { c }')
525531
restored = JSON.parse(JSON.stringify(ast))
526-
is(restored, ['finally', ['try', 'a'], 'c'])
532+
is(restored, ['try', 'a', ['finally', 'c']])
527533

528534
ast = parse('try { a } catch (e) { b } finally { c }')
529535
restored = JSON.parse(JSON.stringify(ast))
530-
is(restored, ['finally', ['catch', ['try', 'a'], 'e', 'b'], 'c'])
536+
is(restored, ['try', 'a', ['catch', 'e', 'b'], ['finally', 'c']])
531537
})
532538

533539
test('jessie: JSON roundtrip - functions', () => {

0 commit comments

Comments
 (0)