jz 0.2.0 → 0.3.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/module/core.js CHANGED
@@ -173,16 +173,25 @@ export default (ctx) => {
173
173
  } else {
174
174
  // Own memory: heap offset in a global, auto-grow when needed
175
175
  ctx.scope.globals.set('__heap', '(global $__heap (mut i32) (i32.const 1024))')
176
+ // Bump allocator with geometric growth. Growing one page at a time turns a
177
+ // long-running embedding (e.g. watr called thousands of times) into O(n²) —
178
+ // each memory.grow may relocate and copy the whole heap. So when we must
179
+ // grow, request at least the current size (≥2× total) in one shot; only on
180
+ // hitting the declared maximum do we fall back to the bare minimum.
176
181
  ctx.core.stdlib['__alloc'] = `(func $__alloc (param $bytes i32) (result i32)
177
- (local $ptr i32) (local $next i32)
182
+ (local $ptr i32) (local $next i32) (local $cur i32) (local $need i32)
178
183
  (local.set $ptr (global.get $__heap))
179
184
  ;; Align next allocation to 8 bytes
180
185
  (local.set $next (i32.and (i32.add (i32.add (local.get $ptr) (local.get $bytes)) (i32.const 7)) (i32.const -8)))
181
- ;; Grow memory if needed (each page = 65536 bytes)
182
- (if (i32.gt_u (local.get $next) (i32.mul (memory.size) (i32.const 65536)))
183
- (then (if (i32.eq (memory.grow
184
- (i32.shr_u (i32.add (i32.sub (local.get $next) (i32.mul (memory.size) (i32.const 65536))) (i32.const 65535)) (i32.const 16)))
185
- (i32.const -1)) (then (unreachable)))))
186
+ (local.set $cur (i32.shl (memory.size) (i32.const 16)))
187
+ (if (i32.gt_u (local.get $next) (local.get $cur))
188
+ (then
189
+ (local.set $need (i32.shr_u (i32.add (i32.sub (local.get $next) (local.get $cur)) (i32.const 65535)) (i32.const 16)))
190
+ (if (i32.lt_u (local.get $need) (memory.size)) (then (local.set $need (memory.size))))
191
+ (if (i32.eq (memory.grow (local.get $need)) (i32.const -1))
192
+ (then (if (i32.eq (memory.grow
193
+ (i32.shr_u (i32.add (i32.sub (local.get $next) (local.get $cur)) (i32.const 65535)) (i32.const 16)))
194
+ (i32.const -1)) (then (unreachable)))))))
186
195
  (global.set $__heap (local.get $next))
187
196
  (local.get $ptr))`
188
197
  ctx.core.stdlib['__clear'] = `(func $__clear
@@ -404,9 +413,15 @@ export default (ctx) => {
404
413
  return ['call', '$__ptr_type', receiver]
405
414
  }
406
415
 
407
- function emitDynGetExprTyped(base, key, vt) {
408
- inc('__dyn_get_expr_t')
416
+ function emitDynGetExprTyped(base, key, vt, prop) {
409
417
  const receiver = asI64(base?.type ? base : typed(base, 'f64'))
418
+ // Constant string key: fold the FNV hash at compile time and call the
419
+ // prehashed body — no __str_hash on every access.
420
+ if (typeof prop === 'string') {
421
+ inc('__dyn_get_expr_t_h')
422
+ return typed(['f64.reinterpret_i64', ['call', '$__dyn_get_expr_t_h', receiver, key, emitTypeTag(receiver, vt), ['i32.const', strHashLiteral(prop)]]], 'f64')
423
+ }
424
+ inc('__dyn_get_expr_t')
410
425
  return typed(['f64.reinterpret_i64', ['call', '$__dyn_get_expr_t', receiver, key, emitTypeTag(receiver, vt)]], 'f64')
411
426
  }
412
427
 
@@ -486,7 +501,7 @@ export default (ctx) => {
486
501
  if (typeof obj === 'string') {
487
502
  const vt = lookupValType(obj)
488
503
  if (usesDynProps(vt)) {
489
- return emitDynGetExprTyped(va, key, vt)
504
+ return emitDynGetExprTyped(va, key, vt, prop)
490
505
  }
491
506
  if (vt === VAL.HASH) {
492
507
  return emitHashGetLocalConst(va, key, prop)
@@ -495,12 +510,12 @@ export default (ctx) => {
495
510
  // at off-16 (set by __dyn_set). __hash_get assumes HASH bucket layout
496
511
  // and would mis-read OBJECT memory.
497
512
  if (vt === VAL.OBJECT) {
498
- return emitDynGetExprTyped(va, key, vt)
513
+ return emitDynGetExprTyped(va, key, vt, prop)
499
514
  }
500
515
  if (vt == null) {
501
516
  // In WASI mode, values are always JSON-derived (never PTR.EXTERNAL host objects).
502
517
  // Skip the external branch and dispatch through the typed HASH/OBJECT path.
503
- if (ctx.transform.host === 'wasi') return emitDynGetExprTyped(va, key, vt)
518
+ if (ctx.transform.host === 'wasi') return emitDynGetExprTyped(va, key, vt, prop)
504
519
  ctx.features.external = true
505
520
  return emitDynGetAnyTyped(va, key, vt)
506
521
  }
@@ -598,7 +613,8 @@ export default (ctx) => {
598
613
 
599
614
  // Module-registered property emitter (.size, etc.)
600
615
  const propKey = `.${prop}`
601
- if (ctx.core.emit[propKey]) return ctx.core.emit[propKey](obj)
616
+ const propEmitter = ctx.core.emit[propKey]
617
+ if (propEmitter && propEmitter.length <= 1) return propEmitter(obj)
602
618
 
603
619
  return emitPropAccess(emit(obj), obj, prop)
604
620
  }
@@ -618,7 +634,7 @@ export default (ctx) => {
618
634
  if (typeof obj === 'string') {
619
635
  const objType = lookupValType(obj)
620
636
  if (usesDynProps(objType)) {
621
- access = emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), objType)
637
+ access = emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), objType, prop)
622
638
  } else if (objType === VAL.HASH) {
623
639
  access = emitHashGetLocalConst(['local.get', `$${t}`], asI64(emit(['str', prop])), prop)
624
640
  } else if (objType == null) {
@@ -626,7 +642,7 @@ export default (ctx) => {
626
642
  // In JS host mode use __dyn_get_any_t but don't force features.external here
627
643
  // since ?.prop short-circuits on nullish (EXTERNAL arm is dead unless already on).
628
644
  access = ctx.transform.host === 'wasi'
629
- ? emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), objType)
645
+ ? emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), objType, prop)
630
646
  : emitDynGetAnyTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), objType)
631
647
  } else {
632
648
  inc('__hash_get', '__str_hash', '__str_eq')
@@ -636,12 +652,20 @@ export default (ctx) => {
636
652
  if (valTypeOf(obj) === VAL.HASH) {
637
653
  access = emitHashGetLocalConst(['local.get', `$${t}`], asI64(emit(['str', prop])), prop)
638
654
  } else {
639
- access = emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), valTypeOf(obj))
655
+ access = emitDynGetExprTyped(['local.get', `$${t}`], asI64(emit(['str', prop])), valTypeOf(obj), prop)
640
656
  }
641
657
  }
642
658
  }
643
659
  }
644
- return emitNullishGuarded(['local.tee', `$${t}`, va], access)
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'],
664
+ ['local.set', `$${t}`, va],
665
+ ['if', ['result', 'f64'],
666
+ notNullish(typed(['local.get', `$${t}`], 'f64')),
667
+ ['then', access],
668
+ ['else', ['f64.const', `nan:${UNDEF_NAN}`]]]], 'f64')
645
669
  }
646
670
 
647
671
  // Optional index: arr?.[i] → null if arr is null, else arr[i]
@@ -656,7 +680,15 @@ export default (ctx) => {
656
680
  if (!ctx.types.typedElem) ctx.types.typedElem = new Map()
657
681
  ctx.types.typedElem.set(t, ctx.types.typedElem.get(arr))
658
682
  }
659
- return emitNullishGuarded(['local.tee', `$${t}`, va], asF64(ctx.core.emit['[]'](t, idx)))
683
+ // Use local.set + local.get (not local.tee inside guard) because isNullish
684
+ // inlines the null/undefined check as (i32.or (i64.eq X NULL) (i64.eq X UNDEF)),
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')
660
692
  }
661
693
 
662
694
  // Optional call: fn?.(...args) → null if fn is null, else call fn
@@ -674,7 +706,14 @@ export default (ctx) => {
674
706
  const vt = typeof recv === 'string' ? repOf(recv)?.val : valTypeOf(recv)
675
707
  if (vt) updateRep(t, { val: vt })
676
708
  const callResult = methodEmit(t, ...args)
677
- return emitNullishGuarded(['local.tee', `$${t}`, va], asF64(callResult))
709
+ // Use local.set + local.get (not local.tee inside guard) because isNullish
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')
678
717
  }
679
718
  }
680
719
  const t = temp()
@@ -682,7 +721,14 @@ export default (ctx) => {
682
721
  // If nullish → return NULL_NAN, else call via fn.call
683
722
  if (!ctx.closure.call) err('Optional call requires fn module')
684
723
  const callResult = ctx.closure.call(typed(['local.get', `$${t}`], 'f64'), args)
685
- return emitNullishGuarded(['local.tee', `$${t}`, va], asF64(callResult))
724
+ // Use local.set + local.get (not local.tee inside guard) because isNullish
725
+ // inlines the null/undefined check which duplicates the expression.
726
+ return typed(['block', ['result', 'f64'],
727
+ ['local.set', `$${t}`, va],
728
+ ['if', ['result', 'f64'],
729
+ notNullish(typed(['local.get', `$${t}`], 'f64')),
730
+ ['then', asF64(callResult)],
731
+ ['else', ['f64.const', `nan:${UNDEF_NAN}`]]]], 'f64')
686
732
  }
687
733
 
688
734
  // typeof: returns JS-style string. Reachable results are number/undefined/string/function/symbol/object
package/module/date.js CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Date module — deterministic UTC algorithms first.
3
3
  *
4
- * Current scope: Date.UTC(...), Date object construction, UTC getters/setters,
5
- * toISOString, toUTCString. Local-time methods, parsing, and locale-sensitive
6
- * formatting are deliberately staged later.
4
+ * Current scope: Date.UTC(...), Date object construction, timestamp/string
5
+ * construction, UTC getters/setters, UTC-backed local getters, toISOString,
6
+ * toUTCString. Timezone-aware local-time methods and locale-sensitive formatting
7
+ * are deliberately staged later.
7
8
  *
8
9
  * @module date
9
10
  */
@@ -23,6 +24,9 @@ export default (ctx) => {
23
24
  __date_make_time: [],
24
25
  __date_time_clip: [],
25
26
  __date_utc: ['__date_make_day', '__date_make_time', '__date_time_clip'],
27
+ __date_digit: ['__char_at'],
28
+ __date_parse_iso_date: ['__str_byteLen', '__char_at', '__date_digit', '__date_make_day', '__date_time_clip'],
29
+ __date_from_value: ['__ptr_type', '__to_num', '__date_parse_iso_date'],
26
30
  __date_day: [],
27
31
  __date_time_within_day: ['__date_day'],
28
32
  __date_weekday: ['__date_day'],
@@ -72,6 +76,11 @@ export default (ctx) => {
72
76
  ], 'f64')
73
77
  }
74
78
 
79
+ ctx.core.emit['Date.parse'] = (value) => {
80
+ inc('__date_from_value')
81
+ return typed(['call', '$__date_from_value', ['i64.reinterpret_f64', asF64(emit(value))]], 'f64')
82
+ }
83
+
75
84
  // ── Core algorithms ───────────────────────────────────────────────────────
76
85
 
77
86
  ctx.core.stdlib['__date_days_from_year'] = `(func $__date_days_from_year (param $y f64) (result f64)
@@ -149,6 +158,66 @@ export default (ctx) => {
149
158
  (f64.const ${MS_PER_DAY}))
150
159
  (call $__date_make_time (local.get $hours) (local.get $minutes) (local.get $seconds) (local.get $ms)))))`
151
160
 
161
+ ctx.core.stdlib['__date_digit'] = `(func $__date_digit (param $str i64) (param $i i32) (result i32)
162
+ (local $c i32)
163
+ (local.set $c (call $__char_at (local.get $str) (local.get $i)))
164
+ (if (result i32)
165
+ (i32.and (i32.ge_s (local.get $c) (i32.const 48)) (i32.le_s (local.get $c) (i32.const 57)))
166
+ (then (i32.sub (local.get $c) (i32.const 48)))
167
+ (else (i32.const -1))))`
168
+
169
+ ctx.core.stdlib['__date_parse_iso_date'] = `(func $__date_parse_iso_date (param $str i64) (result f64)
170
+ (local $y f64) (local $m f64) (local $d f64)
171
+ (local $d0 i32) (local $d1 i32) (local $d2 i32) (local $d3 i32)
172
+ (if (i32.lt_s (call $__str_byteLen (local.get $str)) (i32.const 10))
173
+ (then (return (f64.const nan))))
174
+ (if (i32.ne (call $__char_at (local.get $str) (i32.const 4)) (i32.const 45))
175
+ (then (return (f64.const nan))))
176
+ (if (i32.ne (call $__char_at (local.get $str) (i32.const 7)) (i32.const 45))
177
+ (then (return (f64.const nan))))
178
+
179
+ (local.set $d0 (call $__date_digit (local.get $str) (i32.const 0)))
180
+ (local.set $d1 (call $__date_digit (local.get $str) (i32.const 1)))
181
+ (local.set $d2 (call $__date_digit (local.get $str) (i32.const 2)))
182
+ (local.set $d3 (call $__date_digit (local.get $str) (i32.const 3)))
183
+ (if (i32.or (i32.or (i32.lt_s (local.get $d0) (i32.const 0)) (i32.lt_s (local.get $d1) (i32.const 0)))
184
+ (i32.or (i32.lt_s (local.get $d2) (i32.const 0)) (i32.lt_s (local.get $d3) (i32.const 0))))
185
+ (then (return (f64.const nan))))
186
+ (local.set $y (f64.convert_i32_s
187
+ (i32.add
188
+ (i32.add (i32.mul (local.get $d0) (i32.const 1000)) (i32.mul (local.get $d1) (i32.const 100)))
189
+ (i32.add (i32.mul (local.get $d2) (i32.const 10)) (local.get $d3)))))
190
+
191
+ (local.set $d0 (call $__date_digit (local.get $str) (i32.const 5)))
192
+ (local.set $d1 (call $__date_digit (local.get $str) (i32.const 6)))
193
+ (if (i32.or (i32.lt_s (local.get $d0) (i32.const 0)) (i32.lt_s (local.get $d1) (i32.const 0)))
194
+ (then (return (f64.const nan))))
195
+ (local.set $m (f64.convert_i32_s (i32.add (i32.mul (local.get $d0) (i32.const 10)) (local.get $d1))))
196
+ (if (i32.or (f64.lt (local.get $m) (f64.const 1)) (f64.gt (local.get $m) (f64.const 12)))
197
+ (then (return (f64.const nan))))
198
+
199
+ (local.set $d0 (call $__date_digit (local.get $str) (i32.const 8)))
200
+ (local.set $d1 (call $__date_digit (local.get $str) (i32.const 9)))
201
+ (if (i32.or (i32.lt_s (local.get $d0) (i32.const 0)) (i32.lt_s (local.get $d1) (i32.const 0)))
202
+ (then (return (f64.const nan))))
203
+ (local.set $d (f64.convert_i32_s (i32.add (i32.mul (local.get $d0) (i32.const 10)) (local.get $d1))))
204
+ (if (i32.or (f64.lt (local.get $d) (f64.const 1)) (f64.gt (local.get $d) (f64.const 31)))
205
+ (then (return (f64.const nan))))
206
+
207
+ (call $__date_time_clip
208
+ (f64.mul
209
+ (call $__date_make_day (local.get $y) (f64.sub (local.get $m) (f64.const 1)) (local.get $d))
210
+ (f64.const ${MS_PER_DAY}))))`
211
+
212
+ ctx.core.stdlib['__date_from_value'] = `(func $__date_from_value (param $v i64) (result f64)
213
+ (local $f f64)
214
+ (local.set $f (f64.reinterpret_i64 (local.get $v)))
215
+ (if (f64.eq (local.get $f) (local.get $f))
216
+ (then (return (call $__date_time_clip (local.get $f)))))
217
+ (if (i32.eq (call $__ptr_type (local.get $v)) (i32.const ${PTR.STRING}))
218
+ (then (return (call $__date_parse_iso_date (local.get $v)))))
219
+ (call $__date_time_clip (call $__to_num (local.get $v))))`
220
+
152
221
  // ── Time decomposition (ECMA-262 §21.4.1) ────────────────────────────────
153
222
 
154
223
  ctx.core.stdlib['__date_day'] = `(func $__date_day (param $t f64) (result f64)
@@ -513,13 +582,26 @@ export default (ctx) => {
513
582
  // Represented as PTR.OBJECT with a single f64 slot at offset 0 (the time value).
514
583
  // No schemaId (aux=0); dynamic property access falls through to undefined.
515
584
 
516
- ctx.core.emit['new.Date'] = (ms) => {
585
+ ctx.core.emit['new.Date'] = (ms, month, date, hours, minutes, seconds, millis) => {
517
586
  let timeVal
518
587
  if (ms === undefined || ms === null) {
519
- timeVal = typed(['f64.const', NaN], 'f64')
588
+ const now = ctx.core.emit['Date.now']
589
+ if (!now) throw new Error('Date constructor requires Date.now support')
590
+ timeVal = now()
591
+ } else if (!missingArg(month)) {
592
+ inc('__date_utc')
593
+ timeVal = typed(['call', '$__date_utc',
594
+ asF64(dateArg(ms, NaN, true)),
595
+ asF64(dateArg(month, 0)),
596
+ asF64(dateArg(date, 1)),
597
+ asF64(dateArg(hours, 0)),
598
+ asF64(dateArg(minutes, 0)),
599
+ asF64(dateArg(seconds, 0)),
600
+ asF64(dateArg(millis, 0)),
601
+ ], 'f64')
520
602
  } else {
521
- inc('__date_time_clip')
522
- timeVal = typed(['call', '$__date_time_clip', toNumF64(ms, emit(ms))], 'f64')
603
+ inc('__date_from_value')
604
+ timeVal = typed(['call', '$__date_from_value', ['i64.reinterpret_f64', asF64(emit(ms))]], 'f64')
523
605
  }
524
606
  const out = allocPtr({ type: PTR.OBJECT, len: 1, cap: 1, stride: 8, tag: 'date' })
525
607
  return typed(['block', ['result', 'f64'],
@@ -585,6 +667,21 @@ export default (ctx) => {
585
667
  ctx.core.emit['.getUTCMilliseconds'] = dateGetter('__date_ms_from_time')
586
668
  ctx.core.emit[`.${VAL.DATE}:getUTCMilliseconds`] = dateGetter('__date_ms_from_time')
587
669
 
670
+ // UTC-backed local getters. Full timezone-aware local time is intentionally
671
+ // staged separately; these aliases make deterministic/server Date use cases
672
+ // available without a timezone database.
673
+ ctx.core.emit['.getFullYear'] = ctx.core.emit['.getUTCFullYear']
674
+ ctx.core.emit[`.${VAL.DATE}:getFullYear`] = ctx.core.emit[`.${VAL.DATE}:getUTCFullYear`]
675
+
676
+ ctx.core.emit['.getMonth'] = ctx.core.emit['.getUTCMonth']
677
+ ctx.core.emit[`.${VAL.DATE}:getMonth`] = ctx.core.emit[`.${VAL.DATE}:getUTCMonth`]
678
+
679
+ ctx.core.emit['.getDate'] = ctx.core.emit['.getUTCDate']
680
+ ctx.core.emit[`.${VAL.DATE}:getDate`] = ctx.core.emit[`.${VAL.DATE}:getUTCDate`]
681
+
682
+ ctx.core.emit['.getDay'] = ctx.core.emit['.getUTCDay']
683
+ ctx.core.emit[`.${VAL.DATE}:getDay`] = ctx.core.emit[`.${VAL.DATE}:getUTCDay`]
684
+
588
685
  // ── UTC setter emit handlers ──────────────────────────────────────────────
589
686
 
590
687
  const emitDatePtr = (dateExpr) =>
@@ -11,7 +11,8 @@
11
11
  */
12
12
 
13
13
  import { typed, asF64, asI32, mkPtrIR, temp, tempI32, MAX_CLOSURE_ARITY, UNDEF_NAN } from '../src/ir.js'
14
- import { emit, isReassigned } from '../src/emit.js'
14
+ import { emit } from '../src/emit.js'
15
+ import { isReassigned } from '../src/ast.js'
15
16
  import { T, lookupValType, repOf, findFreeVars } from '../src/analyze.js'
16
17
  import { PTR, LAYOUT, inc, err } from '../src/ctx.js'
17
18
 
package/module/json.js CHANGED
@@ -50,7 +50,8 @@ export default (ctx) => {
50
50
  __jput_str: ['__char_at', '__str_byteLen'],
51
51
  __jp: ['__jp_val', '__jp_str', '__jp_num', '__jp_arr', '__jp_obj', '__sso_char', '__ptr_aux', '__ptr_type', '__ptr_offset', '__str_byteLen'],
52
52
  __jp_val: ['__jp_str', '__jp_num', '__jp_arr', '__jp_obj'],
53
- __jp_str: ['__sso_char', '__char_at', '__str_byteLen'],
53
+ __jp_str: ['__sso_char', '__char_at', '__str_byteLen', '__hex4', '__utf8_enc'],
54
+ __hex4: ['__hex1'],
54
55
  __jp_num: ['__pow10'],
55
56
  __jp_arr: ['__jp_val'],
56
57
  __jp_obj: ['__jp_val', '__jp_str', '__jp_schema_get', '__alloc_hdr', '__mkptr'],
@@ -319,8 +320,45 @@ export default (ctx) => {
319
320
  // SSO byte packing for ≤4-char ASCII keys, and FNV-1a hash. The hash is
320
321
  // stashed in $__jp_keyh so __jp_obj can use the prehashed insert and skip
321
322
  // a redundant __str_hash call.
323
+ // Hex nibble: '0'-'9' / 'a'-'f' / 'A'-'F' → 0..15; anything else → 0 (lenient).
324
+ ctx.core.stdlib['__hex1'] = `(func $__hex1 (param $c i32) (result i32)
325
+ (if (i32.le_u (i32.sub (local.get $c) (i32.const 48)) (i32.const 9))
326
+ (then (return (i32.sub (local.get $c) (i32.const 48)))))
327
+ (if (i32.le_u (i32.sub (i32.or (local.get $c) (i32.const 0x20)) (i32.const 97)) (i32.const 5))
328
+ (then (return (i32.sub (i32.or (local.get $c) (i32.const 0x20)) (i32.const 87)))))
329
+ (i32.const 0))`
330
+
331
+ // Read 4 hex bytes at absolute address $p → 16-bit value.
332
+ ctx.core.stdlib['__hex4'] = `(func $__hex4 (param $p i32) (result i32)
333
+ (i32.or (i32.or (i32.or
334
+ (i32.shl (call $__hex1 (i32.load8_u (local.get $p))) (i32.const 12))
335
+ (i32.shl (call $__hex1 (i32.load8_u (i32.add (local.get $p) (i32.const 1)))) (i32.const 8)))
336
+ (i32.shl (call $__hex1 (i32.load8_u (i32.add (local.get $p) (i32.const 2)))) (i32.const 4)))
337
+ (call $__hex1 (i32.load8_u (i32.add (local.get $p) (i32.const 3))))))`
338
+
339
+ // Encode code point $cp as UTF-8 at $off; returns bytes written (1-4).
340
+ ctx.core.stdlib['__utf8_enc'] = `(func $__utf8_enc (param $off i32) (param $cp i32) (result i32)
341
+ (if (i32.lt_u (local.get $cp) (i32.const 0x80))
342
+ (then (i32.store8 (local.get $off) (local.get $cp)) (return (i32.const 1))))
343
+ (if (i32.lt_u (local.get $cp) (i32.const 0x800))
344
+ (then
345
+ (i32.store8 (local.get $off) (i32.or (i32.const 0xC0) (i32.shr_u (local.get $cp) (i32.const 6))))
346
+ (i32.store8 (i32.add (local.get $off) (i32.const 1)) (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))))
347
+ (return (i32.const 2))))
348
+ (if (i32.lt_u (local.get $cp) (i32.const 0x10000))
349
+ (then
350
+ (i32.store8 (local.get $off) (i32.or (i32.const 0xE0) (i32.shr_u (local.get $cp) (i32.const 12))))
351
+ (i32.store8 (i32.add (local.get $off) (i32.const 1)) (i32.or (i32.const 0x80) (i32.and (i32.shr_u (local.get $cp) (i32.const 6)) (i32.const 0x3F))))
352
+ (i32.store8 (i32.add (local.get $off) (i32.const 2)) (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))))
353
+ (return (i32.const 3))))
354
+ (i32.store8 (local.get $off) (i32.or (i32.const 0xF0) (i32.shr_u (local.get $cp) (i32.const 18))))
355
+ (i32.store8 (i32.add (local.get $off) (i32.const 1)) (i32.or (i32.const 0x80) (i32.and (i32.shr_u (local.get $cp) (i32.const 12)) (i32.const 0x3F))))
356
+ (i32.store8 (i32.add (local.get $off) (i32.const 2)) (i32.or (i32.const 0x80) (i32.and (i32.shr_u (local.get $cp) (i32.const 6)) (i32.const 0x3F))))
357
+ (i32.store8 (i32.add (local.get $off) (i32.const 3)) (i32.or (i32.const 0x80) (i32.and (local.get $cp) (i32.const 0x3F))))
358
+ (i32.const 4))`
359
+
322
360
  ctx.core.stdlib['__jp_str'] = `(func $__jp_str (result f64)
323
- (local $start i32) (local $ch i32) (local $len i32) (local $off i32) (local $i i32) (local $simple i32) (local $sso i32) (local $h i32)
361
+ (local $start i32) (local $ch i32) (local $len i32) (local $off i32) (local $i i32) (local $simple i32) (local $sso i32) (local $h i32) (local $cp i32)
324
362
  (local.set $start (global.get $__jppos))
325
363
  (local.set $simple (i32.const 1))
326
364
  (local.set $h (i32.const 0x811c9dc5))
@@ -382,12 +420,32 @@ export default (ctx) => {
382
420
  ${ADV(1)}
383
421
  (local.set $ch ${PEEK})
384
422
  ${ADV(1)}
385
- ;; Decode escape: n→10 t→9 r→13 b→8 f→12, else literal
386
- (if (i32.eq (local.get $ch) (i32.const 110)) (then (local.set $ch (i32.const 10))))
387
- (if (i32.eq (local.get $ch) (i32.const 116)) (then (local.set $ch (i32.const 9))))
388
- (if (i32.eq (local.get $ch) (i32.const 114)) (then (local.set $ch (i32.const 13))))
389
- (if (i32.eq (local.get $ch) (i32.const 98)) (then (local.set $ch (i32.const 8))))
390
- (if (i32.eq (local.get $ch) (i32.const 102)) (then (local.set $ch (i32.const 12)))))
423
+ (if (i32.eq (local.get $ch) (i32.const 117)) ;; \\uXXXX
424
+ (then
425
+ (local.set $cp (call $__hex4 (i32.add (global.get $__jpstr) (global.get $__jppos))))
426
+ ${ADV(4)}
427
+ ;; High surrogate immediately followed by \\uXXXX low surrogate → combine.
428
+ (if (i32.and
429
+ (i32.eq (i32.and (local.get $cp) (i32.const 0xFC00)) (i32.const 0xD800))
430
+ (i32.and (i32.eq ${PEEK} (i32.const 92))
431
+ (i32.eq (i32.load8_u (i32.add (global.get $__jpstr) (i32.add (global.get $__jppos) (i32.const 1)))) (i32.const 117))))
432
+ (then
433
+ ${ADV(2)}
434
+ (local.set $i (call $__hex4 (i32.add (global.get $__jpstr) (global.get $__jppos))))
435
+ ${ADV(4)}
436
+ (local.set $cp (i32.add (i32.const 0x10000)
437
+ (i32.or (i32.shl (i32.and (local.get $cp) (i32.const 0x3FF)) (i32.const 10))
438
+ (i32.and (local.get $i) (i32.const 0x3FF)))))))
439
+ (local.set $len (i32.add (local.get $len)
440
+ (call $__utf8_enc (i32.add (local.get $off) (local.get $len)) (local.get $cp))))
441
+ (br $l2))
442
+ (else
443
+ ;; Decode simple escape: n→10 t→9 r→13 b→8 f→12, else literal char.
444
+ (if (i32.eq (local.get $ch) (i32.const 110)) (then (local.set $ch (i32.const 10))))
445
+ (if (i32.eq (local.get $ch) (i32.const 116)) (then (local.set $ch (i32.const 9))))
446
+ (if (i32.eq (local.get $ch) (i32.const 114)) (then (local.set $ch (i32.const 13))))
447
+ (if (i32.eq (local.get $ch) (i32.const 98)) (then (local.set $ch (i32.const 8))))
448
+ (if (i32.eq (local.get $ch) (i32.const 102)) (then (local.set $ch (i32.const 12)))))))
391
449
  (else ${ADV(1)}))
392
450
  (i32.store8 (i32.add (local.get $off) (local.get $len)) (local.get $ch))
393
451
  (local.set $len (i32.add (local.get $len) (i32.const 1)))
package/module/number.js CHANGED
@@ -10,7 +10,8 @@
10
10
  */
11
11
 
12
12
  import { typed, asF64, asI32, asI64, toNumF64, NULL_NAN, UNDEF_NAN, temp, tempI32, tempI64 } from '../src/ir.js'
13
- import { emit, isReassigned } from '../src/emit.js'
13
+ import { emit } from '../src/emit.js'
14
+ import { isReassigned } from '../src/ast.js'
14
15
  import { valTypeOf, VAL } from '../src/analyze.js'
15
16
  import { inc, PTR } from '../src/ctx.js'
16
17
 
@@ -22,6 +23,7 @@ export default (ctx) => {
22
23
  __to_num: ['__char_at', '__str_byteLen', '__pow10', '__to_str'],
23
24
  __to_bigint: ['__char_at', '__str_byteLen'],
24
25
  __parseInt: ['__char_at', '__str_byteLen'],
26
+ __parseFloat: ['__char_at', '__str_byteLen', '__pow10', '__to_str'],
25
27
  })
26
28
 
27
29
 
@@ -346,7 +348,6 @@ export default (ctx) => {
346
348
  ['f64.eq', ['local.get', `$${t}`], ['f64.trunc', ['local.get', `$${t}`]]]], 'i32')
347
349
  }
348
350
 
349
- // parseInt(str, radix) — parse string to integer
350
351
  ctx.core.stdlib['__parseInt'] = `(func $__parseInt (param $str i64) (param $radix i32) (result f64)
351
352
  (local $off i32) (local $len i32) (local $i i32) (local $c i32) (local $neg i32)
352
353
  (local $result f64) (local $digit i32) (local $seen i32) (local $f f64)
@@ -592,14 +593,135 @@ export default (ctx) => {
592
593
  (then (i64.sub (i64.const 0) (local.get $result)))
593
594
  (else (local.get $result)))))`
594
595
 
596
+ ctx.core.stdlib['__parseFloat'] = `(func $__parseFloat (param $v i64) (result f64)
597
+ (local $t i32) (local $len i32) (local $i i32) (local $c i32) (local $neg i32)
598
+ (local $seen i32) (local $exp i32) (local $expNeg i32) (local $expDigits i32)
599
+ (local $dot i32) (local $sigDigits i32) (local $decExp i32) (local $dropped i32) (local $round i32)
600
+ (local $result f64) (local $f f64)
601
+ (local.set $f (f64.reinterpret_i64 (local.get $v)))
602
+ (if (f64.eq (local.get $f) (local.get $f)) (then (return (local.get $f))))
603
+ (local.set $t (call $__ptr_type (local.get $v)))
604
+ ;; parseFloat first applies ToString, then parses the longest decimal prefix.
605
+ ;; Unlike Number(), empty strings and non-decimal prefixes produce NaN/0
606
+ ;; according to the consumed prefix rather than whole-string numeric coercion.
607
+ (if (i32.ne (local.get $t) (i32.const ${PTR.STRING}))
608
+ (then
609
+ (local.set $v (call $__to_str (local.get $v)))
610
+ (local.set $t (call $__ptr_type (local.get $v)))
611
+ (if (i32.ne (local.get $t) (i32.const ${PTR.STRING}))
612
+ (then (return (f64.const nan))))))
613
+ (local.set $len (call $__str_byteLen (local.get $v)))
614
+ ;; Skip leading whitespace.
615
+ (block $ws (loop $wsl
616
+ (br_if $ws (i32.ge_s (local.get $i) (local.get $len)))
617
+ (br_if $ws (i32.gt_s (call $__char_at (local.get $v) (local.get $i)) (i32.const 32)))
618
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
619
+ (br $wsl)))
620
+ ;; Sign.
621
+ (if (i32.and (i32.lt_s (local.get $i) (local.get $len))
622
+ (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 45)))
623
+ (then (local.set $neg (i32.const 1)) (local.set $i (i32.add (local.get $i) (i32.const 1)))))
624
+ (if (i32.and (i32.lt_s (local.get $i) (local.get $len))
625
+ (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 43)))
626
+ (then (local.set $i (i32.add (local.get $i) (i32.const 1)))))
627
+ ;; Decimal significand. Keep 17 significant decimal digits, track the
628
+ ;; base-10 exponent for skipped digits, and round once before pow10 scaling.
629
+ (block $numDone (loop $numLoop
630
+ (br_if $numDone (i32.ge_s (local.get $i) (local.get $len)))
631
+ (local.set $c (call $__char_at (local.get $v) (local.get $i)))
632
+ (if (i32.and (i32.eq (local.get $c) (i32.const 46)) (i32.eqz (local.get $dot)))
633
+ (then
634
+ (local.set $dot (i32.const 1))
635
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
636
+ (br $numLoop)))
637
+ (br_if $numDone
638
+ (i32.or
639
+ (i32.lt_s (local.get $c) (i32.const 48))
640
+ (i32.gt_s (local.get $c) (i32.const 57))))
641
+ (local.set $seen (i32.const 1))
642
+ (local.set $c (i32.sub (local.get $c) (i32.const 48)))
643
+ (if (i32.and (i32.eqz (local.get $sigDigits)) (i32.eqz (local.get $c)))
644
+ (then
645
+ (if (local.get $dot) (then (local.set $decExp (i32.sub (local.get $decExp) (i32.const 1)))))
646
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
647
+ (br $numLoop)))
648
+ (if (i32.lt_s (local.get $sigDigits)
649
+ (if (result i32) (local.get $dot) (then (i32.const 16)) (else (i32.const 17))))
650
+ (then
651
+ (local.set $result
652
+ (f64.add
653
+ (f64.mul (local.get $result) (f64.const 10))
654
+ (f64.convert_i32_s (local.get $c))))
655
+ (local.set $sigDigits (i32.add (local.get $sigDigits) (i32.const 1)))
656
+ (if (local.get $dot) (then (local.set $decExp (i32.sub (local.get $decExp) (i32.const 1))))))
657
+ (else
658
+ (if (i32.eqz (local.get $dropped))
659
+ (then (if (i32.ge_s (local.get $c) (i32.const 5)) (then (local.set $round (i32.const 1))))))
660
+ (local.set $dropped (i32.const 1))
661
+ (if (i32.eqz (local.get $dot)) (then (local.set $decExp (i32.add (local.get $decExp) (i32.const 1)))))))
662
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
663
+ (br $numLoop)))
664
+ (if (i32.eqz (local.get $seen)) (then (return (f64.const nan))))
665
+ (if (local.get $round) (then (local.set $result (f64.add (local.get $result) (f64.const 1)))))
666
+ ;; Scientific notation.
667
+ (if (i32.and (i32.lt_s (local.get $i) (local.get $len))
668
+ (i32.or
669
+ (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 101))
670
+ (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 69))))
671
+ (then
672
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
673
+ (if (i32.and (i32.lt_s (local.get $i) (local.get $len))
674
+ (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 45)))
675
+ (then (local.set $expNeg (i32.const 1)) (local.set $i (i32.add (local.get $i) (i32.const 1)))))
676
+ (if (i32.and (i32.lt_s (local.get $i) (local.get $len))
677
+ (i32.eq (call $__char_at (local.get $v) (local.get $i)) (i32.const 43)))
678
+ (then (local.set $i (i32.add (local.get $i) (i32.const 1)))))
679
+ (block $expDone (loop $expLoop
680
+ (br_if $expDone (i32.ge_s (local.get $i) (local.get $len)))
681
+ (local.set $c (call $__char_at (local.get $v) (local.get $i)))
682
+ (br_if $expDone
683
+ (i32.or
684
+ (i32.lt_s (local.get $c) (i32.const 48))
685
+ (i32.gt_s (local.get $c) (i32.const 57))))
686
+ (local.set $exp
687
+ (i32.add
688
+ (i32.mul (local.get $exp) (i32.const 10))
689
+ (i32.sub (local.get $c) (i32.const 48))))
690
+ (local.set $expDigits (i32.add (local.get $expDigits) (i32.const 1)))
691
+ (local.set $i (i32.add (local.get $i) (i32.const 1)))
692
+ (br $expLoop)))
693
+ (if (local.get $expDigits)
694
+ (then
695
+ (if (local.get $expNeg)
696
+ (then (local.set $decExp (i32.sub (local.get $decExp) (local.get $exp))))
697
+ (else (local.set $decExp (i32.add (local.get $decExp) (local.get $exp)))))))))
698
+ (if (i32.gt_s (local.get $decExp) (i32.const 0))
699
+ (then (local.set $result (f64.mul (local.get $result) (call $__pow10 (local.get $decExp))))))
700
+ (if (i32.lt_s (local.get $decExp) (i32.const 0))
701
+ (then (local.set $result (f64.div (local.get $result) (call $__pow10 (i32.sub (i32.const 0) (local.get $decExp)))))))
702
+ (if (result f64) (local.get $neg) (then (f64.neg (local.get $result))) (else (local.get $result))))`
703
+
595
704
  ctx.core.emit['Number.parseInt'] = (x, radix) => {
596
- inc('__parseInt')
705
+ needParseInt()
597
706
  return typed(['call', '$__parseInt', asI64(emit(x)), radix ? asI32(emit(radix)) : ['i32.const', 0]], 'f64')
598
707
  }
599
708
  ctx.core.emit['parseInt'] = ctx.core.emit['Number.parseInt']
709
+ const addImportOnce = (ctx, mod, name, fn) => {
710
+ if (ctx.module.imports.some(i => i[1] === `"${mod}"` && i[2] === `"${name}"`)) return
711
+ ctx.module.imports.push(['import', `"${mod}"`, `"${name}"`, fn])
712
+ }
713
+ const needParseFloat = () => addImportOnce(ctx, 'env', 'parseFloat',
714
+ ['func', '$__parseFloat', ['param', 'i64'], ['result', 'f64']])
715
+ const needParseInt = () => addImportOnce(ctx, 'env', 'parseInt',
716
+ ['func', '$__parseInt', ['param', 'i64'], ['param', 'i32'], ['result', 'f64']])
717
+
600
718
  ctx.core.emit['Number.parseFloat'] = (x) => {
601
- inc('__to_num')
602
- return typed(['call', '$__to_num', asI64(emit(x))], 'f64')
719
+ if (ctx.transform.host === 'wasi') {
720
+ inc('__parseFloat')
721
+ return typed(['call', '$__parseFloat', asI64(emit(x))], 'f64')
722
+ }
723
+ needParseFloat()
724
+ return typed(['call', '$__parseFloat', asI64(emit(x))], 'f64')
603
725
  }
604
726
  ctx.core.emit['parseFloat'] = ctx.core.emit['Number.parseFloat']
605
727