Skip to content

Commit 9bc39c9

Browse files
committed
Make more JSON-friendly
1 parent bca9766 commit 9bc39c9

File tree

8 files changed

+102
-29
lines changed

8 files changed

+102
-29
lines changed

feature/literal.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22
* Literal values
33
*
44
* true, false, null, undefined, NaN, Infinity
5+
*
6+
* Note: undefined uses [] (empty array) for JSON round-trip compatibility.
7+
* [, undefined] serializes to [null, null] which would compile to null.
8+
* [] serializes to [] and compiles back to undefined.
59
*/
6-
import { literal } from '../parse.js';
10+
import { literal, token } from '../parse.js';
711

812
literal('true', true);
913
literal('false', false);
1014
literal('null', null);
11-
literal('undefined', undefined);
15+
token('undefined', 200, a => !a && []); // [] for JSON compatibility
1216
literal('NaN', NaN);
1317
literal('Infinity', Infinity);

feature/regex.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
/**
22
* Regex literals: /pattern/flags
33
*
4-
* AST:
5-
* /abc/gi → [, /abc/gi]
4+
* AST (constructor form, JSON-serializable):
5+
* /abc/gi → ['//', 'abc', 'gi']
6+
* /abc/ → ['//', 'abc']
67
*
78
* Note: Disambiguates from division by context:
89
* - `/` after value = division (falls through to prev)
910
* - `/` at start or after operator = regex
11+
*
12+
* Compile:
13+
* ['//', 'abc', 'gi'] → new RegExp('abc', 'gi')
1014
*/
11-
import { token, skip, err, next, idx, cur } from '../parse.js';
15+
import { token, skip, err, next, idx, cur, operator } from '../parse.js';
1216

1317
const PREFIX = 140, SLASH = 47, BSLASH = 92;
1418

@@ -26,6 +30,16 @@ token('/', PREFIX, a => {
2630
cur.charCodeAt(idx) === SLASH || err('Unterminated regex');
2731
skip(); // consume closing /
2832

29-
try { return [, new RegExp(pattern, next(regexFlag))]; }
33+
const flags = next(regexFlag);
34+
// Validate regex syntax
35+
try { new RegExp(pattern, flags); }
3036
catch (e) { err('Invalid regex: ' + e.message); }
37+
38+
return flags ? ['//', pattern, flags] : ['//', pattern];
39+
});
40+
41+
// Compile: ['//', pattern, flags?] → RegExp
42+
operator('//', (a, b) => {
43+
const re = new RegExp(a, b || '');
44+
return () => re;
3145
});

parse.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,10 @@ export const operator = (op, fn, prev = operators[op]) =>
137137
(operators[op] = (...args) => fn(...args) || prev?.(...args));
138138

139139
// Compile AST to evaluator function
140+
// Note: [, value] serializes to [null, value] in JSON, both forms accepted
140141
export const compile = node => (
141142
!Array.isArray(node) ? (node === undefined ? () => undefined : ctx => ctx?.[node]) :
142-
node[0] === undefined ? (v => () => v)(node[1]) :
143+
node[0] == null ? (v => () => v)(node[1]) : // == catches both undefined and null
143144
operators[node[0]]?.(...node.slice(1)) ?? err(`Unknown operator: ${node[0]}`, node?.loc)
144145
);
145146

