jz 0.0.0 → 0.1.1

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,418 @@
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(params) && !usesArguments(body)) return [params, body]
126
+ const name = `\uE001arg${argsIdx++}`
127
+ const decls = []
128
+ for (const [idx, param] of paramList(params).entries()) {
129
+ if (Array.isArray(param) && param[0] === '...') {
130
+ decls.push(['=', param[1], ['()', ['.', name, 'slice'], [null, idx]]])
131
+ continue
132
+ }
133
+ if (Array.isArray(param) && param[0] === '=') {
134
+ decls.push(['=', param[1], ['??', ['[]', name, [null, idx]], renameArguments(param[2], name)]])
135
+ continue
136
+ }
137
+ decls.push(['=', param, ['[]', name, [null, idx]]])
138
+ }
139
+ const renamed = renameArguments(body, name)
140
+ return [['()', ['...', name]], decls.length ? [';', ['let', ...decls], renamed] : renamed]
141
+ }
142
+
143
+ const arrowParams = params => Array.isArray(params) && params[0] === '()' ? params : ['()', params]
144
+
145
+ const handlers = {
146
+ // Named IIFE: (function name(p){b})(a) → let name = arrow; name(a)
147
+ '()'(callee, ...rest) {
148
+ if (Array.isArray(callee) && callee[0] === '()' && Array.isArray(callee[1]) && callee[1][0] === 'function' && callee[1][1]) {
149
+ const [, name, params, body] = callee[1]
150
+ const [p2, b2] = lowerArguments(params, body)
151
+ return [';', ['let', ['=', name, ['=>', arrowParams(p2), wrapArrowBody(b2)]]], ['()', name, ...rest.map(transform)]]
152
+ }
153
+ },
154
+
155
+ // function → arrow (named → hoisted const)
156
+ 'function'(name, params, body) {
157
+ const [p2, b2] = lowerArguments(params, body)
158
+ const arrow = ['=>', p2, wrapArrowBody(b2)]
159
+ if (name) { const decl = ['const', ['=', name, arrow]]; decl._hoisted = true; return decl }
160
+ return arrow
161
+ },
162
+
163
+ 'var'(...args) {
164
+ // for-in/for-of: ['var', ['in', 'k', obj]] → ['in', ['let', 'k'], obj]
165
+ if (args.length === 1 && Array.isArray(args[0]) && (args[0][0] === 'in' || args[0][0] === 'of')) {
166
+ const [, name, src] = args[0]
167
+ return [args[0][0], ['let', typeof name === 'string' ? name : transform(name)], transform(src)]
168
+ }
169
+ return ['let', ...args.map(transform)]
170
+ },
171
+
172
+ '='(lhs, rhs) {
173
+ // var assignment: ['=', ['var', name], init] → let
174
+ if (Array.isArray(lhs) && lhs[0] === 'var')
175
+ return ['let', ['=', lhs[1], transform(rhs)]]
176
+ // Chained property assignment: a.x = a.y = v → a.y = v; a.x = v
177
+ if (Array.isArray(lhs) && lhs[0] === '.' && Array.isArray(rhs) && rhs[0] === '=') {
178
+ const targets = []
179
+ let cur = ['=', lhs, rhs]
180
+ while (Array.isArray(cur) && cur[0] === '=') { targets.push(cur[1]); cur = cur[2] }
181
+ const val = transform(cur)
182
+ const stmts = []
183
+ for (let i = targets.length - 1; i >= 0; i--) stmts.push(['=', transform(targets[i]), val])
184
+ return stmts.length === 1 ? stmts[0] : [';', ...stmts]
185
+ }
186
+ },
187
+
188
+ 'switch'(disc, ...cases) {
189
+ const clean = cases.map(c => {
190
+ if (c[0] === 'case' && Array.isArray(c[2]) && c[2][0] === ';') {
191
+ const body = c[2].slice(1).filter(s => typeof s !== 'number')
192
+ return ['case', c[1], body.length === 1 ? body[0] : [';', ...body]]
193
+ }
194
+ if (c[0] === 'default' && Array.isArray(c[1]) && c[1][0] === ';') {
195
+ const body = c[1].slice(1).filter(s => s != null && typeof s !== 'number')
196
+ return ['default', body.length === 1 ? body[0] : [';', ...body]]
197
+ }
198
+ return c
199
+ })
200
+ return transformSwitch(disc, clean)
201
+ },
202
+
203
+ // == → ===, != → !== (with prototype identity folding)
204
+ '=='(a, b) { return isProto(a) || isProto(b) ? 1 : ['===', transform(a), transform(b)] },
205
+ '!='(a, b) { return isProto(a) || isProto(b) ? 0 : ['!==', transform(a), transform(b)] },
206
+ '==='(a, b) { if (isProto(a) || isProto(b)) return 1 },
207
+ '!=='(a, b) { if (isProto(a) || isProto(b)) return 0 },
208
+
209
+ // new → call (keep TypedArrays)
210
+ 'new'(ctor, ...cargs) {
211
+ const name = typeof ctor === 'string' ? ctor : (Array.isArray(ctor) && ctor[0] === '()' ? ctor[1] : null)
212
+ if (typeof name === 'string' && TYPED_ARRAYS.has(name)) return ['new', transform(ctor), ...cargs.map(transform)]
213
+ if (Array.isArray(ctor) && ctor[0] === '()') return transform(ctor)
214
+ return ['()', transform(ctor), ...cargs.map(transform)]
215
+ },
216
+
217
+ // instanceof → typeof / Array.isArray (jzify allows what strict mode prohibits)
218
+ 'instanceof'(val, ctor) {
219
+ const t = transform(val)
220
+ const name = typeof ctor === 'string' ? ctor : (Array.isArray(ctor) && ctor[0] === '()' ? ctor[1] : null)
221
+ if (name === 'Array') return ['()', ['.', 'Array', 'isArray'], t]
222
+ if (name === 'Object') return ['===', ['typeof', t], [null, 'object']]
223
+ if (typeof name === 'string' && TYPED_ARRAYS.has(name)) return ['===', ['typeof', t], [null, 'object']]
224
+ return ['===', ['typeof', t], [null, 'object']]
225
+ },
226
+
227
+ // do { body } while (cond) → let _once = true; while (_once || cond) { _once = false; body }
228
+ // Avoids body duplication and preserves continue: `continue` jumps back to the
229
+ // while condition after the one-shot flag has been cleared.
230
+ 'do'(body, cond) {
231
+ const flag = `do${doIdx++}`
232
+ return [';',
233
+ ['let', ['=', flag, [null, true]]],
234
+ ['while', ['||', flag, transform(cond)], ['{}', [';', ['=', flag, [null, false]], transform(body)]]]]
235
+ },
236
+
237
+ // Block body: recurse as scope for hoisting
238
+ '{}'(...args) { return ['{}', ...args.map(a => transformScope(a) ?? a)] },
239
+
240
+ // Export: recurse into exported declaration
241
+ 'export'(inner) {
242
+ if (Array.isArray(inner) && inner[0] === 'default' && Array.isArray(inner[1]) && inner[1][0] === 'function' && inner[1][1]) {
243
+ const decl = transform(inner[1])
244
+ return [';', decl, ['export', ['default', inner[1][1]]]]
245
+ }
246
+ return ['export', transform(inner)]
247
+ },
248
+ }
249
+
250
+ /** Transform a single AST node recursively. */
251
+ function transform(node) {
252
+ if (node == null || typeof node !== 'object' || !Array.isArray(node)) return node
253
+ const [op, ...args] = node
254
+ if (op == null) return node
255
+ const h = handlers[op]
256
+ return (h && h(...args)) ?? (h ? [op, ...args.map(transform)] : [op, ...args.map(transform)])
257
+ }
258
+
259
+ /** Transform switch statement to if/else chain. */
260
+ let swIdx = 0
261
+ function transformSwitch(discriminant, cases) {
262
+ const disc = transform(discriminant)
263
+ const tmp = `\uE000sw${swIdx++}`
264
+
265
+ // Collect case/default
266
+ const stmts = [['let', ['=', tmp, disc]]]
267
+ let chain = null
268
+
269
+ for (let i = cases.length - 1; i >= 0; i--) {
270
+ const c = cases[i]
271
+ if (c[0] === 'default') {
272
+ chain = transform(c[1])
273
+ } else if (c[0] === 'case') {
274
+ const cond = ['===', tmp, transform(c[1])]
275
+ const body = transform(c[2])
276
+ chain = chain != null ? ['if', cond, body, chain] : ['if', cond, body]
277
+ }
278
+ }
279
+ if (chain) stmts.push(chain)
280
+ return [';', ...stmts]
281
+ }
282
+
283
+ // === AST → jz source codegen ===
284
+
285
+ const INDENT = ' '
286
+ const prec = { '=': 1, '+=': 1, '-=': 1, '*=': 1, '/=': 1, '%=': 1, '&=': 1, '|=': 1, '^=': 1, '>>=': 1, '<<=': 1, '>>>=': 1, '||=': 1, '&&=': 1,
287
+ '??': 2, '||': 3, '&&': 4, '|': 5, '^': 6, '&': 7, '===': 8, '!==': 8, '==': 8, '!=': 8,
288
+ '<': 9, '>': 9, '<=': 9, '>=': 9, '<<': 10, '>>': 10, '>>>': 10,
289
+ '+': 11, '-': 11, '*': 12, '/': 12, '%': 12, '**': 13 }
290
+
291
+ /** Wrap statement in { } if not already a block */
292
+ function wrapBlock(node, depth) {
293
+ if (Array.isArray(node) && node[0] === '{}') return codegen(node, depth)
294
+ return '{ ' + codegen(node, depth) + '; }'
295
+ }
296
+
297
+ /** Generate jz source from AST. Enforces semicolons. */
298
+ export function codegen(node, depth = 0) {
299
+ if (node == null) return ''
300
+ if (typeof node === 'number') return String(node)
301
+ if (typeof node === 'bigint') return node + 'n'
302
+ if (typeof node === 'string') return node
303
+ if (!Array.isArray(node)) return String(node)
304
+
305
+ const [op, ...a] = node
306
+ const ind = INDENT.repeat(depth), ind1 = INDENT.repeat(depth + 1)
307
+
308
+ // Literal: [, value]
309
+ if (op == null) return typeof a[0] === 'string' ? JSON.stringify(a[0]) : a[0] == null ? 'null' : String(a[0]) + (typeof a[0] === 'bigint' ? 'n' : '')
310
+
311
+ // Statements
312
+ if (op === ';') return a.map(s => codegen(s, depth)).filter(Boolean).join(';\n' + ind) + ';'
313
+ if (op === '{}') {
314
+ const body = a.map(s => codegen(s, depth + 1)).filter(Boolean).join(';\n' + ind1)
315
+ return '{\n' + ind1 + body + (body ? ';' : '') + '\n' + ind + '}'
316
+ }
317
+
318
+ // Declarations
319
+ if (op === 'let' || op === 'const') return op + ' ' + a.map(d => codegen(d, depth)).join(', ')
320
+ if (op === 'export') { const inner = codegen(a[0], depth); return inner ? 'export ' + inner : '' }
321
+ if (op === 'default') return 'default ' + codegen(a[0], depth)
322
+
323
+ // Control flow
324
+ if (op === 'if') {
325
+ const cond = codegen(a[0]), then = wrapBlock(a[1], depth)
326
+ return a[2] != null
327
+ ? 'if (' + cond + ') ' + then + ' else ' + wrapBlock(a[2], depth)
328
+ : 'if (' + cond + ') ' + then
329
+ }
330
+ if (op === 'while') return 'while (' + codegen(a[0]) + ') ' + wrapBlock(a[1], depth)
331
+ if (op === 'for') {
332
+ if (a.length === 2) { // for...of / for...in
333
+ const [head, body] = a
334
+ if (Array.isArray(head) && (head[0] === 'of' || head[0] === 'in'))
335
+ return 'for (' + codegen(head[1]) + ' ' + head[0] + ' ' + codegen(head[2]) + ') ' + codegen(body, depth)
336
+ // ['let'/'const', ['in'/'of', name, obj]] — subscript wraps var→let around in/of
337
+ if (Array.isArray(head) && (head[0] === 'let' || head[0] === 'const') && Array.isArray(head[1]) && (head[1][0] === 'in' || head[1][0] === 'of'))
338
+ return 'for (' + head[0] + ' ' + codegen(head[1][1]) + ' ' + head[1][0] + ' ' + codegen(head[1][2]) + ') ' + codegen(body, depth)
339
+ return 'for (' + codegen(head) + ') ' + codegen(body, depth)
340
+ }
341
+ return 'for (' + (codegen(a[0]) || '') + '; ' + (codegen(a[1]) || '') + '; ' + (codegen(a[2]) || '') + ') ' + codegen(a[3], depth)
342
+ }
343
+ if (op === 'return') return 'return ' + codegen(a[0])
344
+ if (op === 'throw') return 'throw ' + codegen(a[0])
345
+ if (op === 'break') return 'break'
346
+ if (op === 'continue') return 'continue'
347
+ // catch with optional binding: ['catch', tryBlock, catchBody] or ['catch', tryBlock, paramName, catchBody]
348
+ if (op === 'catch') {
349
+ if (a.length === 3) return 'try ' + codegen(a[0], depth) + ' catch (' + a[1] + ') ' + codegen(a[2], depth)
350
+ return 'try ' + codegen(a[0], depth) + ' catch ' + codegen(a[1], depth)
351
+ }
352
+
353
+ // Arrow
354
+ if (op === '=>') {
355
+ // Params: already wrapped in () by parser, or bare name
356
+ const p = a[0]
357
+ const params = Array.isArray(p) && p[0] === '()' ? codegen(p) : '(' + codegen(p) + ')'
358
+ const body = a[1]
359
+ const isBlock = Array.isArray(body) && (body[0] === '{}' || body[0] === ';' || body[0] === 'return')
360
+ const bodyStr = Array.isArray(body) && body[0] !== '{}' && isBlock
361
+ ? '{ ' + codegen(body, depth) + '; }'
362
+ : codegen(body, depth)
363
+ return params + ' => ' + bodyStr
364
+ }
365
+
366
+ // Grouping parens / function call
367
+ if (op === '()') {
368
+ if (a.length === 1) return '(' + (a[0] == null ? '' : codegen(a[0])) + ')'
369
+ return codegen(a[0]) + '(' + a.slice(1).map(x => codegen(x)).join(', ') + ')'
370
+ }
371
+
372
+ // Property access
373
+ if (op === '.') return codegen(a[0]) + '.' + a[1]
374
+ if (op === '?.') return codegen(a[0]) + '?.' + a[1]
375
+ if (op === '[]') return codegen(a[0]) + '[' + codegen(a[1]) + ']'
376
+
377
+ // Array/object literals
378
+ if (op === '[') return '[' + a.map(x => codegen(x)).join(', ') + ']'
379
+ if (op === ':') return codegen(a[0]) + ': ' + codegen(a[1])
380
+ if (op === 'str') return JSON.stringify(a[0])
381
+ if (op === '//') return '/' + a[0] + '/' + (a[1] || '')
382
+
383
+ // Comma
384
+ if (op === ',') return a.map(x => codegen(x)).join(', ')
385
+ // Template literal: alternating string/expr parts. String parts are [null, "str"], expr parts are AST nodes.
386
+ if (op === '`') return '`' + a.map(p => {
387
+ if (Array.isArray(p) && p[0] == null && typeof p[1] === 'string') return p[1].replace(/[`\\$]/g, c => '\\' + c)
388
+ return '${' + codegen(p) + '}'
389
+ }).join('') + '`'
390
+
391
+ // Spread
392
+ if (op === '...') return '...' + codegen(a[0])
393
+
394
+ // Import
395
+ if (op === 'import') return 'import ' + codegen(a[0])
396
+ if (op === 'from') return codegen(a[0]) + ' from ' + codegen(a[1])
397
+
398
+ // Unary prefix
399
+ if (a.length === 1) {
400
+ if (op === '++' || op === '--') return a[0] == null ? op : op + codegen(a[0])
401
+ if (op === 'typeof') return 'typeof ' + codegen(a[0])
402
+ if (op === 'u-') return '-' + codegen(a[0])
403
+ if (op === 'u+') return '+' + codegen(a[0])
404
+ return op + codegen(a[0])
405
+ }
406
+
407
+ // Postfix
408
+ if (a.length === 2 && a[1] === null) return codegen(a[0]) + op
409
+
410
+ // Binary
411
+ if (a.length === 2 && prec[op]) return codegen(a[0]) + ' ' + op + ' ' + codegen(a[1])
412
+
413
+ // Ternary
414
+ if (op === '?' || op === '?:') return codegen(a[0]) + ' ? ' + codegen(a[1]) + ' : ' + codegen(a[2])
415
+
416
+ // Fallback
417
+ return op + '(' + a.map(x => codegen(x)).join(', ') + ')'
418
+ }
package/src/key.js ADDED
@@ -0,0 +1,73 @@
1
+ /** Static property-key evaluation for computed member names. */
2
+
3
+ const NO_VALUE = Symbol('no-static-property-key')
4
+
5
+ export function staticPropertyKey(node) {
6
+ const value = staticValue(node)
7
+ return value === NO_VALUE ? null : String(value)
8
+ }
9
+
10
+ function staticValue(node) {
11
+ if (node === undefined) return undefined
12
+ if (node === null || typeof node === 'number' || typeof node === 'string' || typeof node === 'boolean') return node
13
+ if (!Array.isArray(node)) return NO_VALUE
14
+
15
+ const [op, ...args] = node
16
+ if (op == null) return args.length ? args[0] : undefined
17
+ if (op === 'str') return args[0]
18
+ if (op === '[]' && args.length === 1) return staticValue(args[0])
19
+ if (op === '()' && args[0] === 'String' && args.length === 2) {
20
+ const value = staticValue(args[1])
21
+ return value === NO_VALUE ? NO_VALUE : String(value)
22
+ }
23
+ if (op === '()' && args[0] === 'Number' && args.length === 2) {
24
+ const value = staticValue(args[1])
25
+ return value === NO_VALUE ? NO_VALUE : Number(value)
26
+ }
27
+ if (op === '?:' || op === '?') {
28
+ const cond = staticValue(args[0])
29
+ return cond === NO_VALUE ? NO_VALUE : staticValue(cond ? args[1] : args[2])
30
+ }
31
+ if (op === '&&' || op === '||') {
32
+ const left = staticValue(args[0])
33
+ if (left === NO_VALUE) return NO_VALUE
34
+ return op === '&&' ? (left ? staticValue(args[1]) : left) : (left ? left : staticValue(args[1]))
35
+ }
36
+ if (op === '??') {
37
+ const left = staticValue(args[0])
38
+ return left === NO_VALUE ? NO_VALUE : left == null ? staticValue(args[1]) : left
39
+ }
40
+
41
+ if (args.length === 1) {
42
+ const value = staticValue(args[0])
43
+ if (value === NO_VALUE) return NO_VALUE
44
+ if (op === 'u+') return +value
45
+ if (op === 'u-') return -value
46
+ if (op === '!') return !value
47
+ if (op === '~') return ~value
48
+ return NO_VALUE
49
+ }
50
+
51
+ if (args.length === 2) {
52
+ const left = staticValue(args[0])
53
+ const right = staticValue(args[1])
54
+ if (left === NO_VALUE || right === NO_VALUE) return NO_VALUE
55
+ switch (op) {
56
+ case '+': return typeof left === 'string' || typeof right === 'string' ? String(left) + String(right) : Number(left) + Number(right)
57
+ case '-': return Number(left) - Number(right)
58
+ case '*': return Number(left) * Number(right)
59
+ case '/': return Number(left) / Number(right)
60
+ case '%': return Number(left) % Number(right)
61
+ case '**': return Number(left) ** Number(right)
62
+ case '&': return Number(left) & Number(right)
63
+ case '|': return Number(left) | Number(right)
64
+ case '^': return Number(left) ^ Number(right)
65
+ case '<<': return Number(left) << Number(right)
66
+ case '>>': return Number(left) >> Number(right)
67
+ case '>>>': return Number(left) >>> Number(right)
68
+ default: return NO_VALUE
69
+ }
70
+ }
71
+
72
+ return NO_VALUE
73
+ }