Skip to content

Shape TypedArray constructors; modernize element codecs with BinaryPrimitives - #2582

Merged
lahma merged 3 commits into
sebastienros:mainfrom
lahma:shapes-followup2
Jul 6, 2026
Merged

lahma merged 3 commits into
sebastienros:mainfrom
lahma:shapes-followup2

Conversation

@lahma

@lahma lahma commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #2580 / #2581. Finishes the TypedArray corner of the built-in-shape rollout and, along the way, modernizes the TypedArray element codecs with current .NET idioms.

  • Shape the TypedArray constructor family. The abstract TypedArrayConstructor and Uint8ArrayConstructor opt into [JsObject(UseShape = true)]. The 11 concrete per-type constructors (Int8Array, Uint16Array, …) declare no [JsObject] of their own, so they inherit the base shape and fill their per-type BYTES_PER_ELEMENT instance slot from it; Uint8ArrayConstructor re-declares BYTES_PER_ELEMENT locally (for its base64 statics) and so re-implements IBuiltinShaped with its own self-contained shape — the same derived-from-shaped interface re-implementation used by the iterator prototypes.

  • Modernize JsArrayBuffer element read/write with BinaryPrimitives. RawBytesToNumeric / SetValueInBuffer hand-rolled the integer codecs — reads assembled values with per-byte bit-shifting, and writes allocated a scratch byte[] per element via BitConverter.GetBytes before an Array.Copy. Every integer and bigint element type now goes through BinaryPrimitives.Read/Write{Int,UInt}{16,32,64}{Little,Big}Endian straight on the buffer span: no per-element allocation, no manual bit assembly, and endianness-correct regardless of host byte order (the old GetBytes+conditional-reverse write was only correct on a little-endian host). The floating-point path is deliberately unchanged (same BitConverter round-trip, NaN canonicalization, and big-endian scratch reversal).

  • Bulk-copy decoded base64/hex bytes into Uint8Array. setFromBase64 / setFromHex wrote the decoder output into the backing buffer one byte at a time through SetValueInBuffer; since the target is Uint8 and the decoder caps output at the array's byte length, this is now a single Span.CopyTo.

Benchmarks

Default BenchmarkDotNet job, each change A/B'd against its parent commit.

Built-in shape init (BuiltinShapeBenchmark, EngineOnly control byte-identical at 13.70 KB):

Commit Δ
TypedArray constructor family EngineInitConstructors 54.71 → 53.66 KB (−1.05 KB)

Element read/write throughput (new TypedArrayElementBenchmark, A/B/A to filter a thermal outlier on the neutral float row). Allocation is the exact, mechanism-backed signal:

Method Time Allocated
IntWriteRead 1,225 → 1,175 µs (−4%) 447.5 → 287.5 KB (−35.8%)
DataViewMixed 758 → 731 µs (−3.5%) 265.1 → 217.1 KB (−18.1%)
FloatWriteRead 508 → 491 µs (noise) 203.8 → 203.8 KB (unchanged)

The integer/bigint write path no longer allocates a per-element scratch byte[]; the float path is byte-identical in allocation and within noise on time, as expected.

Conformance

  • Full Test262: 99260 passed, 0 failed (133 skipped).
  • Jint.Tests 3210/0 (net10) · 3148/0 (net472); Jint.Tests.PublicInterface 82/0 (net10 + net472).
  • REPL smoke tests: 35 cases across every element type, LE/BE round-trips, byte-order inspection, NaN canonicalization, −0 preservation, Uint8ClampedArray rounding and integer wrapping; plus setFromHex/setFromBase64 with subarray offsets and partial/empty inputs — all matching prior behavior byte-for-byte.

🤖 Generated with Claude Code

https://claude.ai/code/session_018Fq5mF1QEwh2qDMD8vVwt3

lahma and others added 3 commits July 5, 2026 23:04
Shape the abstract TypedArrayConstructor and Uint8ArrayConstructor. The 11 concrete per-type
constructors (Int8Array, Uint16Array, ... in TypedArrayConstructor.Types.cs) declare no
[JsObject] of their own, so they inherit TypedArrayConstructor's shape and fill their per-type
BYTES_PER_ELEMENT instance slot from the base ctor. Uint8ArrayConstructor already re-declares
BYTES_PER_ELEMENT locally (for its base64 statics), so it re-implements IBuiltinShaped with its
own self-contained shape — the same derived-from-shaped interface re-implementation used by the
iterator prototypes. No inherited-[JsObject]-member support is needed.

