jz 0.2.0 → 0.2.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/README.md CHANGED
@@ -32,7 +32,7 @@ JZ distills the modern functional core – the "good parts" ([Crockford](https:/
32
32
  | Parsing / transforms | Async / I/O-heavy logic |
33
33
  | WASM utilities | JavaScript runtime |
34
34
 
35
- Inspired by [porffor](https://github.com/CanadaHonk/porffor) and [piezo](https://github.com/dy/piezo).
35
+ <!-- Inspired by [porffor](https://github.com/CanadaHonk/porffor) and [piezo](https://github.com/dy/piezo). -->
36
36
  <!-- Used internally by: web-audio-api, color-space, audiojs -->
37
37
 
38
38
 
@@ -66,7 +66,7 @@ Options are passed as `jz(source, opts)` or `compile(source, opts)`. Common ones
66
66
  | `jzify: true` | Accept broader JS patterns such as `var`, `function`, `switch`, `arguments`, `==`, and `undefined` by lowering them to the JZ subset. |
67
67
  | `modules: { specifier: source }` | Bundle static ES imports into one WASM module. CLI import resolution does this from files automatically. |
68
68
  | `imports: { mod: host }` | Wire host namespaces/functions used by `import { fn } from "mod"`; functions may be plain JS functions or `{ fn, returns }` specs. |
69
- | `memory` | Pass `memory: N` to create owned memory with `N` initial pages, or pass `memory: jz.memory()` / `WebAssembly.Memory` to share memory across modules. `memoryPages` remains as a legacy alias for the page count. |
69
+ | `memory` | Pass `memory: N` to create owned memory with `N` initial pages, or pass `memory: jz.memory()` / `WebAssembly.Memory` to share memory across modules. |
70
70
  | `host: 'js' \| 'wasi'` | Select runtime-service lowering. Default `js` uses small `env.*` imports auto-wired by `jz()`; `wasi` emits WASI Preview 1 imports for wasmtime/wasmer/deno. |
71
71
  | `optimize` | `false`/`0` disables optimization, `1` keeps cheap size passes, `true`/`2` is the default, `3` enables aggressive experimental passes, object form overrides individual passes. |
72
72
  | `strict: true` | Reject dynamic fallbacks such as unknown receiver method calls, `obj[k]`, and `for-in` instead of emitting JS-host dynamic dispatch. |
@@ -495,7 +495,6 @@ cc program.c -o program
495
495
  | [poly](bench/poly/poly.js) | 0.27ms<br>1.4kB | 1.62ms<br>1014 B | fails | 0.73ms<br>1.3kB | 0.81ms<br>359 B | 0.57ms | 0.79ms | 0.89ms | 0.63ms | 0.60ms |
496
496
  | [callback](bench/callback/callback.js) | 0.03ms<br>1.6kB | 0.69ms<br>828 B | fails | 1.04ms<br>1.9kB | 0.24ms<br>267 B | 0.08ms | 0.23ms | 0.01ms | 0.12ms | 1.78ms |
497
497
  | [json](bench/json/json.js) | 0.24ms<br>11.0kB | 0.37ms<br>1.2kB | fails | — | — | 0.03ms | 1.05ms | <0.01ms | 0.03ms | 1.21ms |
498
- | [json-dynamic](bench/json-dynamic/json-dynamic.js) | 0.24ms<br>11.3kB | 0.39ms<br>1.2kB | — | — | — | — | — | — | — | — |
499
498
  | [watr](bench/watr/watr.js) | 1.04ms<br>169.8kB | 1.05ms<br>2.6kB | fails | — | — | — | — | — | — | — |
500
499
 
501
500
  _Numbers from `node bench/bench.mjs` on Apple Silicon. Porffor cells were refreshed with `porf` 0.61.13; `fails` means the latest Porffor compiler/runtime did not complete that benchmark._
package/index.js CHANGED
@@ -163,8 +163,6 @@ jz.memory = enhanceMemory
163
163
  * receiver method calls) at compile time. Avoids pulling dynamic-dispatch stdlib
164
164
  * into output; large size win for static programs.
165
165
  * @param {WebAssembly.Memory|number} [opts.memory] - Shared memory import, or
166
- * initial page count for owned memory when a number is passed.
167
- * @param {number} [opts.memoryPages] - Legacy alias for owned/imported memory
168
166
  * initial page count. Prefer `memory: N` for owned memory.
169
167
  * @param {boolean} [opts.alloc=true] - Export raw allocator helpers
170
168
  * (`_alloc`, `_clear`) for JS memory wrapping. Set false for standalone host-run
@@ -194,7 +192,6 @@ jz.compile = (code, opts = {}) => {
194
192
 
195
193
  if (typeof opts.memory === 'number') ctx.memory.pages = opts.memory
196
194
  else if (opts.memory) ctx.memory.shared = true
197
- if (opts.memoryPages) ctx.memory.pages = opts.memoryPages
198
195
  if (opts.modules) ctx.module.importSources = opts.modules
199
196
  if (opts.imports) {
200
197
  ctx.module.hostImports = opts.imports
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'],
@@ -149,6 +153,66 @@ export default (ctx) => {
149
153
  (f64.const ${MS_PER_DAY}))
150
154
  (call $__date_make_time (local.get $hours) (local.get $minutes) (local.get $seconds) (local.get $ms)))))`
151
155
 
156
+ ctx.core.stdlib['__date_digit'] = `(func $__date_digit (param $str i64) (param $i i32) (result i32)
157
+ (local $c i32)
158
+ (local.set $c (call $__char_at (local.get $str) (local.get $i)))
159
+ (if (result i32)
160
+ (i32.and (i32.ge_s (local.get $c) (i32.const 48)) (i32.le_s (local.get $c) (i32.const 57)))
161
+ (then (i32.sub (local.get $c) (i32.const 48)))
162
+ (else (i32.const -1))))`
163
+
164
+ ctx.core.stdlib['__date_parse_iso_date'] = `(func $__date_parse_iso_date (param $str i64) (result f64)
165
+ (local $y f64) (local $m f64) (local $d f64)
166
+ (local $d0 i32) (local $d1 i32) (local $d2 i32) (local $d3 i32)
167
+ (if (i32.lt_s (call $__str_byteLen (local.get $str)) (i32.const 10))
168
+ (then (return (f64.const nan))))
169
+ (if (i32.ne (call $__char_at (local.get $str) (i32.const 4)) (i32.const 45))
170
+ (then (return (f64.const nan))))
171
+ (if (i32.ne (call $__char_at (local.get $str) (i32.const 7)) (i32.const 45))
172
+ (then (return (f64.const nan))))
173
+
174
+ (local.set $d0 (call $__date_digit (local.get $str) (i32.const 0)))
175
+ (local.set $d1 (call $__date_digit (local.get $str) (i32.const 1)))
176
+ (local.set $d2 (call $__date_digit (local.get $str) (i32.const 2)))
177
+ (local.set $d3 (call $__date_digit (local.get $str) (i32.const 3)))
178
+ (if (i32.or (i32.or (i32.lt_s (local.get $d0) (i32.const 0)) (i32.lt_s (local.get $d1) (i32.const 0)))
179
+ (i32.or (i32.lt_s (local.get $d2) (i32.const 0)) (i32.lt_s (local.get $d3) (i32.const 0))))
180
+ (then (return (f64.const nan))))
181
+ (local.set $y (f64.convert_i32_s
182
+ (i32.add
183
+ (i32.add (i32.mul (local.get $d0) (i32.const 1000)) (i32.mul (local.get $d1) (i32.const 100)))
184
+ (i32.add (i32.mul (local.get $d2) (i32.const 10)) (local.get $d3)))))
185
+
186
+ (local.set $d0 (call $__date_digit (local.get $str) (i32.const 5)))
187
+ (local.set $d1 (call $__date_digit (local.get $str) (i32.const 6)))
188
+ (if (i32.or (i32.lt_s (local.get $d0) (i32.const 0)) (i32.lt_s (local.get $d1) (i32.const 0)))
189
+ (then (return (f64.const nan))))
190
+ (local.set $m (f64.convert_i32_s (i32.add (i32.mul (local.get $d0) (i32.const 10)) (local.get $d1))))
191
+ (if (i32.or (f64.lt (local.get $m) (f64.const 1)) (f64.gt (local.get $m) (f64.const 12)))
192
+ (then (return (f64.const nan))))
193
+
194
+ (local.set $d0 (call $__date_digit (local.get $str) (i32.const 8)))
195
+ (local.set $d1 (call $__date_digit (local.get $str) (i32.const 9)))
196
+ (if (i32.or (i32.lt_s (local.get $d0) (i32.const 0)) (i32.lt_s (local.get $d1) (i32.const 0)))
197
+ (then (return (f64.const nan))))
198
+ (local.set $d (f64.convert_i32_s (i32.add (i32.mul (local.get $d0) (i32.const 10)) (local.get $d1))))
199
+ (if (i32.or (f64.lt (local.get $d) (f64.const 1)) (f64.gt (local.get $d) (f64.const 31)))
200
+ (then (return (f64.const nan))))
201
+
202
+ (call $__date_time_clip
203
+ (f64.mul
204
+ (call $__date_make_day (local.get $y) (f64.sub (local.get $m) (f64.const 1)) (local.get $d))
205
+ (f64.const ${MS_PER_DAY}))))`
206
+
207
+ ctx.core.stdlib['__date_from_value'] = `(func $__date_from_value (param $v i64) (result f64)
208
+ (local $f f64)
209
+ (local.set $f (f64.reinterpret_i64 (local.get $v)))
210
+ (if (f64.eq (local.get $f) (local.get $f))
211
+ (then (return (call $__date_time_clip (local.get $f)))))
212
+ (if (i32.eq (call $__ptr_type (local.get $v)) (i32.const ${PTR.STRING}))
213
+ (then (return (call $__date_parse_iso_date (local.get $v)))))
214
+ (call $__date_time_clip (call $__to_num (local.get $v))))`
215
+
152
216
  // ── Time decomposition (ECMA-262 §21.4.1) ────────────────────────────────
153
217
 
154
218
  ctx.core.stdlib['__date_day'] = `(func $__date_day (param $t f64) (result f64)
@@ -513,13 +577,26 @@ export default (ctx) => {
513
577
  // Represented as PTR.OBJECT with a single f64 slot at offset 0 (the time value).
514
578
  // No schemaId (aux=0); dynamic property access falls through to undefined.
515
579
 
516
- ctx.core.emit['new.Date'] = (ms) => {
580
+ ctx.core.emit['new.Date'] = (ms, month, date, hours, minutes, seconds, millis) => {
517
581
  let timeVal
518
582
  if (ms === undefined || ms === null) {
519
- timeVal = typed(['f64.const', NaN], 'f64')
583
+ const now = ctx.core.emit['Date.now']
584
+ if (!now) throw new Error('Date constructor requires Date.now support')
585
+ timeVal = now()
586
+ } else if (!missingArg(month)) {
587
+ inc('__date_utc')
588
+ timeVal = typed(['call', '$__date_utc',
589
+ asF64(dateArg(ms, NaN, true)),
590
+ asF64(dateArg(month, 0)),
591
+ asF64(dateArg(date, 1)),
592
+ asF64(dateArg(hours, 0)),
593
+ asF64(dateArg(minutes, 0)),
594
+ asF64(dateArg(seconds, 0)),
595
+ asF64(dateArg(millis, 0)),
596
+ ], 'f64')
520
597
  } else {
521
- inc('__date_time_clip')
522
- timeVal = typed(['call', '$__date_time_clip', toNumF64(ms, emit(ms))], 'f64')
598
+ inc('__date_from_value')
599
+ timeVal = typed(['call', '$__date_from_value', ['i64.reinterpret_f64', asF64(emit(ms))]], 'f64')
523
600
  }
524
601
  const out = allocPtr({ type: PTR.OBJECT, len: 1, cap: 1, stride: 8, tag: 'date' })
525
602
  return typed(['block', ['result', 'f64'],
@@ -585,6 +662,21 @@ export default (ctx) => {
585
662
  ctx.core.emit['.getUTCMilliseconds'] = dateGetter('__date_ms_from_time')
586
663
  ctx.core.emit[`.${VAL.DATE}:getUTCMilliseconds`] = dateGetter('__date_ms_from_time')
587
664
 
665
+ // UTC-backed local getters. Full timezone-aware local time is intentionally
666
+ // staged separately; these aliases make deterministic/server Date use cases
667
+ // available without a timezone database.
668
+ ctx.core.emit['.getFullYear'] = ctx.core.emit['.getUTCFullYear']
669
+ ctx.core.emit[`.${VAL.DATE}:getFullYear`] = ctx.core.emit[`.${VAL.DATE}:getUTCFullYear`]
670
+
671
+ ctx.core.emit['.getMonth'] = ctx.core.emit['.getUTCMonth']
672
+ ctx.core.emit[`.${VAL.DATE}:getMonth`] = ctx.core.emit[`.${VAL.DATE}:getUTCMonth`]
673
+
674
+ ctx.core.emit['.getDate'] = ctx.core.emit['.getUTCDate']
675
+ ctx.core.emit[`.${VAL.DATE}:getDate`] = ctx.core.emit[`.${VAL.DATE}:getUTCDate`]
676
+
677
+ ctx.core.emit['.getDay'] = ctx.core.emit['.getUTCDay']
678
+ ctx.core.emit[`.${VAL.DATE}:getDay`] = ctx.core.emit[`.${VAL.DATE}:getUTCDay`]
679
+
588
680
  // ── UTC setter emit handlers ──────────────────────────────────────────────
589
681
 
590
682
  const emitDatePtr = (dateExpr) =>
package/module/string.js CHANGED
@@ -959,6 +959,11 @@ export default (ctx) => {
959
959
  return typed(['call', '$__str_case', asI64(emit(str)), ['i32.const', 65], ['i32.const', 90], ['i32.const', 32]], 'f64')
960
960
  }
961
961
 
962
+ // Locale-specific casing needs ICU/CLDR data. jz intentionally has no
963
+ // runtime, so this follows the existing ASCII-only lowercase helper and
964
+ // ignores optional locale arguments.
965
+ ctx.core.emit['.toLocaleLowerCase'] = ctx.core.emit['.toLowerCase']
966
+
962
967
  // Byte-wise variant of String.prototype.localeCompare. Returns -1/0/1 from
963
968
  // an unsigned byte-by-byte compare with shorter-string-sorts-first tiebreak.
964
969
  // NOT locale-aware: real localeCompare is ICU-driven (CLDR collation, case
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jz",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Modern functional JS compiling to WASM",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/src/analyze.js CHANGED
@@ -359,7 +359,7 @@ export function valTypeOf(expr) {
359
359
  if (method === 'add' || method === 'delete') return VAL.SET
360
360
  if (method === 'set') return VAL.MAP
361
361
  // String-returning methods
362
- if (['toUpperCase', 'toLowerCase', 'trim', 'trimStart', 'trimEnd',
362
+ if (['toUpperCase', 'toLowerCase', 'toLocaleLowerCase', 'trim', 'trimStart', 'trimEnd',
363
363
  'repeat', 'padStart', 'padEnd', 'replace', 'replaceAll', 'charAt', 'substring'].includes(method)) return VAL.STRING
364
364
  if (method === 'split') return VAL.ARRAY
365
365
  // slice/concat preserve caller type (string.slice → string, array.slice → array)
@@ -1263,7 +1263,7 @@ export function invalidateLocalsCache(body) {
1263
1263
  // concat) that strings share with arrays/typed-arrays.
1264
1264
  const STRING_ONLY_METHODS = new Set([
1265
1265
  'charCodeAt', 'charAt', 'codePointAt', 'startsWith', 'endsWith',
1266
- 'toUpperCase', 'toLowerCase', 'normalize', 'localeCompare',
1266
+ 'toUpperCase', 'toLowerCase', 'toLocaleLowerCase', 'normalize', 'localeCompare',
1267
1267
  'padStart', 'padEnd', 'repeat', 'trimStart', 'trimEnd', 'trim',
1268
1268
  'matchAll', 'match', 'replace', 'replaceAll', 'split',
1269
1269
  ])
package/src/autoload.js CHANGED
@@ -14,7 +14,7 @@ export const PROP_MODULES = Object.assign(Object.create(null), {
14
14
  every: ['core', 'array'], some: ['core', 'array'], flat: ['core', 'array'], flatMap: ['core', 'array'],
15
15
  join: ['core', 'array'], copyWithin: ['core', 'array'], at: ['core', 'array'],
16
16
  charAt: ['core', 'string'], charCodeAt: ['core', 'string'], codePointAt: ['core', 'string'],
17
- toUpperCase: ['core', 'string'], toLowerCase: ['core', 'string'], trim: ['core', 'string'],
17
+ toUpperCase: ['core', 'string'], toLowerCase: ['core', 'string'], toLocaleLowerCase: ['core', 'string'], trim: ['core', 'string'],
18
18
  trimStart: ['core', 'string'], trimEnd: ['core', 'string'],
19
19
  split: ['core', 'string'], replace: ['core', 'string'], replaceAll: ['core', 'string'],
20
20
  repeat: ['core', 'string'], startsWith: ['core', 'string'], endsWith: ['core', 'string'],
@@ -168,7 +168,7 @@ export const includeForRuntimeCtor = name => {
168
168
  const kind = runtimeCtorKind(name)
169
169
  if (kind === 'typedarray') includeMods('core', 'typedarray')
170
170
  else if (kind === 'collection') includeMods('core', 'collection')
171
- else if (kind === 'date') includeMods('core', 'date')
171
+ else if (kind === 'date') includeMods('core', 'console', 'date')
172
172
  return kind
173
173
  }
174
174
 
package/src/emit.js CHANGED
@@ -867,6 +867,20 @@ const cmpOp = (i32op, f64op, fn) => (a, b) => {
867
867
  inc('__str_cmp')
868
868
  return typed([`i32.${i32op}`, ['call', '$__str_cmp', asI64(va), asI64(vb)], ['i32.const', 0]], 'i32')
869
869
  }
870
+ if (vta === VAL.DATE || vtb === VAL.DATE) {
871
+ const dateNum = (node, v, vt) => {
872
+ if (vt !== VAL.DATE) return toNumF64(node, v)
873
+ const ptr = v.ptrKind === VAL.DATE
874
+ ? v
875
+ : ['i32.wrap_i64', ['i64.reinterpret_f64', asF64(v)]]
876
+ return typed(['f64.load', ptr], 'f64')
877
+ }
878
+ return typed([`f64.${f64op}`, dateNum(a, va, vta), dateNum(b, vb, vtb)], 'i32')
879
+ }
880
+ if (vtb === VAL.NUMBER && needsRelationalToNumber(a, vta))
881
+ return typed([`f64.${f64op}`, toNumF64(a, va), asF64(vb)], 'i32')
882
+ if (vta === VAL.NUMBER && needsRelationalToNumber(b, vtb))
883
+ return typed([`f64.${f64op}`, asF64(va), toNumF64(b, vb)], 'i32')
870
884
  const ai = intConstValue(a), bi = intConstValue(b)
871
885
  if (va.type === 'i32' && bi != null) return typed([`i32.${i32op}`, va, ['i32.const', bi]], 'i32')
872
886
  if (vb.type === 'i32' && ai != null) return typed([`i32.${i32op}`, ['i32.const', ai], vb], 'i32')
@@ -874,6 +888,16 @@ const cmpOp = (i32op, f64op, fn) => (a, b) => {
874
888
  ? typed([`i32.${i32op}`, va, vb], 'i32') : typed([`f64.${f64op}`, asF64(va), asF64(vb)], 'i32')
875
889
  }
876
890
 
891
+ function needsRelationalToNumber(expr, vt) {
892
+ if (vt === VAL.STRING) return true
893
+ if (vt != null) return false
894
+ return mayReadBoxedValue(expr)
895
+ }
896
+
897
+ function mayReadBoxedValue(expr) {
898
+ return Array.isArray(expr) && (expr[0] === '.' || expr[0] === '[]' || expr[0] === '?.' || expr[0] === '?.[]')
899
+ }
900
+
877
901
  function intConstValue(expr) {
878
902
  if (typeof expr === 'number' && Number.isInteger(expr)) return expr
879
903
  if (Array.isArray(expr) && expr[0] == null && typeof expr[1] === 'number' && Number.isInteger(expr[1])) return expr[1]
package/src/ir.js CHANGED
@@ -347,6 +347,12 @@ export function toNumF64(node, v) {
347
347
  if (v.type === 'i32' || isLit(v)) return asF64(v)
348
348
  const vt = keyValType(node)
349
349
  if (vt === VAL.NUMBER || vt === VAL.BIGINT) return asF64(v)
350
+ if (vt === VAL.DATE) {
351
+ const ptr = v.ptrKind === VAL.DATE
352
+ ? v
353
+ : ['i32.wrap_i64', ['i64.reinterpret_f64', asF64(v)]]
354
+ return typed(['f64.load', ptr], 'f64')
355
+ }
350
356
  // intCertain locals: every reachable def is integer-valued, so the binding
351
357
  // never carries a NaN-boxed pointer — skip the __to_num wrapper.
352
358
  if (typeof node === 'string' && repOf(node)?.intCertain === true) return asF64(v)
package/src/plan.js CHANGED
@@ -329,6 +329,24 @@ const collectScalarTypedArrayWrites = (node, name, len, out = new Set()) => {
329
329
  return out
330
330
  }
331
331
 
332
+ const hasScalarTypedArrayRead = (node, name) => {
333
+ if (!Array.isArray(node)) return false
334
+ const op = node[0]
335
+ const isTarget = target => Array.isArray(target) && target[0] === '[]' && target[1] === name
336
+ if ((op === '++' || op === '--') && isTarget(node[1])) return true
337
+ if (ASSIGN_TARGET_OPS.has(op)) {
338
+ if (isTarget(node[1])) {
339
+ if (op !== '=') return true
340
+ for (let i = 2; i < node.length; i++) if (hasScalarTypedArrayRead(node[i], name)) return true
341
+ return false
342
+ }
343
+ }
344
+ if (op === '[]' && node[1] === name) return true
345
+ if (op === '=>') return false
346
+ for (let i = 1; i < node.length; i++) if (hasScalarTypedArrayRead(node[i], name)) return true
347
+ return false
348
+ }
349
+
332
350
  const scalarizeTypedArrayLiteralSeq = (seq) => {
333
351
  if (!Array.isArray(seq) || seq[0] !== ';') return { node: seq, changed: false }
334
352
  let changed = false
@@ -553,6 +571,7 @@ const scalarTypedParamCandidates = (func, sites, fixedByFunc) => {
553
571
 
554
572
  const scalarizeTypedArrayParams = (func, paramCands) => {
555
573
  for (const [name, c] of [...paramCands]) if (!safeScalarTypedArrayUse(func.body, name, c.len)) paramCands.delete(name)
574
+ for (const [name] of [...paramCands]) if (!hasScalarTypedArrayRead(func.body, name)) paramCands.delete(name)
556
575
  if (!paramCands.size) return { body: func.body, changed: false }
557
576
  const arrays = new Map()
558
577
  for (const [name, c] of paramCands) {
@@ -920,6 +939,7 @@ const inlineHotInternalCalls = (programFacts, ast) => {
920
939
  if (cfg && cfg.sourceInline === false) return false
921
940
 
922
941
  const fixedByFunc = new Map(ctx.func.list.map(func => [func, fixedTypedArraysInBody(func.body)]))
942
+ const typedByFunc = new Map(ctx.func.list.map(func => [func, analyzeBody(func.body).typedElems]))
923
943
  const sitesByCallee = new Map()
924
944
  for (const cs of programFacts.callSites) {
925
945
  const list = sitesByCallee.get(cs.callee)
@@ -942,8 +962,22 @@ const inlineHotInternalCalls = (programFacts, ast) => {
942
962
  return typeof arg === 'string' && fixedByFunc.get(site.callerFunc)?.has(arg)
943
963
  }))
944
964
  }
945
- const hasFixedTypedArrayHotSites = (func, sites) =>
946
- hasFixedTypedArraySites(func, sites) && sites.some(site => site.callerFunc?.body && containsNode(site.callerFunc.body, site.node))
965
+ const hasFullyFixedTypedArraySites = (func, sites) => {
966
+ const params = func.sig?.params || []
967
+ if (!sites?.length) return false
968
+ let sawTypedArg = false
969
+ for (const site of sites) {
970
+ const typed = typedByFunc.get(site.callerFunc)
971
+ const fixed = fixedByFunc.get(site.callerFunc)
972
+ for (let i = 0; i < params.length; i++) {
973
+ const arg = site.argList[i]
974
+ if (typeof arg !== 'string' || !typed?.has(arg)) continue
975
+ sawTypedArg = true
976
+ if (!fixed?.has(arg)) return false
977
+ }
978
+ }
979
+ return sawTypedArg
980
+ }
947
981
 
948
982
  const candidates = new Map()
949
983
  for (const func of ctx.func.list) {
@@ -951,6 +985,7 @@ const inlineHotInternalCalls = (programFacts, ast) => {
951
985
  if (func.defaults && Object.keys(func.defaults).length) continue
952
986
  const sites = sitesByCallee.get(func.name)
953
987
  const fixedTypedArraySite = hasFixedTypedArraySites(func, sites)
988
+ const fullyFixedTypedArraySite = hasFullyFixedTypedArraySites(func, sites)
954
989
  if (!sites || sites.length < 1 || (!fixedTypedArraySite && sites.length > 2) || sites.length > 8) continue
955
990
  const stmts = blockStmts(func.body)
956
991
  // Expression-bodied arrow funcs (`(c) => expr`) have no block — body IS the
@@ -980,7 +1015,7 @@ const inlineHotInternalCalls = (programFacts, ast) => {
980
1015
  // loop carries most of the cost. Inlining them into a host that V8 can't
981
1016
  // tier up (e.g. a once-called wrapper) freezes the kernel in baseline.
982
1017
  // Keep them as standalone functions so V8 wasm tier-up can warm them.
983
- if (loopDepth(func.body, 0) >= 2 && !fixedTypedArraySite) continue
1018
+ if (loopDepth(func.body, 0) >= 2 && !fullyFixedTypedArraySite) continue
984
1019
  // Factory functions that allocate pointers (`new TypedArray`, `new Array`,
985
1020
  // object/array literals returned) break downstream pointer-ABI specialization
986
1021
  // when inlined: narrow.js can't trace the post-inline alias chain back to a
package/src/prepare.js CHANGED
@@ -1475,6 +1475,7 @@ const handlers = {
1475
1475
  'new'(ctor, ...args) {
1476
1476
  let name = ctor, ctorArgs = args
1477
1477
  if (Array.isArray(ctor) && ctor[0] === '()') { name = ctor[1]; ctorArgs = ctor.slice(2) }
1478
+ if (name === 'Date' && ctorArgs.length === 1 && ctorArgs[0] == null) ctorArgs = []
1478
1479
  // Flatten comma-grouped args: [',', a, b, c] → [a, b, c]
1479
1480
  if (ctorArgs.length === 1 && Array.isArray(ctorArgs[0]) && ctorArgs[0][0] === ',')
1480
1481
  ctorArgs = ctorArgs[0].slice(1)