spec.md

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,9 @@ Sequence operators flatten naturally into n-ary form.
151151
[, true] true
152152
[, false] false
153153
[, null] null
154-
[, undefined] undefined
155-
[, NaN] NaN
154+
[] undefined (empty array for JSON safety)
155+
[, NaN] NaN (JS only)
156+
[, Infinity] Infinity (JS only)
156157
```
157158

158159
Keywords are literals — they have no operator, only a value.
@@ -272,21 +273,29 @@ Custom operators are simply strings in position zero.
272273

273274
### Non-JSON Primitives
274275

275-
JSON supports: number, string, boolean, null. Other values require representation choices:
276+
Literals (`[, value]`) hold JSON primitives only: number, string, boolean, null.
276277

277-
| Value | Options |
278-
|-------|---------|
279-
| `undefined` | `[, undefined]` (JS only, serializes as `[null]`) |
280-
| `NaN` | `[, NaN]` (JS only) or keyword via context |
281-
| `10n` (BigInt) | `['n', [, '10']]` (constructor) or `[, '10n']` (string literal) |
278+
Non-JSON values use **constructor form** — an operator that constructs the value:
279+
280+
| Value | Constructor Form |
281+
|-------|------------------|
282+
| `undefined` | `[]` (empty array, JSON round-trip safe) |
283+
| `NaN` | `[, NaN]` (JS runtime only, serializes to `[null, null]`) |
284+
| `Infinity` | `[, Infinity]` (JS runtime only, serializes to `[null, null]`) |
285+
| `/abc/gi` | `['//', 'abc', 'gi']` |
286+
| `/abc/` | `['//', 'abc']` |
287+
| `10n` (BigInt) | `['n', '10']` |
282288
| `Symbol('x')` | `['()', 'Symbol', [, 'x']]` (function call) |
283-
| `/abc/gi` | `['regex', [, 'abc'], [, 'gi']]` (constructor) or `[, '/abc/gi']` (string) |
284-
| `100px` | `['px', [, 100]]` (operator) or `[, '100px']` (string literal) |
285-
| `` `a${x}b` `` | `['\`', [, 'a'], 'x', [, 'b']]` (must interpolate) |
289+
| `100px` | `['px', [, 100]]` (unit operator) |
290+
| `` `a${x}b` `` | `` ['`', [, 'a'], 'x', [, 'b']] `` (interpolation) |
291+
292+
**Regex** uses `//` as constructor operator (distinct from `/` division):
293+
- Division: `['/', a, b]`
294+
- Regex: `['//', pattern]` or `['//', pattern, flags]`
286295

287296
**Template literals** must be operations — they contain sub-expressions to evaluate.
288297

289-
**Units, regex, BigInt** can be either operators or string literals — parser decides, evaluator must match.
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`.
290299

291300

292301
### Operand Validity

test/feature/regex.js

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,20 @@ const run = (code, ctx = {}) => compile(parse(code))(ctx)
66

77
test('regex: basic', t => {
88
const ast = parse('/abc/')
9-
is(ast[0], undefined)
10-
is(ast[1] instanceof RegExp, true)
11-
is(ast[1].source, 'abc')
9+
is(ast[0], '//')
10+
is(ast[1], 'abc')
11+
is(ast.length, 2) // no flags
1212
})
1313

1414
test('regex: flags', t => {
15-
is(parse('/abc/gi')[1].flags, 'gi')
16-
is(parse('/test/m')[1].multiline, true)
17-
is(parse('/test/i')[1].ignoreCase, true)
15+
is(parse('/abc/gi'), ['//', 'abc', 'gi'])
16+
is(parse('/test/m'), ['//', 'test', 'm'])
17+
is(parse('/test/i'), ['//', 'test', 'i'])
1818
})
1919

2020
test('regex: escapes', t => {
21-
is(parse('/a\\/b/')[1].source, 'a\\/b')
22-
is(parse('/a\\nb/')[1].source, 'a\\nb')
21+
is(parse('/a\\/b/')[1], 'a\\/b')
22+
is(parse('/a\\nb/')[1], 'a\\nb')
2323
})
2424

2525
test('regex: eval', t => {
@@ -41,3 +41,15 @@ test('regex: in expressions', t => {
4141
is(run('x.match(/\\d+/)', { x: 'abc123def' })[0], '123')
4242
is(run('x.replace(/a/g, "b")', { x: 'aaa' }), 'bbb')
4343
})
44+
45+
test('regex: JSON serializable', t => {
46+
const ast = parse('/abc/gi')
47+
const json = JSON.stringify(ast)
48+
const restored = JSON.parse(json)
49+
is(restored, ['//', 'abc', 'gi'])
50+
// Can compile from restored AST
51+
const re = compile(restored)()
52+
is(re instanceof RegExp, true)
53+
is(re.source, 'abc')
54+
is(re.flags, 'gi')
55+
})

test/justin.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,27 @@ test('justin: booleans', () => {
6767
is(justin('null')(), null)
6868
})
6969

70+
test('justin: JSON serialization', () => {
71+
// JSON primitives serialize cleanly
72+
is(JSON.parse(JSON.stringify(parse('true'))), [null, true])
73+
is(JSON.parse(JSON.stringify(parse('false'))), [null, false])
74+
is(JSON.parse(JSON.stringify(parse('null'))), [null, null])
75+
is(JSON.parse(JSON.stringify(parse('42'))), [null, 42])
76+
is(JSON.parse(JSON.stringify(parse('"hi"'))), [null, 'hi'])
77+
78+
// undefined uses [] for JSON round-trip (compiles back to undefined)
79+
is(JSON.parse(JSON.stringify(parse('undefined'))), [])
80+
is(compile(JSON.parse(JSON.stringify(parse('undefined'))))(), undefined)
81+
82+
// NaN/Infinity lose value in JSON (become null)
83+
is(JSON.parse(JSON.stringify(parse('NaN'))), [null, null])
84+
is(JSON.parse(JSON.stringify(parse('Infinity'))), [null, null])
85+
86+
// Compile from JSON-restored AST
87+
const ast = JSON.parse(JSON.stringify(parse('1 + 2')))
88+
is(compile(ast)({}), 3)
89+
})
90+
7091
test('justin: logical', () => {
7192
is(parse('a && b'), ['&&', 'a', 'b'])
7293
is(parse('a || b'), ['||', 'a', 'b'])

test/stringify.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,9 @@ test('stringify: sequence', () => {
149149
is(gen('a,b'), 'a, b')
150150
is(gen('a,b,c'), 'a, b, c')
151151
})
152+
153+
test('stringify: regex', () => {
154+
is(gen('/abc/'), '/abc/')
155+
is(gen('/abc/gi'), '/abc/gi')
156+
is(gen('/a\\/b/'), '/a\\/b/')
157+
})

util/stringify.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@ export const codegen = node => {
2525
// Literal: [, value]
2626
if (op === undefined) return typeof args[0] === 'string' ? JSON.stringify(args[0]) : String(args[0]);
2727

28-
// Custom generator
29-
if (generators[op]) return generators[op](...args);
28+
// Custom generator (if returns result, use it; undefined falls through)
29+
if (generators[op]) {
30+
const result = generators[op](...args);
31+
if (result !== undefined) return result;
32+
}
3033

3134
// Brackets: [], {}, ()
3235
if (op === '[]' || op === '{}' || op === '()') {
@@ -143,6 +146,9 @@ generator(':', (k, v) => (typeof k === 'string' ? k : '[' + codegen(k) + ']') +
143146
generator('`', (...parts) => '`' + parts.map(p => p?.[0] === undefined ? String(p[1]).replace(/`/g, '\\`').replace(/\$/g, '\\$') : '${' + codegen(p) + '}').join('') + '`');
144147
generator('``', (tag, ...parts) => codegen(tag) + '`' + parts.map(p => p?.[0] === undefined ? String(p[1]).replace(/`/g, '\\`').replace(/\$/g, '\\$') : '${' + codegen(p) + '}').join('') + '`');
145148

149+
// Regex constructor: ['//', pattern, flags?]
150+
generator('//', (a, b) => '/' + a + '/' + (b || ''));
151+
146152
// Getter/Setter
147153
generator('get', (name, body) => 'get ' + name + '() { ' + (body ? codegen(body) : '') + ' }');
148154
generator('set', (name, param, body) => 'set ' + name + '(' + param + ') { ' + (body ? codegen(body) : '') + ' }');

0 commit comments

Comments
 (0)