feat(modules): support TC39 Import Bytes with immutable backing#794
feat(modules): support TC39 Import Bytes with immutable backing#794frostney wants to merge 1 commit into
Conversation
Implement the TC39 Import Bytes proposal (Stage 2.7): `import x from
"./f" with { type: "bytes" }` (static, dynamic, and import.defer) yields a
default-only Uint8Array backed by an immutable ArrayBuffer. Bytes are
preserved exactly (no UTF-8 decoding or newline normalization) and the
attribute selects the loader regardless of file extension/MIME. import.source
of a bytes module is rejected (non-goal); modules cache by specifier + type.
To make the "immutable" guarantee real, complete the Immutable ArrayBuffers
observable surface rather than staging it: add `get ArrayBuffer.prototype.
immutable`, make immutable buffers non-detachable (transfer/transferToFixedLength/
transferToImmutable throw), and reject writes through a backing view — indexed
[[Set]] (silent no-op after coercion), integer-index defineProperty, and
DataView setters (TypedArray methods and Atomics already threw).
Closes #775
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📝 WalkthroughWalkthroughAdds bytes import support through raw-byte module loading, immutable ArrayBuffer-backed default exports, import-attribute routing updates, and related tests and documentation. Also extends ArrayBuffer, DataView, and TypedArray behavior for immutable buffers. ChangesImport Bytes and Immutable ArrayBuffers
Sequence Diagram(s)sequenceDiagram
participant TGocciaImportCallExpression
participant TGocciaVM
participant LoadBytesModule
participant TGocciaModuleContentProvider
participant TGocciaArrayBufferValue
TGocciaImportCallExpression->>TGocciaVM: validate import attribute type "bytes"
TGocciaVM->>LoadBytesModule: dispatch bytes import
LoadBytesModule->>TGocciaModuleContentProvider: LoadContentBytes(resolved path)
TGocciaModuleContentProvider-->>LoadBytesModule: raw TBytes
LoadBytesModule->>TGocciaArrayBufferValue: CreateImmutableFromBytes(raw bytes)
LoadBytesModule->>LoadBytesModule: cache synthetic default-only module
LoadBytesModule-->>TGocciaImportCallExpression: return bytes module
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches⚔️ Resolve merge conflicts
Comment |
Suite TimingTest Runner (interpreted: 10,842 passed; bytecode: 10,842 passed)
MemoryGC rows aggregate the main thread plus all worker thread-local GCs. Test runner worker shutdown frees thread-local heaps in bulk; that shutdown reclamation is not counted as GC collections or collected objects.
Benchmarks (interpreted: 430; bytecode: 430)
MemoryGC rows aggregate the main thread plus all worker thread-local GCs. Benchmark runner performs explicit between-file collections, so collection and collected-object counts can be much higher than the test runner.
Measured on ubuntu-latest x64. |
Benchmark Results430 benchmarks Interpreted: 🟢 19 improved · 🔴 92 regressed · 319 unchanged · avg -1.5% arraybuffer.js — Interp: 🔴 4, 10 unch. · avg -2.6% · Bytecode: 🔴 1, 13 unch. · avg +2.1%
arrays.js — Interp: 🔴 7, 12 unch. · avg -2.2% · Bytecode: 🟢 3, 16 unch. · avg +2.1%
async-await.js — Interp: 🟢 1, 5 unch. · avg -5.5% · Bytecode: 6 unch. · avg +0.3%
async-generators.js — Interp: 2 unch. · avg -11.6% · Bytecode: 2 unch. · avg +0.7%
atomics.js — Interp: 🔴 3, 3 unch. · avg -4.6% · Bytecode: 6 unch. · avg -2.0%
base64.js — Interp: 🔴 3, 7 unch. · avg -2.3% · Bytecode: 🔴 2, 8 unch. · avg -0.9%
classes.js — Interp: 🟢 2, 🔴 2, 27 unch. · avg +0.3% · Bytecode: 🟢 4, 27 unch. · avg +1.5%
closures.js — Interp: 🔴 5, 6 unch. · avg -2.8% · Bytecode: 🟢 2, 🔴 1, 8 unch. · avg +0.6%
collections.js — Interp: 🔴 5, 7 unch. · avg -5.6% · Bytecode: 🟢 2, 🔴 1, 9 unch. · avg +0.7%
csv.js — Interp: 🔴 2, 11 unch. · avg -3.1% · Bytecode: 🟢 1, 12 unch. · avg -2.1%
destructuring.js — Interp: 🟢 1, 🔴 8, 13 unch. · avg -2.0% · Bytecode: 🟢 1, 🔴 2, 19 unch. · avg +0.5%
fibonacci.js — Interp: 🔴 1, 7 unch. · avg -3.1% · Bytecode: 8 unch. · avg -1.2%
float16array.js — Interp: 🟢 6, 🔴 7, 19 unch. · avg +0.0% · Bytecode: 🟢 3, 🔴 1, 28 unch. · avg +1.4%
for-of.js — Interp: 🔴 1, 6 unch. · avg +0.3% · Bytecode: 🟢 2, 5 unch. · avg +6.1%
generators.js — Interp: 🔴 1, 3 unch. · avg -4.3% · Bytecode: 4 unch. · avg -1.7%
intl.js — Interp: 🔴 2, 4 unch. · avg -1.4% · Bytecode: 6 unch. · avg -2.9%
iterators.js — Interp: 🔴 12, 30 unch. · avg -5.0% · Bytecode: 🟢 14, 28 unch. · avg +5.2%
json.js — Interp: 20 unch. · avg -2.2% · Bytecode: 🔴 3, 17 unch. · avg -1.0%
jsx.jsx — Interp: 🔴 5, 16 unch. · avg -1.2% · Bytecode: 🔴 3, 18 unch. · avg -2.5%
modules.js — Interp: 9 unch. · avg +0.0% · Bytecode: 9 unch. · avg -1.9%
numbers.js — Interp: 🟢 1, 🔴 3, 7 unch. · avg -3.9% · Bytecode: 🟢 1, 10 unch. · avg -0.2%
objects.js — Interp: 🔴 1, 6 unch. · avg -0.8% · Bytecode: 🟢 1, 6 unch. · avg +0.5%
promises.js — Interp: 🔴 1, 11 unch. · avg -4.3% · Bytecode: 🔴 2, 10 unch. · avg -4.5%
property-access.js — Interp: 🔴 2, 3 unch. · avg -7.1% · Bytecode: 🟢 1, 4 unch. · avg +4.2%
regexp.js — Interp: 🟢 1, 10 unch. · avg +1.5% · Bytecode: 11 unch. · avg -3.4%
strings.js — Interp: 🔴 1, 18 unch. · avg +0.1% · Bytecode: 🟢 1, 18 unch. · avg +1.5%
temporal.js — Interp: 🟢 1, 5 unch. · avg +2.3% · Bytecode: 🔴 1, 5 unch. · avg +0.1%
tsv.js — Interp: 9 unch. · avg -0.2% · Bytecode: 🟢 1, 8 unch. · avg -0.3%
typed-arrays.js — Interp: 🟢 4, 🔴 5, 13 unch. · avg +4.7% · Bytecode: 🟢 2, 🔴 5, 15 unch. · avg -6.4%
uint8array-encoding.js — Interp: 🟢 1, 🔴 6, 11 unch. · avg +3.0% · Bytecode: 🟢 7, 🔴 2, 9 unch. · avg +14.4%
weak-collections.js — Interp: 🟢 1, 🔴 5, 9 unch. · avg -1.4% · Bytecode: 🟢 9, 6 unch. · avg +34.3%
Deterministic profile diffDeterministic profile diff: no significant changes. Measured on ubuntu-latest x64. Benchmark ranges compare cached main-branch min/max ops/sec with the PR run; overlapping ranges are treated as unchanged noise. Percentage deltas are secondary context. |
test262 Conformance
Areas closest to 100%
Per-test deltas (+49 / -2)Newly failing (2):
Newly passing (49):
Steady-state failures are non-blocking; regressions vs the cached main baseline (lower total pass count, or any PASS → non-PASS transition) fail the conformance gate. Measured on ubuntu-latest x64, bytecode mode. Areas grouped by the first two test262 path components; minimum 25 attempted tests, areas already at 100% excluded. Δ vs main compares against the most recent cached |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@source/units/Goccia.Values.DataViewValue.pas`:
- Around line 498-503: The immutable-buffer write guard in SetViewValue is
running before the index bounds validation, which can surface the wrong error
type for invalid offsets. Move the TGocciaArrayBufferValue.Immutable check to
after the existing RangeError/bounds validation in SetViewValue so out-of-range
offsets still throw the correct RangeError first, while still rejecting writes
to immutable buffers afterward.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 379e7230-42c0-4588-8185-9e77d44a9b4f
⛔ Files ignored due to path filters (1)
tests/language/modules/helpers/asset.binis excluded by!**/*.bin
📒 Files selected for processing (21)
docs/adr/0070-import-bytes-with-immutable-backing.mddocs/adr/README.mddocs/built-ins-binary-data.mddocs/language-tables.mddocs/language.mdsource/shared/FileUtils.passource/units/Goccia.AST.Expressions.passource/units/Goccia.Constants.PropertyNames.passource/units/Goccia.Modules.ContentProvider.passource/units/Goccia.Modules.Loader.passource/units/Goccia.Sandbox.Modules.passource/units/Goccia.TextFiles.passource/units/Goccia.VM.passource/units/Goccia.Values.ArrayBufferValue.passource/units/Goccia.Values.DataViewValue.passource/units/Goccia.Values.TypedArrayValue.pastests/built-ins/ArrayBuffer/prototype/immutable.jstests/built-ins/ArrayBuffer/prototype/transferToImmutable.jstests/built-ins/DataView/immutable-buffer.jstests/built-ins/TypedArray/immutable-buffer.jstests/language/modules/bytes-import.js
| // Immutable ArrayBuffers proposal: SetViewValue throws when the viewed buffer | ||
| // is immutable (the observable numeric coercion above still runs first). | ||
| if (FBufferValue is TGocciaArrayBufferValue) and | ||
| TGocciaArrayBufferValue(FBufferValue).Immutable then | ||
| ThrowTypeError('DataView cannot write to an immutable ArrayBuffer'); | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Move immutable-write rejection after index bounds validation.
This guard runs before the RangeError check, so invalid offsets on immutable buffers can throw the wrong error type.
🔧 Proposed fix
- if (FBufferValue is TGocciaArrayBufferValue) and
- TGocciaArrayBufferValue(FBufferValue).Immutable then
- ThrowTypeError('DataView cannot write to an immutable ArrayBuffer');
-
ViewSize := GetViewByteLength;
ElementSize := BinaryBytesPerElement(AKind);
if Int64(Index) + Int64(ElementSize) > Int64(ViewSize) then
ThrowRangeError(SErrorInvalidDataViewOffset, SSuggestTypedArrayLength);
+
+ if (FBufferValue is TGocciaArrayBufferValue) and
+ TGocciaArrayBufferValue(FBufferValue).Immutable then
+ ThrowTypeError('DataView cannot write to an immutable ArrayBuffer');📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Immutable ArrayBuffers proposal: SetViewValue throws when the viewed buffer | |
| // is immutable (the observable numeric coercion above still runs first). | |
| if (FBufferValue is TGocciaArrayBufferValue) and | |
| TGocciaArrayBufferValue(FBufferValue).Immutable then | |
| ThrowTypeError('DataView cannot write to an immutable ArrayBuffer'); | |
| ViewSize := GetViewByteLength; | |
| ElementSize := BinaryBytesPerElement(AKind); | |
| if Int64(Index) + Int64(ElementSize) > Int64(ViewSize) then | |
| ThrowRangeError(SErrorInvalidDataViewOffset, SSuggestTypedArrayLength); | |
| // Immutable ArrayBuffers proposal: SetViewValue throws when the viewed buffer | |
| // is immutable (the observable numeric coercion above still runs first). | |
| if (FBufferValue is TGocciaArrayBufferValue) and | |
| TGocciaArrayBufferValue(FBufferValue).Immutable then | |
| ThrowTypeError('DataView cannot write to an immutable ArrayBuffer'); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@source/units/Goccia.Values.DataViewValue.pas` around lines 498 - 503, The
immutable-buffer write guard in SetViewValue is running before the index bounds
validation, which can surface the wrong error type for invalid offsets. Move the
TGocciaArrayBufferValue.Immutable check to after the existing RangeError/bounds
validation in SetViewValue so out-of-range offsets still throw the correct
RangeError first, while still rejecting writes to immutable buffers afterward.
Summary
import x from "./f" with { type: "bytes" }— plus the dynamicimport(...)andimport.deferforms — loads the resolved file as raw bytes and exposes a synthetic module whose single default export is aUint8Arraybacked by an immutableArrayBuffer.type: "bytes"(static and dynamic share one module).LoadContentBytes); the filesystem and sandbox providers read exact bytes (stream / VFSReadAllBytes), never round-tripping through the UTF-8/source-line text path.type: "bytes"previously threwSyntaxError: Unsupported import attribute type, so it is purely additive — no working program changes behavior. (This differs from the Stage 2.7 JSModuleSourceprovider, which is gated only because it changes existing source-phase resolution — see ADR 0064.)ArrayBuffer" guarantee real (the chosen "full compliance now" path), the Immutable ArrayBuffers observable surface was completed rather than staged: addedget ArrayBuffer.prototype.immutable, made immutable buffers non-detachable (transfer/transferToFixedLength/transferToImmutablethrow on them), and rejected writes through a backing view — indexed[[Set]](silent no-op after the observable coercion), integer-indexdefineProperty, andDataViewsetters (TypedArray mutating methods andAtomicsalready threw). Recorded in ADR 0070.Non-goals honored: no new
import bytes ...phase syntax; bytes are not routed throughimport.source(rejected with a clear message); no browser fetch behavior; JSON/text module semantics unchanged.Follow-up (out of scope, pre-existing): the bytecode VM does not reject named imports that name a missing export — it binds
undefined, whereas the interpreter throwshas no export named. This affects all module kinds (JS/JSON/text/bytes), not just bytes, and is tracked separately. The bytes-specific guarantee (module declares onlydefault) is verified mode-agnostically in the tests.Closes #775
Testing
Value-type changes (
ArrayBuffer/TypedArray/DataView) are covered by the new end-to-end JavaScript tests rather than native Pascal tests, per the project's JS-suite-first testing convention.Commands run (both execution modes):
Result: 10842/10842 tests pass in both interpreter and bytecode modes;
format.pas --checkclean (367 files). New tests:tests/language/modules/bytes-import.js,tests/built-ins/ArrayBuffer/prototype/immutable.js,tests/built-ins/ArrayBuffer/prototype/transferToImmutable.js,tests/built-ins/TypedArray/immutable-buffer.js,tests/built-ins/DataView/immutable-buffer.js.🤖 Generated with Claude Code