jz 0.3.1 → 0.5.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/README.md +277 -258
- package/cli.js +10 -52
- package/index.js +181 -29
- package/{src/host.js → interop.js} +291 -88
- package/module/array.js +195 -76
- package/module/collection.js +249 -20
- package/module/console.js +1 -1
- package/module/core.js +267 -119
- package/module/date.js +9 -5
- package/module/function.js +11 -1
- package/module/json.js +367 -61
- package/module/math.js +568 -128
- package/module/number.js +390 -227
- package/module/object.js +352 -39
- package/module/regex.js +123 -16
- package/module/schema.js +15 -1
- package/module/string.js +555 -96
- package/module/timer.js +1 -1
- package/module/typedarray.js +674 -57
- package/package.json +12 -6
- package/src/abi/array.js +89 -0
- package/src/abi/index.js +35 -0
- package/src/abi/number.js +137 -0
- package/src/abi/object.js +57 -0
- package/src/abi/string.js +529 -0
- package/src/analyze.js +1872 -487
- package/src/assemble.js +58 -20
- package/src/autoload.js +13 -1
- package/src/codegen.js +177 -0
- package/src/compile.js +462 -186
- package/src/ctx.js +85 -18
- package/src/emit.js +668 -197
- package/src/infer.js +548 -0
- package/src/ir.js +302 -27
- package/src/jzify.js +898 -259
- package/src/narrow.js +455 -246
- package/src/optimize.js +263 -99
- package/src/plan.js +713 -35
- package/src/prepare.js +818 -293
- package/src/resolve.js +85 -0
- package/src/vectorize.js +99 -18
- package/src/ast.js +0 -160
- package/src/auto-config.js +0 -120
package/module/core.js
CHANGED
|
@@ -9,25 +9,17 @@
|
|
|
9
9
|
* @module core
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { typed, asF64, asI32, asI64, NULL_NAN, UNDEF_NAN, temp, usesDynProps, ptrOffsetIR, isNullish } from '../src/ir.js'
|
|
13
|
-
import { emit } from '../src/emit.js'
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
12
|
+
import { typed, asF64, asI32, asI64, NULL_NAN, UNDEF_NAN, temp, usesDynProps, ptrOffsetIR, isNullish, valKindToPtr } from '../src/ir.js'
|
|
13
|
+
import { emit, buildArrayWithSpreads } from '../src/emit.js'
|
|
14
|
+
import { reconstructArgsWithSpreads } from '../src/ir.js'
|
|
15
|
+
import { valTypeOf, lookupValType, lookupNotString, VAL, T, repOf, updateRep, shapeOf, inlineArraySid } from '../src/analyze.js'
|
|
16
|
+
import { ctx, err, inc, PTR, LAYOUT } from '../src/ctx.js'
|
|
16
17
|
import { initSchema } from './schema.js'
|
|
17
18
|
import { strHashLiteral } from './collection.js'
|
|
18
19
|
|
|
19
20
|
// Pre-shifted NaN prefix as a full i64 mask, for `(i64.const ${NAN_BITS})` use.
|
|
20
21
|
const NAN_BITS = '0x' + LAYOUT.NAN_PREFIX_BITS.toString(16).toUpperCase().padStart(16, '0')
|
|
21
22
|
|
|
22
|
-
const PTR_BY_VAL = {
|
|
23
|
-
[VAL.ARRAY]: PTR.ARRAY,
|
|
24
|
-
[VAL.OBJECT]: PTR.OBJECT,
|
|
25
|
-
[VAL.TYPED]: PTR.TYPED,
|
|
26
|
-
[VAL.SET]: PTR.SET,
|
|
27
|
-
[VAL.MAP]: PTR.MAP,
|
|
28
|
-
[VAL.CLOSURE]: PTR.CLOSURE,
|
|
29
|
-
}
|
|
30
|
-
|
|
31
23
|
export default (ctx) => {
|
|
32
24
|
Object.assign(ctx.core.stdlibDeps, {
|
|
33
25
|
__eq: ['__str_eq', '__ptr_type'],
|
|
@@ -42,6 +34,7 @@ export default (ctx) => {
|
|
|
42
34
|
__length: ['__ptr_type', '__ptr_offset', '__str_len', '__len'],
|
|
43
35
|
__typeof: ['__ptr_type', '__is_nullish'],
|
|
44
36
|
__alloc_hdr: ['__alloc'],
|
|
37
|
+
__alloc_hdr_n: ['__alloc'],
|
|
45
38
|
})
|
|
46
39
|
|
|
47
40
|
ctx.core.stdlib['__is_nullish'] = `(func $__is_nullish (param $v i64) (result i32)
|
|
@@ -53,7 +46,10 @@ export default (ctx) => {
|
|
|
53
46
|
(local $fa f64) (local $fb f64) (local $ta i32) (local $tb i32)
|
|
54
47
|
;; Fast path: bit equality covers identical pointers AND interned/SSO strings (same content
|
|
55
48
|
;; → same bits). Failing universal-NaN test catches NaN===NaN→false. Saves the NaN-check
|
|
56
|
-
;; pair (4 f64.eq) on the hottest case in watr (op === 'literal-string').
|
|
49
|
+
;; pair (4 f64.eq) on the hottest case in watr (op === 'literal-string'). A number-NaN is
|
|
50
|
+
;; *only ever* the canonical NAN_BITS here: math ops canonicalize at the source (the
|
|
51
|
+
;; canon helper in module/math.js), so a non-canonical 0xFFF8.. pattern can only be a
|
|
52
|
+
;; negative BigInt carrier — bit-identical to itself and correctly equal.
|
|
57
53
|
(if (result i32) (i64.eq (local.get $a) (local.get $b))
|
|
58
54
|
(then (i64.ne (local.get $a) (i64.const ${NAN_BITS})))
|
|
59
55
|
(else
|
|
@@ -154,6 +150,19 @@ export default (ctx) => {
|
|
|
154
150
|
ctx.core.stdlib['__ptr_type'] = `(func $__ptr_type (param $ptr i64) (result i32)
|
|
155
151
|
(i32.wrap_i64 (i64.and (i64.shr_u (local.get $ptr) (i64.const ${LAYOUT.TAG_SHIFT})) (i64.const ${LAYOUT.TAG_MASK}))))`
|
|
156
152
|
|
|
153
|
+
// True iff a NaN-boxed value is a non-primitive (heap object) — tag is neither
|
|
154
|
+
// ATOM (null/undefined/boolean/symbol) nor STRING. A genuine f64 Number is
|
|
155
|
+
// never NaN-boxed, so `f64.eq(x,x)` holding proves it a primitive. Drives the
|
|
156
|
+
// ES `OrdinaryToPrimitive` method-fallback chain (src/ir.js toPrimitiveChain).
|
|
157
|
+
ctx.core.stdlib['__is_object'] = `(func $__is_object (param $p i64) (result i32)
|
|
158
|
+
(local $t i32)
|
|
159
|
+
(if (f64.eq (f64.reinterpret_i64 (local.get $p)) (f64.reinterpret_i64 (local.get $p)))
|
|
160
|
+
(then (return (i32.const 0))))
|
|
161
|
+
(local.set $t (call $__ptr_type (local.get $p)))
|
|
162
|
+
(i32.and
|
|
163
|
+
(i32.ne (local.get $t) (i32.const ${PTR.ATOM}))
|
|
164
|
+
(i32.ne (local.get $t) (i32.const ${PTR.STRING}))))`
|
|
165
|
+
|
|
157
166
|
// === Bump allocator ===
|
|
158
167
|
|
|
159
168
|
// Heap-base watermark: gates header-backed propsPtr fast paths so static-data
|
|
@@ -161,18 +170,38 @@ export default (ctx) => {
|
|
|
161
170
|
// Updated by optimizeModule() when data segment exceeds 1024 bytes.
|
|
162
171
|
ctx.scope.globals.set('__heap_start', '(global $__heap_start (mut i32) (i32.const 1024))')
|
|
163
172
|
|
|
173
|
+
// Shared memory keeps the heap pointer in linear memory (memory[1020]):
|
|
174
|
+
// wasm globals are per-instance, so threads sharing one memory must share one
|
|
175
|
+
// pointer cell. Non-shared memory (incl. alloc:false) uses the `$__heap`
|
|
176
|
+
// global — exported so the JS-side adapter (memory.String etc) bumps the same
|
|
177
|
+
// pointer. Storing it in memory would collide with the static data section
|
|
178
|
+
// whenever the data exceeds 1020 bytes.
|
|
164
179
|
if (ctx.memory.shared) {
|
|
165
|
-
//
|
|
180
|
+
// Heap offset stored at memory[1020] (i32), just before heap start at 1024.
|
|
181
|
+
// We still want to grow memory when running out of pages — without growth a
|
|
182
|
+
// host-run binary traps on the first allocation that exceeds the initial
|
|
183
|
+
// 64 KB. Use the same geometric-growth strategy as the owned-memory variant.
|
|
166
184
|
ctx.core.stdlib['__alloc'] = `(func $__alloc (param $bytes i32) (result i32)
|
|
167
|
-
(local $ptr i32)
|
|
185
|
+
(local $ptr i32) (local $next i32) (local $cur i32) (local $need i32)
|
|
168
186
|
(local.set $ptr (i32.load (i32.const 1020)))
|
|
169
|
-
(
|
|
187
|
+
(local.set $next (i32.and (i32.add (i32.add (local.get $ptr) (local.get $bytes)) (i32.const 7)) (i32.const -8)))
|
|
188
|
+
(local.set $cur (i32.shl (memory.size) (i32.const 16)))
|
|
189
|
+
(if (i32.gt_u (local.get $next) (local.get $cur))
|
|
190
|
+
(then
|
|
191
|
+
(local.set $need (i32.shr_u (i32.add (i32.sub (local.get $next) (local.get $cur)) (i32.const 65535)) (i32.const 16)))
|
|
192
|
+
(if (i32.lt_u (local.get $need) (memory.size)) (then (local.set $need (memory.size))))
|
|
193
|
+
(if (i32.eq (memory.grow (local.get $need)) (i32.const -1))
|
|
194
|
+
(then (if (i32.eq (memory.grow
|
|
195
|
+
(i32.shr_u (i32.add (i32.sub (local.get $next) (local.get $cur)) (i32.const 65535)) (i32.const 16)))
|
|
196
|
+
(i32.const -1)) (then (unreachable)))))))
|
|
197
|
+
(i32.store (i32.const 1020) (local.get $next))
|
|
170
198
|
(local.get $ptr))`
|
|
171
199
|
ctx.core.stdlib['__clear'] = `(func $__clear
|
|
172
200
|
(i32.store (i32.const 1020) (i32.const 1024)))`
|
|
173
201
|
} else {
|
|
174
|
-
// Own memory: heap offset in a global, auto-grow when needed
|
|
175
|
-
|
|
202
|
+
// Own memory: heap offset in a global, auto-grow when needed. Exported so
|
|
203
|
+
// the JS-side adapter (alloc:false, no `_alloc` export) shares the pointer.
|
|
204
|
+
ctx.scope.globals.set('__heap', '(global $__heap (export "__heap") (mut i32) (i32.const 1024))')
|
|
176
205
|
// Bump allocator with geometric growth. Growing one page at a time turns a
|
|
177
206
|
// long-running embedding (e.g. watr called thousands of times) into O(n²) —
|
|
178
207
|
// each memory.grow may relocate and copy the whole heap. So when we must
|
|
@@ -334,7 +363,21 @@ export default (ctx) => {
|
|
|
334
363
|
// (NaN-boxed PTR.HASH) for ARRAY/HASH/MAP/SET; 0 means "no dyn props yet". This lets
|
|
335
364
|
// __dyn_get_t / __dyn_set sidestep the global __dyn_props lookup on the hot path.
|
|
336
365
|
// Read offsets relative to the returned data ptr stay unchanged (-8 len, -4 cap).
|
|
337
|
-
|
|
366
|
+
// Default stride=8 (f64 NaN-boxed slot) — used by every Array/HASH/OBJECT alloc.
|
|
367
|
+
// Specialized over a generic (len, cap, stride) helper to drop a fat (i32.const 8)
|
|
368
|
+
// immediate at every call site (~20+) plus a param/local.get pair in the body.
|
|
369
|
+
// Non-8 strides (Set: 16, Map/HASH probe: 24, TypedArray raw: 1) use __alloc_hdr_n.
|
|
370
|
+
ctx.core.stdlib['__alloc_hdr'] = `(func $__alloc_hdr (param $len i32) (param $cap i32) (result i32)
|
|
371
|
+
(local $ptr i32)
|
|
372
|
+
(local.set $ptr (call $__alloc (i32.add (i32.const 16) (i32.shl (local.get $cap) (i32.const 3)))))
|
|
373
|
+
(i64.store (local.get $ptr) (i64.const 0))
|
|
374
|
+
(i32.store (i32.add (local.get $ptr) (i32.const 8)) (local.get $len))
|
|
375
|
+
(i32.store (i32.add (local.get $ptr) (i32.const 12)) (local.get $cap))
|
|
376
|
+
(i32.add (local.get $ptr) (i32.const 16)))`
|
|
377
|
+
|
|
378
|
+
// Generic header allocator for non-8 strides: Set (16), Map probe (24), TypedArray raw (1).
|
|
379
|
+
// Same 16-byte header layout as __alloc_hdr; per-entry stride is passed dynamically.
|
|
380
|
+
ctx.core.stdlib['__alloc_hdr_n'] = `(func $__alloc_hdr_n (param $len i32) (param $cap i32) (param $stride i32) (result i32)
|
|
338
381
|
(local $ptr i32)
|
|
339
382
|
(local.set $ptr (call $__alloc (i32.add (i32.const 16) (i32.mul (local.get $cap) (local.get $stride)))))
|
|
340
383
|
(i64.store (local.get $ptr) (i64.const 0))
|
|
@@ -368,18 +411,39 @@ export default (ctx) => {
|
|
|
368
411
|
* ARRAY/SET/MAP share a single layout: length is i32 at offset-8. We inline that load
|
|
369
412
|
* directly instead of calling __len which re-dispatches on type. ptrOffsetIR handles
|
|
370
413
|
* ARRAY forwarding (non-ARRAY skips the forwarding loop). TYPED has a variable-width
|
|
371
|
-
* layout depending on the aux typed-element shift, so it still routes through __len.
|
|
372
|
-
|
|
414
|
+
* layout depending on the aux typed-element shift, so it still routes through __len.
|
|
415
|
+
* `notString` (from rep.notString — write-shape evidence rules out primitive string)
|
|
416
|
+
* routes the otherwise-unknown case through __len directly, eliding the STRING arm
|
|
417
|
+
* of __length. __len returns 0 on tags it doesn't recognize, matching JS's
|
|
418
|
+
* `undefined` semantics on non-pointer .length (the binding writes through xs[i]
|
|
419
|
+
* / xs.length, so reaching .length with a non-pointer is unreachable in practice). */
|
|
420
|
+
function emitLengthAccess(va, vt, notString = false) {
|
|
421
|
+
// jsstring carrier: receiver is an externref slot (boundary param tagged
|
|
422
|
+
// `jsstring` by narrow.js phase J). Route to the `wasm:js-string` length
|
|
423
|
+
// builtin directly — no SSO unbox, zero copy.
|
|
424
|
+
if (va?.type === 'externref') {
|
|
425
|
+
ctx.core.jsstring.add('length')
|
|
426
|
+
return typed(['f64.convert_i32_s', ['call', '$__jss_length', va]], 'f64')
|
|
427
|
+
}
|
|
373
428
|
if (vt === VAL.ARRAY || vt === VAL.SET || vt === VAL.MAP) {
|
|
374
429
|
const off = ptrOffsetIR(va, vt)
|
|
375
430
|
return typed(['f64.convert_i32_s', ['i32.load', ['i32.sub', off, ['i32.const', 8]]]], 'f64')
|
|
376
431
|
}
|
|
377
432
|
if (vt === VAL.TYPED)
|
|
378
433
|
return typed(['f64.convert_i32_s', ['call', '$__len', ['i64.reinterpret_f64', va]]], 'f64')
|
|
379
|
-
// Known string → byteLen
|
|
434
|
+
// Known string → byteLen via the active string rep. Pass the slot
|
|
435
|
+
// carrier (f64 under nanbox-sso) — the rep op handles internal
|
|
436
|
+
// reinterpret/wrap. The `?.` call site passes a bare `['local.get', $t]`
|
|
437
|
+
// without a `.type` tag, so coerce defensively to f64.
|
|
380
438
|
if (vt === VAL.STRING) {
|
|
381
|
-
|
|
382
|
-
return typed(['f64.convert_i32_s',
|
|
439
|
+
const f64Va = va?.type === 'f64' ? va : typed(va, 'f64')
|
|
440
|
+
return typed(['f64.convert_i32_s', ctx.abi.string.ops.byteLen(f64Va, ctx)], 'f64')
|
|
441
|
+
}
|
|
442
|
+
// Unknown but proven not-string → __len directly (skips the STRING arm of __length).
|
|
443
|
+
if (notString) {
|
|
444
|
+
inc('__len')
|
|
445
|
+
ctx.features.typedarray = true
|
|
446
|
+
return typed(['f64.convert_i32_s', ['call', '$__len', ['i64.reinterpret_f64', va]]], 'f64')
|
|
383
447
|
}
|
|
384
448
|
// Unknown → runtime dispatch via stdlib. Set/Map dispatch arms are pulled
|
|
385
449
|
// only when user code actually constructs Set/Map (collection.js sets the
|
|
@@ -397,7 +461,7 @@ export default (ctx) => {
|
|
|
397
461
|
// (read on the AST `.prop` node), not via tagging this IR node.
|
|
398
462
|
function emitSchemaSlotRead(baseExpr, idx) {
|
|
399
463
|
const base = baseExpr?.type === 'f64' ? baseExpr : typed(baseExpr, 'f64')
|
|
400
|
-
return typed(
|
|
464
|
+
return typed(ctx.abi.object.ops.load(ptrOffsetIR(base, VAL.OBJECT), idx), 'f64')
|
|
401
465
|
}
|
|
402
466
|
|
|
403
467
|
function emitHashGetLocalConst(base, key, prop) {
|
|
@@ -407,7 +471,7 @@ export default (ctx) => {
|
|
|
407
471
|
}
|
|
408
472
|
|
|
409
473
|
function emitTypeTag(receiver, vt) {
|
|
410
|
-
const p =
|
|
474
|
+
const p = valKindToPtr(vt)
|
|
411
475
|
if (p != null) return ['i32.const', p]
|
|
412
476
|
inc('__ptr_type')
|
|
413
477
|
return ['call', '$__ptr_type', receiver]
|
|
@@ -425,9 +489,15 @@ export default (ctx) => {
|
|
|
425
489
|
return typed(['f64.reinterpret_i64', ['call', '$__dyn_get_expr_t', receiver, key, emitTypeTag(receiver, vt)]], 'f64')
|
|
426
490
|
}
|
|
427
491
|
|
|
428
|
-
function emitDynGetAnyTyped(base, key, vt) {
|
|
429
|
-
inc('__dyn_get_any_t')
|
|
492
|
+
function emitDynGetAnyTyped(base, key, vt, prop) {
|
|
430
493
|
const receiver = asI64(base?.type ? base : typed(base, 'f64'))
|
|
494
|
+
// Constant string key: fold the FNV hash at compile time and call the
|
|
495
|
+
// prehashed body — no __str_hash on every access (hot for `parse.step` etc).
|
|
496
|
+
if (typeof prop === 'string') {
|
|
497
|
+
inc('__dyn_get_any_t_h')
|
|
498
|
+
return typed(['f64.reinterpret_i64', ['call', '$__dyn_get_any_t_h', receiver, key, emitTypeTag(receiver, vt), ['i32.const', strHashLiteral(prop)]]], 'f64')
|
|
499
|
+
}
|
|
500
|
+
inc('__dyn_get_any_t')
|
|
431
501
|
return typed(['f64.reinterpret_i64', ['call', '$__dyn_get_any_t', receiver, key, emitTypeTag(receiver, vt)]], 'f64')
|
|
432
502
|
}
|
|
433
503
|
|
|
@@ -484,6 +554,15 @@ export default (ctx) => {
|
|
|
484
554
|
// (`{...x}`) shift slot ordering and would need their own resolution.
|
|
485
555
|
const slot = literalSlot(obj, prop)
|
|
486
556
|
if (slot >= 0) return emitSchemaSlotRead(asF64(va), slot)
|
|
557
|
+
// Receiver IR is an unboxed OBJECT pointer carrying its own schema (a
|
|
558
|
+
// structInline element cell, a narrowed local): resolve the field's fixed
|
|
559
|
+
// slot directly from `ptrAux` — more precise than the structural
|
|
560
|
+
// `ctx.schema.find(null, …)` and never falls to the dyn dispatcher.
|
|
561
|
+
if (va?.ptrKind === VAL.OBJECT && va.ptrAux != null && typeof prop === 'string') {
|
|
562
|
+
const sch = ctx.schema.list[va.ptrAux]
|
|
563
|
+
const si = sch ? sch.indexOf(prop) : -1
|
|
564
|
+
if (si >= 0) return emitSchemaSlotRead(asF64(va), si)
|
|
565
|
+
}
|
|
487
566
|
let schemaIdx = typeof obj === 'string' ? ctx.schema.find(obj, prop) : ctx.schema.find(null, prop)
|
|
488
567
|
// Chain receiver (e.g. `o.meta.bias`): when the chain resolves to a known
|
|
489
568
|
// OBJECT shape via JSON-shape propagation, the parent shape's `names`
|
|
@@ -491,7 +570,7 @@ export default (ctx) => {
|
|
|
491
570
|
// ctx.schema.find(null, prop) when multiple registered schemas share a key.
|
|
492
571
|
if (schemaIdx < 0 && typeof obj !== 'string') {
|
|
493
572
|
const sh = shapeOf(obj)
|
|
494
|
-
if ((sh?.
|
|
573
|
+
if ((sh?.val === VAL.OBJECT || sh?.val === VAL.HASH) && sh.names) {
|
|
495
574
|
const i = sh.names.indexOf(prop)
|
|
496
575
|
if (i >= 0) schemaIdx = i
|
|
497
576
|
}
|
|
@@ -517,7 +596,7 @@ export default (ctx) => {
|
|
|
517
596
|
// Skip the external branch and dispatch through the typed HASH/OBJECT path.
|
|
518
597
|
if (ctx.transform.host === 'wasi') return emitDynGetExprTyped(va, key, vt, prop)
|
|
519
598
|
ctx.features.external = true
|
|
520
|
-
return emitDynGetAnyTyped(va, key, vt)
|
|
599
|
+
return emitDynGetAnyTyped(va, key, vt, prop)
|
|
521
600
|
}
|
|
522
601
|
inc('__hash_get', '__str_hash', '__str_eq')
|
|
523
602
|
return typed(['f64.reinterpret_i64', ['call', '$__hash_get', asI64(va), key]], 'f64')
|
|
@@ -570,6 +649,13 @@ export default (ctx) => {
|
|
|
570
649
|
// === Property dispatch (.length, .prop) ===
|
|
571
650
|
|
|
572
651
|
ctx.core.emit['.'] = (obj, prop) => {
|
|
652
|
+
// SRoA flat object: `o.prop` → `local.get $o#i` (analyze.js scanFlatObjects).
|
|
653
|
+
const flatR = typeof obj === 'string' ? ctx.func.flatObjects?.get(obj) : null
|
|
654
|
+
if (flatR) {
|
|
655
|
+
const fi = flatR.names.indexOf(prop)
|
|
656
|
+
if (fi >= 0) return typed(['local.get', `$${obj}#${fi}`], 'f64')
|
|
657
|
+
}
|
|
658
|
+
|
|
573
659
|
// Boxed object: delegate .length and .prop to inner value or schema
|
|
574
660
|
if (typeof obj === 'string' && ctx.schema.isBoxed(obj)) {
|
|
575
661
|
if (prop === 'length') {
|
|
@@ -607,8 +693,33 @@ export default (ctx) => {
|
|
|
607
693
|
if (Array.isArray(obj) && (obj[0] === 'str' || obj[0] == null) && typeof obj[1] === 'string') {
|
|
608
694
|
return typed(['f64.const', Buffer.byteLength(obj[1], 'utf8')], 'f64')
|
|
609
695
|
}
|
|
610
|
-
|
|
611
|
-
|
|
696
|
+
// structInline Array<S>: the header `len` counts physical f64 cells (K
|
|
697
|
+
// per element), so the JS array length is `physicalLen / K`.
|
|
698
|
+
const inlSid = inlineArraySid(obj)
|
|
699
|
+
if (inlSid != null) {
|
|
700
|
+
const K = ctx.schema.list[inlSid].length
|
|
701
|
+
const physLen = ['i32.load', ['i32.sub', ptrOffsetIR(asF64(emit(obj)), VAL.ARRAY), ['i32.const', 8]]]
|
|
702
|
+
return typed(['f64.convert_i32_s', K > 1 ? ['i32.div_s', physLen, ['i32.const', K]] : physLen], 'f64')
|
|
703
|
+
}
|
|
704
|
+
const rep = typeof obj === 'string' ? repOf(obj) : null
|
|
705
|
+
const vt = rep ? rep.val : valTypeOf(obj)
|
|
706
|
+
const notString = vt == null && typeof obj === 'string' && lookupNotString(obj)
|
|
707
|
+
// jsstring carrier: keep the externref-typed IR so emitLengthAccess can
|
|
708
|
+
// dispatch to `wasm:js-string.length` instead of forcing through f64.
|
|
709
|
+
const recv = emit(obj)
|
|
710
|
+
if (recv?.type === 'externref') return emitLengthAccess(recv, vt, notString)
|
|
711
|
+
return emitLengthAccess(asF64(recv), vt, notString)
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Type-specific property emitter (`.regex:source`, …) — the property-read
|
|
715
|
+
// mirror of the `.vt:method` method-dispatch table. Property emitters take
|
|
716
|
+
// a single arg (the receiver); arity ≥ 2 entries are method emitters and
|
|
717
|
+
// must not be routed here (reading `re.test` as a value is not a call).
|
|
718
|
+
const ptRep = typeof obj === 'string' ? repOf(obj) : null
|
|
719
|
+
const ptVt = ptRep ? ptRep.val : valTypeOf(obj)
|
|
720
|
+
if (ptVt) {
|
|
721
|
+
const tpEmitter = ctx.core.emit[`.${ptVt}:${prop}`]
|
|
722
|
+
if (tpEmitter && tpEmitter.length <= 1) return tpEmitter(obj)
|
|
612
723
|
}
|
|
613
724
|
|
|
614
725
|
// Module-registered property emitter (.size, etc.)
|
|
@@ -619,77 +730,78 @@ export default (ctx) => {
|
|
|
619
730
|
return emitPropAccess(emit(obj), obj, prop)
|
|
620
731
|
}
|
|
621
732
|
|
|
622
|
-
// Optional
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
} else {
|
|
631
|
-
const propIdx = typeof obj === 'string' ? ctx.schema.find(obj, prop) : -1
|
|
632
|
-
if (propIdx >= 0) access = emitSchemaSlotRead(['local.get', `$${t}`], propIdx)
|
|
633
|
-
else {
|
|
634
|
-
if (typeof obj === 'string') {
|
|
635
|
-
const objType = lookupValType(obj)
|
|
636
|
-
if (usesDynProps(objType)) {
|
|
637
|
-
access = emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), objType, prop)
|
|
638
|
-
} else if (objType === VAL.HASH) {
|
|
639
|
-
access = emitHashGetLocalConst(['local.get', `$${t}`], asI64(emit(['str', prop])), prop)
|
|
640
|
-
} else if (objType == null) {
|
|
641
|
-
// Unknown receiver — in WASI mode use typed dispatch (no PTR.EXTERNAL values).
|
|
642
|
-
// In JS host mode use __dyn_get_any_t but don't force features.external here
|
|
643
|
-
// since ?.prop short-circuits on nullish (EXTERNAL arm is dead unless already on).
|
|
644
|
-
access = ctx.transform.host === 'wasi'
|
|
645
|
-
? emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), objType, prop)
|
|
646
|
-
: emitDynGetAnyTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), objType)
|
|
647
|
-
} else {
|
|
648
|
-
inc('__hash_get', '__str_hash', '__str_eq')
|
|
649
|
-
access = ['f64.reinterpret_i64', ['call', '$__hash_get', ['i64.reinterpret_f64', ['local.get', `$${t}`]], asI64(emit(['str', prop]))]]
|
|
650
|
-
}
|
|
651
|
-
} else {
|
|
652
|
-
if (valTypeOf(obj) === VAL.HASH) {
|
|
653
|
-
access = emitHashGetLocalConst(['local.get', `$${t}`], asI64(emit(['str', prop])), prop)
|
|
654
|
-
} else {
|
|
655
|
-
access = emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), valTypeOf(obj), prop)
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
// Use local.set + local.get (not local.tee inside guard) because isNullish
|
|
661
|
-
// inlines the null/undefined check as (i32.or (i64.eq X NULL) (i64.eq X UNDEF)),
|
|
662
|
-
// which duplicates X — a local.tee(block(call $sideEffect)) would run twice.
|
|
663
|
-
return typed(['block', ['result', 'f64'],
|
|
733
|
+
// Optional-chain short-circuit: store the receiver/callee into temp `$t`
|
|
734
|
+
// once, evaluate `thenIR` when it is non-nullish, else yield `undefined`.
|
|
735
|
+
// local.set + local.get (never a local.tee feeding the guard) because
|
|
736
|
+
// notNullish inlines an isNullish check — (i32.or (i64.eq X NULL)
|
|
737
|
+
// (i64.eq X UNDEF)) — that duplicates its operand, so a tee'd
|
|
738
|
+
// side-effecting value would run twice.
|
|
739
|
+
const optionalGuard = (t, va, thenIR) =>
|
|
740
|
+
typed(['block', ['result', 'f64'],
|
|
664
741
|
['local.set', `$${t}`, va],
|
|
665
742
|
['if', ['result', 'f64'],
|
|
666
743
|
notNullish(typed(['local.get', `$${t}`], 'f64')),
|
|
667
|
-
['then',
|
|
744
|
+
['then', thenIR],
|
|
668
745
|
['else', ['f64.const', `nan:${UNDEF_NAN}`]]]], 'f64')
|
|
746
|
+
|
|
747
|
+
// Receiver-evaluate-once: allocate a fresh hoist-temp `$t`, emit `value` into
|
|
748
|
+
// it, then call `useFn(t)` to build the consumer IR — wrapping both in an
|
|
749
|
+
// optionalGuard so the consumer runs only when `$t` is non-nullish. Used by
|
|
750
|
+
// `?.` / `?.[]` / `?.()` whose receiver is read twice (the nullish check and
|
|
751
|
+
// the dispatched access) but must evaluate once. Rep-seeding for the temp,
|
|
752
|
+
// when the receiver's value-type drives downstream dispatch, lives inside
|
|
753
|
+
// the useFn callback so it runs before the consumer IR consults reps.
|
|
754
|
+
const evalOnce = (value, useFn) => {
|
|
755
|
+
const t = temp()
|
|
756
|
+
const va = asF64(emit(value))
|
|
757
|
+
return optionalGuard(t, va, useFn(t))
|
|
669
758
|
}
|
|
670
759
|
|
|
760
|
+
// Optional chaining: obj?.prop → null if obj is null, else obj.prop
|
|
761
|
+
ctx.core.emit['?.'] = (obj, prop) => evalOnce(obj, (t) => {
|
|
762
|
+
const rep = typeof obj === 'string' ? repOf(obj) : null
|
|
763
|
+
const vt = rep ? rep.val : valTypeOf(obj)
|
|
764
|
+
if (prop === 'length') {
|
|
765
|
+
const notString = vt == null && typeof obj === 'string' && lookupNotString(obj)
|
|
766
|
+
return emitLengthAccess(['local.get', `$${t}`], vt, notString)
|
|
767
|
+
}
|
|
768
|
+
const propIdx = typeof obj === 'string' ? ctx.schema.find(obj, prop) : -1
|
|
769
|
+
if (propIdx >= 0) return emitSchemaSlotRead(['local.get', `$${t}`], propIdx)
|
|
770
|
+
if (typeof obj === 'string') {
|
|
771
|
+
const objType = lookupValType(obj)
|
|
772
|
+
if (usesDynProps(objType))
|
|
773
|
+
return emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), objType, prop)
|
|
774
|
+
if (objType === VAL.HASH)
|
|
775
|
+
return emitHashGetLocalConst(['local.get', `$${t}`], asI64(emit(['str', prop])), prop)
|
|
776
|
+
if (objType == null)
|
|
777
|
+
// Unknown receiver — in WASI mode use typed dispatch (no PTR.EXTERNAL values).
|
|
778
|
+
// In JS host mode use __dyn_get_any_t but don't force features.external here
|
|
779
|
+
// since ?.prop short-circuits on nullish (EXTERNAL arm is dead unless already on).
|
|
780
|
+
return ctx.transform.host === 'wasi'
|
|
781
|
+
? emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), objType, prop)
|
|
782
|
+
: emitDynGetAnyTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), objType, prop)
|
|
783
|
+
inc('__hash_get', '__str_hash', '__str_eq')
|
|
784
|
+
return ['f64.reinterpret_i64', ['call', '$__hash_get', ['i64.reinterpret_f64', ['local.get', `$${t}`]], asI64(emit(['str', prop]))]]
|
|
785
|
+
}
|
|
786
|
+
if (valTypeOf(obj) === VAL.HASH)
|
|
787
|
+
return emitHashGetLocalConst(['local.get', `$${t}`], asI64(emit(['str', prop])), prop)
|
|
788
|
+
return emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), valTypeOf(obj), prop)
|
|
789
|
+
})
|
|
790
|
+
|
|
671
791
|
// Optional index: arr?.[i] → null if arr is null, else arr[i]
|
|
672
792
|
// Cache base in temp, propagate valType so []'s type dispatch works
|
|
673
|
-
ctx.core.emit['?.[]'] = (arr, idx) => {
|
|
674
|
-
|
|
675
|
-
const va = asF64(emit(arr))
|
|
793
|
+
ctx.core.emit['?.[]'] = (arr, idx) => evalOnce(arr, (t) => {
|
|
794
|
+
// Emit-time rep seed on fresh `?.[]` hoist-temp (lifecycle: analysis-vs-emit).
|
|
676
795
|
// Propagate source type to temp so [] dispatch (string, typed, etc.) works
|
|
796
|
+
// when the inner `ctx.core.emit['[]'](t, idx)` re-enters dispatch.
|
|
677
797
|
const srcType = typeof arr === 'string' ? repOf(arr)?.val : null
|
|
678
798
|
if (srcType) updateRep(t, { val: srcType })
|
|
679
799
|
if (typeof arr === 'string' && ctx.types.typedElem?.has(arr)) {
|
|
680
800
|
if (!ctx.types.typedElem) ctx.types.typedElem = new Map()
|
|
681
801
|
ctx.types.typedElem.set(t, ctx.types.typedElem.get(arr))
|
|
682
802
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
// which duplicates X — a local.tee(block(call $sideEffect)) would run twice.
|
|
686
|
-
return typed(['block', ['result', 'f64'],
|
|
687
|
-
['local.set', `$${t}`, va],
|
|
688
|
-
['if', ['result', 'f64'],
|
|
689
|
-
notNullish(typed(['local.get', `$${t}`], 'f64')),
|
|
690
|
-
['then', asF64(ctx.core.emit['[]'](t, idx))],
|
|
691
|
-
['else', ['f64.const', `nan:${UNDEF_NAN}`]]]], 'f64')
|
|
692
|
-
}
|
|
803
|
+
return asF64(ctx.core.emit['[]'](t, idx))
|
|
804
|
+
})
|
|
693
805
|
|
|
694
806
|
// Optional call: fn?.(...args) → null if fn is null, else call fn
|
|
695
807
|
ctx.core.emit['?.()'] = (callee, ...args) => {
|
|
@@ -701,42 +813,57 @@ export default (ctx) => {
|
|
|
701
813
|
const methodEmit = ctx.core.emit[`.${callee[2]}`]
|
|
702
814
|
if (methodEmit) {
|
|
703
815
|
const recv = callee[1]
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
// inlines the null/undefined check which duplicates the expression.
|
|
711
|
-
return typed(['block', ['result', 'f64'],
|
|
712
|
-
['local.set', `$${t}`, va],
|
|
713
|
-
['if', ['result', 'f64'],
|
|
714
|
-
notNullish(typed(['local.get', `$${t}`], 'f64')),
|
|
715
|
-
['then', asF64(callResult)],
|
|
716
|
-
['else', ['f64.const', `nan:${UNDEF_NAN}`]]]], 'f64')
|
|
816
|
+
return evalOnce(recv, (t) => {
|
|
817
|
+
// Emit-time rep seed on fresh `?.()` recv-temp so methodEmit's dispatch fast-paths fire.
|
|
818
|
+
const vt = typeof recv === 'string' ? repOf(recv)?.val : valTypeOf(recv)
|
|
819
|
+
if (vt) updateRep(t, { val: vt })
|
|
820
|
+
return asF64(methodEmit(t, ...args))
|
|
821
|
+
})
|
|
717
822
|
}
|
|
718
823
|
}
|
|
719
|
-
const t = temp()
|
|
720
|
-
const va = asF64(emit(callee))
|
|
721
|
-
// If nullish → return NULL_NAN, else call via fn.call
|
|
722
824
|
if (!ctx.closure.call) err('Optional call requires fn module')
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
[
|
|
825
|
+
return evalOnce(callee, (t) => {
|
|
826
|
+
// Spread args: mirror the regular `()` emitter — reconstruct the args array
|
|
827
|
+
// and route through `closure.call(_, [arrayIR], prebuiltArray=true)`. Without
|
|
828
|
+
// this, the raw `['...', expr]` node falls through to the bare spread emitter
|
|
829
|
+
// and errors as "Spread (...) can only be used in function/method calls".
|
|
830
|
+
const hasSpread = args.some(a => Array.isArray(a) && a[0] === '...')
|
|
831
|
+
let callResult
|
|
832
|
+
if (hasSpread) {
|
|
833
|
+
const normal = [], spreads = []
|
|
834
|
+
for (const a of args) {
|
|
835
|
+
if (Array.isArray(a) && a[0] === '...') spreads.push({ pos: normal.length, expr: a[1] })
|
|
836
|
+
else normal.push(a)
|
|
837
|
+
}
|
|
838
|
+
const combined = reconstructArgsWithSpreads(normal, spreads)
|
|
839
|
+
const arrayIR = buildArrayWithSpreads(combined)
|
|
840
|
+
callResult = ctx.closure.call(typed(['local.get', `$${t}`], 'f64'), [arrayIR], true)
|
|
841
|
+
} else {
|
|
842
|
+
callResult = ctx.closure.call(typed(['local.get', `$${t}`], 'f64'), args)
|
|
843
|
+
}
|
|
844
|
+
return asF64(callResult)
|
|
845
|
+
})
|
|
732
846
|
}
|
|
733
847
|
|
|
734
|
-
//
|
|
735
|
-
//
|
|
848
|
+
// Statically boolean-typed operands: `Boolean(x)`, logical-not, and the
|
|
849
|
+
// relational/equality comparisons always yield a JS boolean — jz carries it as
|
|
850
|
+
// f64 0/1 but `typeof` must still report "boolean". None of these ops can
|
|
851
|
+
// produce a non-boolean, so the recognizer never false-positives. The `()`
|
|
852
|
+
// arm also unwraps parenthesized expressions (`typeof (a < b)`).
|
|
853
|
+
const BOOL_RESULT_OPS = new Set(['!', '<', '<=', '>', '>=', '==', '!=', '===', '!=='])
|
|
854
|
+
const isBoolExpr = (n) => Array.isArray(n) && (
|
|
855
|
+
BOOL_RESULT_OPS.has(n[0]) ||
|
|
856
|
+
(n[0] === '()' && (n[1] === 'Boolean' || isBoolExpr(n[1]))))
|
|
857
|
+
|
|
858
|
+
// typeof: returns JS-style string. Reachable results are number/undefined/string/function/symbol/object/boolean
|
|
859
|
+
// (booleans without a static type hit the number branch; no bigints). Strings are preallocated into globals and
|
|
736
860
|
// initialized in __start (see compile.js). Comparison patterns (typeof x === 'string') are optimized
|
|
737
861
|
// in prepare.js (resolveTypeof) and emitted as direct type checks via emitTypeofCmp, bypassing this path.
|
|
738
862
|
ctx.core.emit['typeof'] = (a) => {
|
|
739
863
|
if (valTypeOf(a) === VAL.BIGINT) return emit(['str', 'bigint'])
|
|
864
|
+
// VAL.BOOL covers boolean literals, comparisons, `!` and bindings inferred
|
|
865
|
+
// boolean; isBoolExpr additionally catches `Boolean(x)` and parenthesized forms.
|
|
866
|
+
if (valTypeOf(a) === VAL.BOOL || isBoolExpr(a)) return emit(['str', 'boolean'])
|
|
740
867
|
if (!ctx.runtime.typeofStrs) {
|
|
741
868
|
ctx.runtime.typeofStrs = ['number', 'undefined', 'string', 'function', 'symbol', 'object']
|
|
742
869
|
for (const s of ctx.runtime.typeofStrs)
|
|
@@ -767,8 +894,11 @@ export default (ctx) => {
|
|
|
767
894
|
(i64.eq (i64.and (local.get $v) (i64.const 0xFFF0000000000000))
|
|
768
895
|
(i64.const 0xFFF0000000000000)))
|
|
769
896
|
(then (return (global.get $__tof_number))))
|
|
770
|
-
(if (
|
|
897
|
+
(if (i64.eq (local.get $v) (i64.const ${UNDEF_NAN}))
|
|
771
898
|
(then (return (global.get $__tof_undefined))))
|
|
899
|
+
;; typeof null === "object" — the historical JS quirk, distinct from undefined.
|
|
900
|
+
(if (i64.eq (local.get $v) (i64.const ${NULL_NAN}))
|
|
901
|
+
(then (return (global.get $__tof_object))))
|
|
772
902
|
(local.set $t (call $__ptr_type (local.get $v)))
|
|
773
903
|
(if ${stringTest}
|
|
774
904
|
(then (return (global.get $__tof_string))))
|
|
@@ -787,6 +917,24 @@ export default (ctx) => {
|
|
|
787
917
|
ctx.core.emit['__ptr_aux'] = (p) => typed(['f64.convert_i32_s', ['call', '$__ptr_aux', asI64(emit(p))]], 'f64')
|
|
788
918
|
ctx.core.emit['__ptr_offset'] = (p) => typed(['f64.convert_i32_s', ['call', '$__ptr_offset', asI64(emit(p))]], 'f64')
|
|
789
919
|
|
|
790
|
-
// Error(msg) — passthrough (throw handles any value)
|
|
791
|
-
|
|
920
|
+
// Error(msg) — passthrough (throw handles any value). Subclasses share the
|
|
921
|
+
// same shape: jz doesn't model typed-error dispatch, so SyntaxError/TypeError/
|
|
922
|
+
// RangeError/ReferenceError/URIError/EvalError all lower to the message
|
|
923
|
+
// expression. `instanceof SyntaxError` returning correct results would need
|
|
924
|
+
// proper class machinery; until then, code that throws specific subclasses
|
|
925
|
+
// compiles and the user-visible message is preserved.
|
|
926
|
+
// jz models an error as its message value (passthrough — `throw` accepts any
|
|
927
|
+
// value). A no-arg `new Error()` has no message, so it lowers to `undefined`
|
|
928
|
+
// rather than crashing on a missing argument. Emitting `['str','']` here would
|
|
929
|
+
// drag the whole string module into programs that only `throw new Error()`.
|
|
930
|
+
const passthroughError = (msg) => msg == null
|
|
931
|
+
? typed(['f64.const', `nan:${UNDEF_NAN}`], 'f64')
|
|
932
|
+
: asF64(emit(msg))
|
|
933
|
+
ctx.core.emit['Error'] = passthroughError
|
|
934
|
+
ctx.core.emit['SyntaxError'] = passthroughError
|
|
935
|
+
ctx.core.emit['TypeError'] = passthroughError
|
|
936
|
+
ctx.core.emit['RangeError'] = passthroughError
|
|
937
|
+
ctx.core.emit['ReferenceError'] = passthroughError
|
|
938
|
+
ctx.core.emit['URIError'] = passthroughError
|
|
939
|
+
ctx.core.emit['EvalError'] = passthroughError
|
|
792
940
|
}
|
package/module/date.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* @module date
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { typed, asF64, toNumF64, allocPtr } from '../src/ir.js'
|
|
12
|
+
import { typed, asF64, toNumF64, allocPtr, temp } from '../src/ir.js'
|
|
13
13
|
import { emit } from '../src/emit.js'
|
|
14
14
|
import { inc, PTR } from '../src/ctx.js'
|
|
15
15
|
import { VAL } from '../src/analyze.js'
|
|
@@ -30,7 +30,7 @@ export default (ctx) => {
|
|
|
30
30
|
__date_day: [],
|
|
31
31
|
__date_time_within_day: ['__date_day'],
|
|
32
32
|
__date_weekday: ['__date_day'],
|
|
33
|
-
__date_year_from_time: ['__date_days_from_year'],
|
|
33
|
+
__date_year_from_time: ['__date_day', '__date_days_from_year'],
|
|
34
34
|
__date_in_leap_year: ['__date_year_from_time', '__date_days_from_year'],
|
|
35
35
|
__date_day_within_year: ['__date_day', '__date_year_from_time', '__date_days_from_year'],
|
|
36
36
|
__date_month_from_time: ['__date_day_within_year', '__date_in_leap_year'],
|
|
@@ -618,10 +618,14 @@ export default (ctx) => {
|
|
|
618
618
|
const emitDateSetTime = (dateExpr, ms) => {
|
|
619
619
|
inc('__date_time_clip')
|
|
620
620
|
const d = asF64(emit(dateExpr))
|
|
621
|
-
|
|
621
|
+
// Clip once into a temp: the result feeds both the store and the block
|
|
622
|
+
// value, and sharing one IR node across two tree positions miscompiles
|
|
623
|
+
// under CSE (it hoists the arg without emitting its local.set).
|
|
624
|
+
const t = temp('st')
|
|
622
625
|
return typed(['block', ['result', 'f64'],
|
|
623
|
-
['
|
|
624
|
-
|
|
626
|
+
['local.set', `$${t}`, typed(['call', '$__date_time_clip', toNumF64(ms, emit(ms))], 'f64')],
|
|
627
|
+
['f64.store', ['i32.wrap_i64', ['i64.reinterpret_f64', d]], ['local.get', `$${t}`]],
|
|
628
|
+
['local.get', `$${t}`]], 'f64')
|
|
625
629
|
}
|
|
626
630
|
|
|
627
631
|
ctx.core.emit['.getTime'] = emitDateGetTime
|
package/module/function.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { typed, asF64, asI32, mkPtrIR, temp, tempI32, MAX_CLOSURE_ARITY, UNDEF_NAN } from '../src/ir.js'
|
|
14
14
|
import { emit } from '../src/emit.js'
|
|
15
|
-
import { isReassigned } from '../src/
|
|
15
|
+
import { isReassigned } from '../src/analyze.js'
|
|
16
16
|
import { T, lookupValType, repOf, findFreeVars } from '../src/analyze.js'
|
|
17
17
|
import { PTR, LAYOUT, inc, err } from '../src/ctx.js'
|
|
18
18
|
|
|
@@ -94,6 +94,14 @@ export default (ctx) => {
|
|
|
94
94
|
const captureValTypes = new Map()
|
|
95
95
|
const captureSchemaVars = new Map()
|
|
96
96
|
const captureTypedElems = new Map()
|
|
97
|
+
// Propagate parent's intCertain rep across captures: the parent's narrower
|
|
98
|
+
// already guarantees every defining RHS (including assignments inside
|
|
99
|
+
// nested arrows) is integer-valued, so the f64 round-trip through the env
|
|
100
|
+
// slot preserves integer semantics. Consumers inside the inner body —
|
|
101
|
+
// `toNumF64` elision (src/ir.js), math fast paths (module/math.js),
|
|
102
|
+
// bitwise-key indexing (src/emit.js) — fire on captures just as they
|
|
103
|
+
// would on directly-declared locals.
|
|
104
|
+
const captureIntCertain = new Set()
|
|
97
105
|
// Propagate parent's directClosures across captures: a const-bound closure captured
|
|
98
106
|
// by an inner arrow can still be direct-dispatched in the inner body (skip
|
|
99
107
|
// call_indirect on the captured pointer). Gated on isReassigned over the inner body
|
|
@@ -108,6 +116,7 @@ export default (ctx) => {
|
|
|
108
116
|
if (elemType != null) captureTypedElems.set(name, elemType)
|
|
109
117
|
const bodyName = ctx.func.directClosures?.get(name)
|
|
110
118
|
if (bodyName && !isReassigned(body, name)) captureDirectClosures.set(name, bodyName)
|
|
119
|
+
if (repOf(name)?.intCertain === true) captureIntCertain.add(name)
|
|
111
120
|
}
|
|
112
121
|
|
|
113
122
|
const schemaNames = ctx.schema.vars?.size ? new Set(ctx.schema.vars.keys()) : null
|
|
@@ -130,6 +139,7 @@ export default (ctx) => {
|
|
|
130
139
|
...(defaults && { defaults }),
|
|
131
140
|
...(boxedCaptures.length && { boxed: new Set(boxedCaptures) }),
|
|
132
141
|
...(captureIntConsts.size && { intConsts: captureIntConsts }),
|
|
142
|
+
...(captureIntCertain.size && { intCertain: captureIntCertain }),
|
|
133
143
|
...(captureValTypes.size && { valTypes: captureValTypes }),
|
|
134
144
|
...(captureSchemaVars.size && { schemaVars: captureSchemaVars }),
|
|
135
145
|
...(captureTypedElems.size && { typedElems: captureTypedElems }),
|