Skip to content

Commit 74f4ce9

Browse files
committed
Make ASI more flexible
1 parent 4ba47ca commit 74f4ce9

6 files changed

Lines changed: 90 additions & 98 deletions

File tree

docs.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,25 @@ jessie`
163163
`({}) // 120
164164
```
165165

166+
#### ASI (Automatic Semicolon Insertion)
166167

168+
Jessie supports JS-style ASI – newlines at statement level act as semicolons:
169+
170+
```js
171+
jessie('a = 1\nb = 2\na + b')({}) // 3
172+
```
173+
174+
ASI precedence can be customized via `prec.asi`:
175+
176+
```js
177+
import { prec } from 'subscript/parse.js'
178+
179+
prec.asi = 0 // disable ASI (require explicit semicolons)
180+
prec.asi = 150 // ASI even inside expressions (newline always separates)
181+
delete prec.asi // restore default (prec[';'])
182+
183+
import 'subscript/feature/asi.js
184+
```
167185
168186
169187
## Extend Parser

feature/asi.js

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
1-
/**
2-
* Automatic Semicolon Insertion (ASI)
3-
*
4-
* JS-style ASI: insert virtual ; when newline precedes illegal token at statement level
5-
*/
6-
import { parse } from '../parse.js';
1+
// ASI: newline at `;` precedence level triggers nary `;`
2+
import { parse, prec } from '../parse.js';
73

8-
const STATEMENT = 5;
4+
// Set prec.asi before importing to customize (default: prec[';'])
5+
const lvl = prec.asi ?? prec[';'];
96

10-
parse.asi = (token, prec, expr) => {
11-
if (prec >= STATEMENT) return; // only at statement level
12-
const next = expr(STATEMENT - .5);
13-
if (!next) return;
14-
return token?.[0] !== ';' ? [';', token, next] : (token.push(next), token);
15-
};
7+
parse.asi = (a, p, expr, b, items) => p < lvl && (b = expr(lvl - .5)) && (
8+
items = b?.[0] === ';' ? b.slice(1) : [b],
9+
a?.[0] === ';' ? (a.push(...items), a) : [';', a, ...items]
10+
);

parse.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export let idx, cur,
4747
cc !== end &&
4848
(newNode =
4949
((fn = lookup[cc]) && fn(token, prec)) ??
50-
(parse.asi && token && nl && (newNode = parse.asi(token, prec, expr))) ??
50+
(token && nl && parse.asi?.(token, prec, expr)) ??
5151
(!token && !parse.reserved && next(parse.id))
5252
)
5353
) token = newNode, parse.reserved = 0;
@@ -84,26 +84,29 @@ export let idx, cur,
8484
// operator lookup table
8585
lookup = [],
8686

87+
// precedence registry - features register, others can read/override
88+
prec = {},
89+
8790
// create operator checker/mapper
8891
token = (
8992
op,
90-
prec = SPACE,
93+
p = SPACE,
9194
map,
9295
c = op.charCodeAt(0),
9396
l = op.length,
9497
prev = lookup[c],
9598
word = op.toUpperCase() !== op,
9699
matched, r
97-
) => lookup[c] = (a, curPrec, curOp, from = idx) =>
100+
) => (prec[op] = p, lookup[c] = (a, curPrec, curOp, from = idx) =>
98101
(matched = curOp,
99102
(curOp ?
100103
op == curOp :
101104
(l < 2 || (op.charCodeAt(1) === cur.charCodeAt(idx + 1) && (l < 3 || cur.substr(idx, l) == op))) && (!word || !parse.id(cur.charCodeAt(idx + l))) && (matched = curOp = op)
102105
) &&
103-
curPrec < prec &&
106+
curPrec < p &&
104107
(idx += l, (r = map(a)) ? loc(r, from) : (idx = from, matched = 0, word && r !== false && (parse.reserved = 1), !word && !prev && err()), r)
105108
) ||
106-
prev?.(a, curPrec, matched),
109+
prev?.(a, curPrec, matched)),
107110

108111
binary = (op, prec, right = false) => token(op, prec, (a, b) => a && (b = expr(prec - (right ? .5 : 0))) && [op, a, b]),
109112

spec.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,16 @@ Keywords are literals — they have no operator, only a value.
181181
Template literals contain string parts (as literals) interleaved with expression parts (as nodes).
182182

183183
### Statements (jessie)
184+
185+
ASI (Automatic Semicolon Insertion) treats newlines as statement separators, producing flat `;` arrays:
186+
```
187+
a; b → [';', 'a', 'b']
188+
a\nb → [';', 'a', 'b'] (ASI inserts ;)
189+
a; b; c → [';', 'a', 'b', 'c']
190+
a\nb\nc → [';', 'a', 'b', 'c'] (flat, not nested)
191+
```
192+
193+
Control flow:
184194
```
185195
['if', cond, then] if (cond) then
186196
['if', cond, then, else] if (cond) then else alt