Benchmarks (BuiltinShapeBenchmark, default job, vs prior commit; EngineOnly control byte-
identical at 13.70 KB): EngineInitConstructors (extended to touch several per-type constructors)
54.71 -> 53.66 KB (-1.05 KB).

Conformance: Test262 TypedArray + ArrayBuffer + DataView 6120/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Fq5mF1QEwh2qDMD8vVwt3
RawBytesToNumeric (reads) and SetValueInBuffer (writes) hand-rolled the integer element
codecs: reads assembled values with manual bit-shifting per byte, and writes went through
NumericToRawBytes which allocated a scratch byte[] per element via BitConverter.GetBytes
(on every TFM except netstandard2.1) before an Array.Copy into the buffer.

Route every integer and bigint element type through System.Buffers.Binary.BinaryPrimitives
Read/Write{Int,UInt}{16,32,64}{Little,Big}Endian directly on the buffer span. This removes
the per-element byte[] allocation on the write path, drops the manual bit assembly on the
read path, and is endianness-correct regardless of the host's byte order (the old GetBytes +
conditional-reverse write path was only correct on a little-endian host). (short)/(ushort)
and (int)/(uint) share a bit pattern, so the signed overload serves the unsigned type too.

The floating-point path is deliberately unchanged: BitConverter round-trips Float16/32/64 with
the same NaN-canonicalization and big-endian scratch-buffer reversal as before (the write side
factored into FloatToRawBytes, Float16 reads into ReadFloat16 to keep the SUPPORTS_HALF
preprocessor branch out of a switch body). The per-instance _workBuffer now backs only the
big-endian float read reversal.

Benchmarks (new Jint.Benchmark/TypedArrayElementBenchmark.cs, default job; A/B/A to filter a
thermal outlier on the neutral float row). Allocation is the exact, mechanism-backed signal:

| Method         | Time            | Allocated              |
|----------------|-----------------|------------------------|
| IntWriteRead   | 1,225 -> 1,175 us (-4%) | 447.5 -> 287.5 KB (-35.8%) |
| DataViewMixed  |   758 ->   731 us (-3.5%) | 265.1 -> 217.1 KB (-18.1%) |
| FloatWriteRead |   508 ->   491 us (noise) | 203.8 -> 203.8 KB (unchanged) |

Conformance: Test262 TypedArray + ArrayBuffer + DataView 6120/0 (2 skipped); a 35-case REPL
smoke test covering every element type, LE/BE round-trips, byte-order inspection, NaN
canonicalization, -0 preservation, Uint8Clamped rounding, and integer wrapping all pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Fq5mF1QEwh2qDMD8vVwt3
Uint8Array.prototype.setFromBase64 / setFromHex wrote the decoder's output into the target's
backing buffer one byte at a time through SetValueInBuffer, so each byte paid a TypedArrayValue
wrap plus the element-set switch and integer conversion — for a plain byte-for-byte Uint8 copy.

FromBase64/FromHex already cap their output at the target's byte length, so the decoded bytes
always fit within the typed array's view. Copy them straight into the backing buffer span with
Span.CopyTo — a single memcpy instead of N element-set calls. Behavior is unchanged (the write
count still comes from result.Bytes.Length; a target too small for a full base64 chunk still
writes nothing, matching the prior loop).

Conformance: Test262 TypedArray + ArrayBuffer + DataView + Uint8Array 6266/0 (2 skipped); REPL
smoke tests for setFromHex/setFromBase64 incl. subarray offsets and partial/empty inputs match
the previous loop byte-for-byte.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Fq5mF1QEwh2qDMD8vVwt3
@lahma
lahma enabled auto-merge (squash) July 6, 2026 05:48
@lahma
lahma merged commit 96b4c17 into sebastienros:main Jul 6, 2026
7 of 8 checks passed
@lahma
lahma deleted the shapes-followup2 branch July 6, 2026 06:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant