jz 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/jzify.js ADDED
@@ -0,0 +1,391 @@
1
+ /**
2
+ * jzify — Transform JS AST into jz-compatible form.
3
+ *
4
+ * Crockford-aligned: eliminates bad parts, enforces good practices.
5
+ * Runs before prepare() as an AST→AST pass.
6
+ *
7
+ * Transforms:
8
+ * function name(args) { body } → const name = (args) => { body }
9
+ * var → let
10
+ * switch → if/else chain
11
+ * new X(args) → X(args) (for known safe constructors)
12
+ * == → ===, != → !==
13
+ *
14
+ * Hoisting: function declarations are collected and moved to the top
15
+ * of their scope (module or block), preserving semantics.
16
+ *
17
+ * @module jzify
18
+ */
19
+
20
+ /**
21
+ * Transform AST in-place. Returns transformed AST.
22
+ * @param {Array} ast - subscript/jessie parsed AST
23
+ * @returns {Array} Transformed AST
24
+ */
25
+ export default function jzify(ast) {
26
+ swIdx = 0
27
+ argsIdx = 0
28
+ doIdx = 0
29
+ return transformScope(ast)
30
+ }
31
+
32
+ /** Transform a scope (module top-level or block body). Collects hoisted functions. */
33
+ function transformScope(node) {
34
+ if (!Array.isArray(node)) return transform(node)
35
+
36
+ const [op, ...args] = node
37
+
38
+ // Statement sequence: collect hoisted functions
39
+ if (op === ';') {
40
+ const hoisted = [], rest = []
41
+ for (const stmt of args) {
42
+ const t = transform(stmt)
43
+ if (t == null) continue
44
+ // Hoist function declarations to top of scope
45
+ if (Array.isArray(t) && t[0] === 'const' && t._hoisted) {
46
+ hoisted.push(t)
47
+ } else if (Array.isArray(t) && t[0] === ';') {
48
+ // Flatten nested ; from multi-statement transforms
49
+ for (const s of t.slice(1)) {
50
+ if (s != null) {
51
+ if (Array.isArray(s) && s[0] === 'const' && s._hoisted) hoisted.push(s)
52
+ else rest.push(s)
53
+ }
54
+ }
55
+ } else {
56
+ rest.push(t)
57
+ }
58
+ }
59
+ // Hoist functions AFTER imports (imports must be processed first for scope resolution)
60
+ const imports = rest.filter(s => Array.isArray(s) && s[0] === 'import')
61
+ const nonImports = rest.filter(s => !(Array.isArray(s) && s[0] === 'import'))
62
+ const all = [...imports, ...hoisted, ...nonImports]
63
+ return all.length === 0 ? null : all.length === 1 ? all[0] : [';', ...all]
64
+ }
65
+
66
+ return transform(node)
67
+ }
68
+
69
+ /** Wrap function body for arrow conversion */
70
+ function wrapArrowBody(body) {
71
+ const t = transformScope(body)
72
+ return Array.isArray(t) && (t[0] === '{}' || t[0] === ';') ? (t[0] === '{}' ? t : ['{}', t]) : ['{}', t]
73
+ }
74
+
75
+ /** Prototype identity check: X.prototype.Y */
76
+ const isProto = n => Array.isArray(n) && n[0] === '.' && Array.isArray(n[1]) && n[1][0] === '.' && n[1][2] === 'prototype'
77
+
78
+ const TYPED_ARRAYS = new Set(['Float64Array','Float32Array','Int32Array','Uint32Array',
79
+ 'Int16Array','Uint16Array','Int8Array','Uint8Array',
80
+ 'ArrayBuffer','BigInt64Array','BigUint64Array','DataView'])
81
+
82
+ // `arguments` lowering: regular `function` has implicit `arguments`; arrow doesn't.
83
+ // jzify converts function → arrow, so any `arguments` use must be rewritten to a rest param.
84
+ // Arrow functions inherit `arguments` from enclosing function — don't stop at '=>'.
85
+ // Nested `function` introduces its own `arguments` — stop recursion there.
86
+ let argsIdx = 0
87
+ let doIdx = 0
88
+
89
+ function usesArguments(node) {
90
+ if (node === 'arguments') return true
91
+ if (!Array.isArray(node)) return false
92
+ if (node[0] === 'function') return false
93
+ if (node[0] === '.' || node[0] === '?.') return usesArguments(node[1])
94
+ if (node[0] === ':') return usesArguments(node[2])
95
+ for (let i = 1; i < node.length; i++) if (usesArguments(node[i])) return true
96
+ return false
97
+ }
98
+
99
+ function renameArguments(node, to) {
100
+ if (node === 'arguments') return to
101
+ if (!Array.isArray(node)) return node
102
+ if (node[0] === 'function') return node
103
+ if (node[0] === '.' || node[0] === '?.')
104
+ return [node[0], renameArguments(node[1], to), node[2]]
105
+ if (node[0] === ':')
106
+ return [node[0], node[1], renameArguments(node[2], to)]
107
+ return node.map(n => renameArguments(n, to))
108
+ }
109
+
110
+ function paramList(params) {
111
+ if (params == null) return []
112
+ if (Array.isArray(params)) {
113
+ if (params[0] === '()') {
114
+ const inner = params[1]
115
+ if (inner == null) return []
116
+ if (Array.isArray(inner) && inner[0] === ',') return inner.slice(1)
117
+ return [inner]
118
+ }
119
+ if (params[0] === ',') return params.slice(1)
120
+ }
121
+ return [params]
122
+ }
123
+
124
+ function lowerArguments(params, body) {
125
+ if (!usesArguments(body)) return [params, body]
126
+ const name = `\uE001arg${argsIdx++}`
127
+ const items = paramList(params)
128
+ items.push(['...', name])
129
+ const inner = items.length === 1 ? items[0] : [',', ...items]
130
+ return [['()', inner], renameArguments(body, name)]
131
+ }
132
+
133
+ const handlers = {
134
+ // Named IIFE: (function name(p){b})(a) → let name = arrow; name(a)
135
+ '()'(callee, ...rest) {
136
+ if (Array.isArray(callee) && callee[0] === '()' && Array.isArray(callee[1]) && callee[1][0] === 'function' && callee[1][1]) {
137
+ const [, name, params, body] = callee[1]
138
+ const [p2, b2] = lowerArguments(params, body)
139
+ return [';', ['let', ['=', name, ['=>', p2, wrapArrowBody(b2)]]], ['()', name, ...rest.map(transform)]]
140
+ }
141
+ },
142
+
143
+ // function → arrow (named → hoisted const)
144
+ 'function'(name, params, body) {
145
+ const [p2, b2] = lowerArguments(params, body)
146
+ const arrow = ['=>', p2, wrapArrowBody(b2)]
147
+ if (name) { const decl = ['const', ['=', name, arrow]]; decl._hoisted = true; return decl }
148
+ return arrow
149
+ },
150
+
151
+ 'var'(...args) { return ['let', ...args.map(transform)] },
152
+
153
+ '='(lhs, rhs) {
154
+ // var assignment: ['=', ['var', name], init] → let
155
+ if (Array.isArray(lhs) && lhs[0] === 'var')
156
+ return ['let', ['=', lhs[1], transform(rhs)]]
157
+ // Chained property assignment: a.x = a.y = v → a.y = v; a.x = v
158
+ if (Array.isArray(lhs) && lhs[0] === '.' && Array.isArray(rhs) && rhs[0] === '=') {
159
+ const targets = []
160
+ let cur = ['=', lhs, rhs]
161
+ while (Array.isArray(cur) && cur[0] === '=') { targets.push(cur[1]); cur = cur[2] }
162
+ const val = transform(cur)
163
+ const stmts = []
164
+ for (let i = targets.length - 1; i >= 0; i--) stmts.push(['=', transform(targets[i]), val])
165
+ return stmts.length === 1 ? stmts[0] : [';', ...stmts]
166
+ }
167
+ },
168
+
169
+ 'switch'(disc, ...cases) {
170
+ const clean = cases.map(c => {
171
+ if (c[0] === 'case' && Array.isArray(c[2]) && c[2][0] === ';') {
172
+ const body = c[2].slice(1).filter(s => typeof s !== 'number')
173
+ return ['case', c[1], body.length === 1 ? body[0] : [';', ...body]]
174
+ }
175
+ if (c[0] === 'default' && Array.isArray(c[1]) && c[1][0] === ';') {
176
+ const body = c[1].slice(1).filter(s => s != null && typeof s !== 'number')
177
+ return ['default', body.length === 1 ? body[0] : [';', ...body]]
178
+ }
179
+ return c
180
+ })
181
+ return transformSwitch(disc, clean)
182
+ },
183
+
184
+ // == → ===, != → !== (with prototype identity folding)
185
+ '=='(a, b) { return isProto(a) || isProto(b) ? 1 : ['===', transform(a), transform(b)] },
186
+ '!='(a, b) { return isProto(a) || isProto(b) ? 0 : ['!==', transform(a), transform(b)] },
187
+ '==='(a, b) { if (isProto(a) || isProto(b)) return 1 },
188
+ '!=='(a, b) { if (isProto(a) || isProto(b)) return 0 },
189
+
190
+ // new → call (keep TypedArrays)
191
+ 'new'(ctor, ...cargs) {
192
+ const name = typeof ctor === 'string' ? ctor : (Array.isArray(ctor) && ctor[0] === '()' ? ctor[1] : null)
193
+ if (typeof name === 'string' && TYPED_ARRAYS.has(name)) return ['new', transform(ctor), ...cargs.map(transform)]
194
+ if (Array.isArray(ctor) && ctor[0] === '()') return transform(ctor)
195
+ return ['()', transform(ctor), ...cargs.map(transform)]
196
+ },
197
+
198
+ // instanceof → typeof / Array.isArray (jzify allows what strict mode prohibits)
199
+ 'instanceof'(val, ctor) {
200
+ const t = transform(val)
201
+ const name = typeof ctor === 'string' ? ctor : (Array.isArray(ctor) && ctor[0] === '()' ? ctor[1] : null)
202
+ if (name === 'Array') return ['()', ['.', 'Array', 'isArray'], t]
203
+ if (name === 'Object') return ['===', ['typeof', t], [null, 'object']]
204
+ if (typeof name === 'string' && TYPED_ARRAYS.has(name)) return ['===', ['typeof', t], [null, 'object']]
205
+ return ['===', ['typeof', t], [null, 'object']]
206
+ },
207
+
208
+ // do { body } while (cond) → for (let _once = true; _once || cond; _once = false) body
209
+ // Avoids body duplication; matches JS continue semantics (continue runs cond, not body).
210
+ 'do'(body, cond) {
211
+ const flag = `do${doIdx++}`
212
+ return ['for',
213
+ [';',
214
+ ['let', ['=', flag, [null, true]]],
215
+ ['||', flag, transform(cond)],
216
+ ['=', flag, [null, false]]],
217
+ transform(body)]
218
+ },
219
+
220
+ // Block body: recurse as scope for hoisting
221
+ '{}'(...args) { return ['{}', ...args.map(a => transformScope(a) ?? a)] },
222
+
223
+ // Export: recurse into exported declaration
224
+ 'export'(inner) {
225
+ if (Array.isArray(inner) && inner[0] === 'default' && Array.isArray(inner[1]) && inner[1][0] === 'function' && inner[1][1]) {
226
+ const decl = transform(inner[1])
227
+ return [';', decl, ['export', ['default', inner[1][1]]]]
228
+ }
229
+ return ['export', transform(inner)]
230
+ },
231
+ }
232
+
233
+ /** Transform a single AST node recursively. */
234
+ function transform(node) {
235
+ if (node == null || typeof node !== 'object' || !Array.isArray(node)) return node
236
+ const [op, ...args] = node
237
+ if (op == null) return node
238
+ const h = handlers[op]
239
+ return (h && h(...args)) ?? (h ? [op, ...args.map(transform)] : [op, ...args.map(transform)])
240
+ }
241
+
242
+ /** Transform switch statement to if/else chain. */
243
+ let swIdx = 0
244
+ function transformSwitch(discriminant, cases) {
245
+ const disc = transform(discriminant)
246
+ const tmp = `\uE000sw${swIdx++}`
247
+
248
+ // Collect case/default
249
+ const stmts = [['let', ['=', tmp, disc]]]
250
+ let chain = null
251
+
252
+ for (let i = cases.length - 1; i >= 0; i--) {
253
+ const c = cases[i]
254
+ if (c[0] === 'default') {
255
+ chain = transform(c[1])
256
+ } else if (c[0] === 'case') {
257
+ const cond = ['===', tmp, transform(c[1])]
258
+ const body = transform(c[2])
259
+ chain = chain != null ? ['if', cond, body, chain] : ['if', cond, body]
260
+ }
261
+ }
262
+ if (chain) stmts.push(chain)
263
+ return [';', ...stmts]
264
+ }
265
+
266
+ // === AST → jz source codegen ===
267
+
268
+ const INDENT = ' '
269
+ const prec = { '=': 1, '+=': 1, '-=': 1, '*=': 1, '/=': 1, '%=': 1, '&=': 1, '|=': 1, '^=': 1, '>>=': 1, '<<=': 1, '>>>=': 1, '||=': 1, '&&=': 1,
270
+ '??': 2, '||': 3, '&&': 4, '|': 5, '^': 6, '&': 7, '===': 8, '!==': 8, '==': 8, '!=': 8,
271
+ '<': 9, '>': 9, '<=': 9, '>=': 9, '<<': 10, '>>': 10, '>>>': 10,
272
+ '+': 11, '-': 11, '*': 12, '/': 12, '%': 12, '**': 13 }
273
+
274
+ /** Wrap statement in { } if not already a block */
275
+ function wrapBlock(node, depth) {
276
+ if (Array.isArray(node) && node[0] === '{}') return codegen(node, depth)
277
+ return '{ ' + codegen(node, depth) + '; }'
278
+ }
279
+
280
+ /** Generate jz source from AST. Enforces semicolons. */
281
+ export function codegen(node, depth = 0) {
282
+ if (node == null) return ''
283
+ if (typeof node === 'number') return String(node)
284
+ if (typeof node === 'bigint') return node + 'n'
285
+ if (typeof node === 'string') return node
286
+ if (!Array.isArray(node)) return String(node)
287
+
288
+ const [op, ...a] = node
289
+ const ind = INDENT.repeat(depth), ind1 = INDENT.repeat(depth + 1)
290
+
291
+ // Literal: [, value]
292
+ if (op == null) return typeof a[0] === 'string' ? JSON.stringify(a[0]) : a[0] == null ? 'null' : String(a[0]) + (typeof a[0] === 'bigint' ? 'n' : '')
293
+
294
+ // Statements
295
+ if (op === ';') return a.map(s => codegen(s, depth)).filter(Boolean).join(';\n' + ind) + ';'
296
+ if (op === '{}') {
297
+ const body = a.map(s => codegen(s, depth + 1)).filter(Boolean).join(';\n' + ind1)
298
+ return '{\n' + ind1 + body + (body ? ';' : '') + '\n' + ind + '}'
299
+ }
300
+
301
+ // Declarations
302
+ if (op === 'let' || op === 'const') return op + ' ' + a.map(d => codegen(d, depth)).join(', ')
303
+ if (op === 'export') { const inner = codegen(a[0], depth); return inner ? 'export ' + inner : '' }
304
+ if (op === 'default') return 'default ' + codegen(a[0], depth)
305
+
306
+ // Control flow
307
+ if (op === 'if') {
308
+ const cond = codegen(a[0]), then = wrapBlock(a[1], depth)
309
+ return a[2] != null
310
+ ? 'if (' + cond + ') ' + then + ' else ' + wrapBlock(a[2], depth)
311
+ : 'if (' + cond + ') ' + then
312
+ }
313
+ if (op === 'while') return 'while (' + codegen(a[0]) + ') ' + wrapBlock(a[1], depth)
314
+ if (op === 'for') {
315
+ if (a.length === 2) { // for...of / for...in
316
+ const [head, body] = a
317
+ if (Array.isArray(head) && (head[0] === 'of' || head[0] === 'in'))
318
+ return 'for (' + codegen(head[1]) + ' ' + head[0] + ' ' + codegen(head[2]) + ') ' + codegen(body, depth)
319
+ return 'for (' + codegen(head) + ') ' + codegen(body, depth)
320
+ }
321
+ return 'for (' + (codegen(a[0]) || '') + '; ' + (codegen(a[1]) || '') + '; ' + (codegen(a[2]) || '') + ') ' + codegen(a[3], depth)
322
+ }
323
+ if (op === 'return') return 'return ' + codegen(a[0])
324
+ if (op === 'throw') return 'throw ' + codegen(a[0])
325
+ if (op === 'break') return 'break'
326
+ if (op === 'continue') return 'continue'
327
+ if (op === 'catch') return 'try ' + codegen(a[0], depth) + ' catch (' + a[1] + ') ' + codegen(a[2], depth)
328
+
329
+ // Arrow
330
+ if (op === '=>') {
331
+ // Params: already wrapped in () by parser, or bare name
332
+ const p = a[0]
333
+ const params = Array.isArray(p) && p[0] === '()' ? codegen(p) : '(' + codegen(p) + ')'
334
+ const body = a[1]
335
+ const isBlock = Array.isArray(body) && (body[0] === '{}' || body[0] === ';' || body[0] === 'return')
336
+ const bodyStr = Array.isArray(body) && body[0] !== '{}' && isBlock
337
+ ? '{ ' + codegen(body, depth) + '; }'
338
+ : codegen(body, depth)
339
+ return params + ' => ' + bodyStr
340
+ }
341
+
342
+ // Grouping parens / function call
343
+ if (op === '()') {
344
+ if (a.length === 1) return '(' + (a[0] == null ? '' : codegen(a[0])) + ')'
345
+ return codegen(a[0]) + '(' + a.slice(1).map(x => codegen(x)).join(', ') + ')'
346
+ }
347
+
348
+ // Property access
349
+ if (op === '.') return codegen(a[0]) + '.' + a[1]
350
+ if (op === '?.') return codegen(a[0]) + '?.' + a[1]
351
+ if (op === '[]') return codegen(a[0]) + '[' + codegen(a[1]) + ']'
352
+
353
+ // Array/object literals
354
+ if (op === '[') return '[' + a.map(x => codegen(x)).join(', ') + ']'
355
+ if (op === ':') return codegen(a[0]) + ': ' + codegen(a[1])
356
+ if (op === 'str') return JSON.stringify(a[0])
357
+ if (op === '//') return '/' + a[0] + '/' + (a[1] || '')
358
+
359
+ // Comma
360
+ if (op === ',') return a.map(x => codegen(x)).join(', ')
361
+ // Template literal
362
+ if (op === '`') return '`' + a.map((p, i) => i % 2 ? '${' + codegen(p) + '}' : (p?.[1] ?? '')).join('') + '`'
363
+
364
+ // Spread
365
+ if (op === '...') return '...' + codegen(a[0])
366
+
367
+ // Import
368
+ if (op === 'import') return 'import ' + codegen(a[0])
369
+ if (op === 'from') return codegen(a[0]) + ' from ' + codegen(a[1])
370
+
371
+ // Unary prefix
372
+ if (a.length === 1) {
373
+ if (op === '++' || op === '--') return a[0] == null ? op : op + codegen(a[0])
374
+ if (op === 'typeof') return 'typeof ' + codegen(a[0])
375
+ if (op === 'u-') return '-' + codegen(a[0])
376
+ if (op === 'u+') return '+' + codegen(a[0])
377
+ return op + codegen(a[0])
378
+ }
379
+
380
+ // Postfix
381
+ if (a.length === 2 && a[1] === null) return codegen(a[0]) + op
382
+
383
+ // Binary
384
+ if (a.length === 2 && prec[op]) return codegen(a[0]) + ' ' + op + ' ' + codegen(a[1])
385
+
386
+ // Ternary
387
+ if (op === '?' || op === '?:') return codegen(a[0]) + ' ? ' + codegen(a[1]) + ' : ' + codegen(a[2])
388
+
389
+ // Fallback
390
+ return op + '(' + a.map(x => codegen(x)).join(', ') + ')'
391
+ }