test/asi.js

Lines changed: 0 additions & 80 deletions
This file was deleted.

test/jessie.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,13 +397,59 @@ test('jessie: ASI basic', () => {
397397
is(ast[2], 'b')
398398
})
399399

400+
test('jessie: ASI flat arrays', () => {
401+
// 2 stmts - both forms should be identical
402+
is(parse('a;b'), [';', 'a', 'b'])
403+
is(parse('a\nb'), [';', 'a', 'b'])
404+
405+
// 3 stmts - flat array, not nested
406+
is(parse('a;b;c'), [';', 'a', 'b', 'c'])
407+
is(parse('a\nb\nc'), [';', 'a', 'b', 'c'])
408+
is(parse('a;b\nc'), [';', 'a', 'b', 'c'])
409+
is(parse('a\nb;c'), [';', 'a', 'b', 'c'])
410+
411+
// 4 stmts - all variations flat
412+
is(parse('a;b;c;d'), [';', 'a', 'b', 'c', 'd'])
413+
is(parse('a\nb\nc\nd'), [';', 'a', 'b', 'c', 'd'])
414+
is(parse('a;b\nc;d'), [';', 'a', 'b', 'c', 'd'])
415+
})
416+
400417
test('jessie: ASI return', () => {
401418
const ast = parse('return\nx')
402419
is(ast[0], ';')
403420
is(ast[1][0], 'return')
404421
is(ast[2], 'x')
405422
})
406423

424+
test('jessie: ASI custom precedence', async () => {
425+
const { prec, parse } = await import('../parse.js')
426+
427+
// Default: ASI at statement level (prec[';'] = 5)
428+
// Newline continues expression when inside higher-precedence context
429+
is(parse('a + b\nc'), [';', ['+', 'a', 'b'], 'c'])
430+
is(parse('a\n+ b'), ['+', 'a', 'b'], 'continuation token binds')
431+
432+
// Disable ASI and reimport feature fresh
433+
prec.asi = 0
434+
parse.asi = null
435+
await import('../feature/asi.js?v=disabled')
436+
let threw = false
437+
try { parse('a\nb') } catch { threw = true }
438+
is(threw, true, 'asi=0 disables ASI')
439+
440+
// High precedence: ASI triggers inside expressions
441+
prec.asi = 150
442+
parse.asi = null
443+
await import('../feature/asi.js?v=high')
444+
is(parse('a + b\nc'), ['+', 'a', [';', 'b', 'c']], 'ASI inside + rhs')
445+
446+
// Restore default
447+
delete prec.asi
448+
parse.asi = null
449+
await import('../feature/asi.js?v=restore')
450+
is(parse('a\nb'), [';', 'a', 'b'])
451+
})
452+
407453
// === compile integration ===
408454

409455
test('jessie: compile integration', () => {

0 commit comments

Comments
 (0)