ReviewOS

zig-utils/zig-js

Intl.Collator: publish complete resolved locale state

#589
Closed chris opened this 9 days ago ·
9 days ago

Parents: #475, #460 Follow-up to: #580 Related: #581, #493

Problem

#580 made bound compare consume immutable native Collator state, but constructor publication still retains a hidden \0opts JavaScript object and stores the pre-ResolveLocale tag in the native record. Every resolvedOptions() call generically traverses that object, reruns co/kf/kn extension resolution, allocates a temporary locale result, and reflects it. Because the receiver path trusts forgeable NUL-led properties before consulting the native record, a fabricated ordinary object can also pass the current resolvedOptions brand check.

Repeated reflection therefore pays avoidable synchronized property/GC work, and native compare state is not yet the complete immutable resolved state claimed by its ownership boundary.

Scope

  • Freeze a dependency-free repeated resolvedOptions() row across default, search, numeric, case-first, sensitivity, punctuation, supported collation, and Unicode-extension configurations, hashing every reflected field with a checksum shared by zig-js and system JSC.
  • Preserve exact InitializeCollator getter/coercion/throw order and ResolveLocale behavior for co, kf, and kn, including extension retention/removal under equal, overriding, and unsupported option values.
  • Publish the final resolved locale spelling and every reflected option in the object-owned, GC-finalized native Collator record after all observable reads succeed.
  • Remove the hidden normalized-options object. Make compare access/invocation and resolvedOptions() require the unforgeable native record while preserving callable-constructor and bound-function identity semantics.
  • Reflect closed fields through immutable static values; locale output remains an independently owned ordinary result string.

Security and no-workaround rules

Locales, options, property getters, coercion hooks, receiver properties, and call frequency are attacker-controlled. No result cache, source/workload recognition, skipped getter, weakened brand, predictable lookup shortcut, unbounded retained state, or locale-specific repro branch is allowed.

Acceptance

  • The benchmark boundary is committed before runtime work and exact checksums match system JSC.
  • Proxy/throwing getters prove exact option observation and no later rereads; forged NUL-led properties fail at the required receiver check.
  • Resolved locale/fields cover default and co/kf/kn equal/override/removal cases.
  • Constructor OOM rollback, bounded repeated reflection, moving GC, shared-realm no-GIL reads, and suppression-free TSan pass.
  • Exact-parent attribution reports locks, backing allocations/bytes, GC cells/collections, RSS, and order-balanced timing without hiding bounded record cost.
  • Collator test262, full Intl402, and the full sharded unit suite do not regress.

Exclusions

No Promise, microtask, Worker, queue, job-lifecycle, stack, or user-owned documentation work.

9 days ago

Benchmark boundary landed first in fe18ee92.

The cross-engine gate exposed a correctness/lifetime defect rather than an equivalent performance baseline: system JSC returns checksum 865567973, while zig-js returns 49317546 after configuring the complete representative fixture. Isolated construction produces identical resolved records, but the retained en-US search Collator later reflects de-DE after unrelated formatter allocations. The scored row keeps this divergence visible and consumes all seven fields; final runtime acceptance requires exact JSC parity. Because the pre-fix and final checksums differ, no wall-time ratio will be claimed across that unequal-output boundary.

Pre-change 100-job attribution: 2,483,210 property locks; 1,447,710 backing allocations / 216,871,113 cumulative bytes; 792,215 GC cells; 25 collections; 73,331,829 current and 75,243,506 peak backing bytes; 82,968,576 retained RSS bytes. The native publication fix must eliminate the hidden record, prove locale stability under unrelated allocation/moving GC, and then report the structural counter delta.

9 days ago

Correction: 3f3bb952 restores the pre-existing de-DE search case in the compare matrix and places en-US in the new reflection row. My earlier duplicate-literal edit changed the wrong array entry; the resulting checksum mismatch was a fixture mistake, not evidence of hidden-locale mutation. The previous diagnosis is retracted.

The corrected untouched-runtime boundary now matches system JSC exactly at checksum 837300589. Its 100-job attribution remains: 2,483,210 property locks; 1,447,710 backing allocations / 216,871,113 bytes; 792,215 GC cells; 25 collections; 73,331,829 current and 75,243,506 peak backing bytes; 82,870,272 retained RSS bytes. This corrected commit is the exact parent for the runtime work.

9 days ago

Completed on main:

  • initial reflection boundary: fe18ee92
  • duplicate-literal fixture correction / true exact parent: 3f3bb952
  • native complete-state implementation: db8ef45b

The correction remains material to the audit trail: the initial checksum mismatch was caused by editing the pre-existing compare array instead of the new reflection array, not by runtime state mutation. The corrected parent, final candidate, and system JSC all produce checksum 837300589.

Collator now owns the final co/kf/kn-resolved locale and every reflected option in its GC-finalized native record. Constructor option getters run once in the required seven-field order. Real instances expose no \0intl, \0locale, or \0opts own properties; compare access/invocation and resolvedOptions() require the native sidecar, so forged NUL-led properties fail. Closed reflected strings are static, while each result locale remains independently owned.

Exact-parent 100-job attribution:

  • property locks: 2,483,210 -> 2,329,451 (-153,759 / -6.19%)
  • backing allocations: 1,447,710 -> 1,422,014 (-25,696 / -1.77%)
  • cumulative backing bytes: 216,871,113 -> 216,715,393 (-155,720)
  • current backing: 73,331,829 -> 71,211,880 (-2,119,949 / -2.89%)
  • GC cells: 792,215 -> 766,583 (-25,632 / -3.24%)
  • collections: 25 -> 25
  • peak backing: 75,243,506 -> 75,315,896 (+72,390)
  • retained RSS: 82,870,272 -> 83,001,344 (+131,072)

Two final seven-pair timing windows disagree on effect size (-9.94% and -0.68%), so no latency claim is made.

Validation:

  • dedicated getter/state/extension/brand test passes; corpus throwing-getter and incompatible-receiver coverage also passes
  • focused normal and suppression-free TSan: 5/5, 0 leaks
  • OOM publication, 32 MiB adversarial sort, native concurrent reads, and full moving/parallel GC unit coverage pass
  • Collator test262: 65 -> 65, 0 failures
  • full Intl402: 3,341 -> 3,341, 0 failures
  • full sharded units: 1,699 passed, 1 skipped, 0 failed, 0 leaked

All acceptance conditions are satisfied without a cache, result reuse, or locale-specific shortcut.

Sign in to comment on this issue.