Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/adr/0112-native-async-local-storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ Deliberately out of scope:
travel into. When a host-scheduled callback surface is added, its scheduling
point becomes a third seam; nothing about the snapshot representation has to
change for that.

**Superseded by [ADR 0113](0113-deterministic-virtual-timer-queue.md).** The
virtual timer queue is that surface, and the prediction held: timer
registration captures the current snapshot and the queue installs it around
the callback through the same `EnterAsyncContext` / `LeaveAsyncContext` pair,
with nothing in `Goccia.AsyncContext` changed.
- **The `async_hooks` observer API.** `createHook`, `executionAsyncId`,
`triggerAsyncId`, and the `init`/`before`/`after`/`destroy` callbacks are not
provided. They describe an async-resource lifecycle GocciaScript does not
Expand Down
301 changes: 301 additions & 0 deletions docs/adr/0113-deterministic-virtual-timer-queue.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,4 @@ Durable architecture and implementation decisions for GocciaScript. New ADRs use
- [0110 — The growth gate collects before refusing, and store paths root their temporaries](0110-growth-gate-collects-before-refusing.md)
- [0111 — Opt-in node_modules resolution](0111-opt-in-node-modules-resolution.md)
- [0112 — Native AsyncLocalStorage over continuation snapshots](0112-native-async-local-storage.md)
- [0113 — Deterministic virtual timer queue](0113-deterministic-virtual-timer-queue.md)
21 changes: 13 additions & 8 deletions docs/built-ins-async-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,19 @@ An async generator body observes the context of whichever call resumed it, in
both executors and as in Node — a `for await` inside a `run` sees that run's
store, and a generator resumed outside one sees no store.

It does not travel into host-scheduled callbacks, because there are none:
GocciaScript has no timer task queue and no general event loop, so there is no
`setTimeout` continuation for a context to reach. The `async_hooks` observer API
(`createHook`, `executionAsyncId`, and the `init` / `before` / `after` /
`destroy` callbacks) is not provided either — it describes an async-resource
lifecycle this engine does not have. [ADR
0112](adr/0112-native-async-local-storage.md) records both cuts and the
snapshot mechanism behind the propagation.
It travels into timer callbacks too. A `setTimeout` scheduled inside a `run`
captures the snapshot at registration and runs under it, even though the `run`
returned long before the timer fired — see [Fake
timers](testing-api.md#fake-timers) for the queue itself. That is the third
propagation seam, and it needed nothing new from the snapshot mechanism.

The `async_hooks` observer API (`createHook`, `executionAsyncId`, and the `init`
/ `before` / `after` / `destroy` callbacks) is not provided — it describes an
async-resource lifecycle this engine does not have. [ADR
0112](adr/0112-native-async-local-storage.md) records that cut and the snapshot
mechanism behind the propagation; [ADR
0113](adr/0113-deterministic-virtual-timer-queue.md) records the timer seam it
predicted.

## Availability

Expand Down
2 changes: 2 additions & 0 deletions docs/built-ins.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Core language built-ins (Math, Object, Array, Number, JSON, Symbol, Set, Map, We

Runtime globals (Console, Performance, TextEncoder/TextDecoder, URL, fetch, Headers, Response, AbortController/AbortSignal, EventTarget/Event) are registered by the loader runtime profile and runtime extension classes under `source/units/Goccia.RuntimeExtensions.*.pas`. The same runtime profile also installs named-export-only Goccia modules for non-standard data-format APIs and SemVer: `goccia:csv`, `goccia:json5`, `goccia:jsonl`, `goccia:toml`, `goccia:tsv`, `goccia:yaml`, and `goccia:semver`. It additionally registers the `goccia:test` module namespace without injecting any testing global, so the testing API is importable from every host that applies the profile. CLI hosts such as `GocciaScriptLoader` and `GocciaREPL` call `ApplyLoaderRuntimeProfile`; `GocciaTestRunner` applies the loader runtime profile with the module-only testing install suppressed and installs `TGocciaTestingLibraryRuntimeExtension` with global injection enabled instead, which is why it is the only binary with global `describe`/`test`/`expect`; `GocciaBenchmarkRunner` applies the loader runtime profile plus `TGocciaBenchmarkRuntimeExtension`. See [Test Framework API](testing-api.md#availability-per-binary) for the per-binary table. `GocciaScriptLoaderBare` does not attach a runtime and exposes only a CLI-local `print(...args)` helper by default; the test262 conformance runner may opt into private test262 host capabilities with `--test262-host`.

Timers are runner-only. `GocciaTestRunner` installs `TGocciaTimersRuntimeExtension`, which registers the `setTimeout`, `clearTimeout`, `setInterval` and `clearInterval` globals plus the `goccia:timers` control module over a deterministic virtual timer queue — no timer ever waits on wall time. The loader runtime profile does not install it: the timers carry no ambient authority, but a scheduling surface is one a sandboxed script does not otherwise get. See [Fake timers](testing-api.md#fake-timers) and [ADR 0113](adr/0113-deterministic-virtual-timer-queue.md).

`GocciaSandboxRunner` applies the loader runtime profile and then installs `TGocciaSandboxRuntimeExtension`. That extension registers sandbox capabilities as import-only runtime modules named `"fs"` and `"goccia"`; it does not create global `fs`, `$`, or `runScript` bindings.

FFI is not part of the loader runtime profile. CLI tools install `TGocciaFFIRuntimeExtension` when `--unsafe-ffi` is passed or `"unsafe-ffi": true` is set in config.
Expand Down
14 changes: 14 additions & 0 deletions docs/differential-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ oracle instead of inheriting a default.
| `o-asynccontext.test.js` | language | skip | gate |
| `p-callintrinsics.test.js` | language | skip | gate |
| `q-reflectconstruct.test.js` | language | skip | gate |
| `r-faketimers.test.js` | timers | gate | skip |

`o-asynccontext.test.js` covers `node:async_hooks` propagation only, and stops
there on purpose. Bun 1.3.14 does not honour the `defaultValue` or `name`
Expand Down Expand Up @@ -126,6 +127,19 @@ script, but under `bun test` 1.4.0 the same class body reports it defined, so
gating would report bun's transpile as a goccia divergence. It is covered
against node in `tests/built-ins/Reflect/construct/instance-elements.js`.

`r-faketimers.test.js` covers the `vi` fake-timer family, and Vitest is the only
possible oracle for it: Vitest's fake timers wrap `@sinonjs/fake-timers`, so what
a tick does — whether microtasks interleave, how a nested zero-delay timer is
scheduled, when `runOnlyPendingTimers` stops, what the loop guard says — is
decided by that clock rather than by ECMAScript. Bun is skipped for the reason
`e-mocks.test.js` records. Two shapes are deliberately absent. The **type of a
timer id** is a documented divergence: Vitest runs in Node, whose fake clock
returns a `Timeout` object, while GocciaScript returns a number as the web
platform does, so the suite asserts on clearing rather than on the id. **Real-mode
timers** are absent because there is nothing to compare — under Vitest they run
on a real event loop and under GocciaScript the clock jumps to them; they are
covered against the intended behaviour in `tests/built-ins/Timers` instead.

`h-modulemock.test.js` and `i-modulemock-isolation.test.js` are a pair: the
first mocks `./mods/mockable.js` with a `vi.mock` factory, the second mocks
nothing and must still see the real module. Under Vitest both files run in one
Expand Down
9 changes: 9 additions & 0 deletions docs/host-environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- Pascal embedders inject implementations of `IGocciaHostClock` and `IGocciaHostRandom` before attaching runtime extensions or executing source.
- `GocciaScriptLoader --host-environment=<module>` accepts the same providers as callable named JavaScript exports.
- Child engines share the clock and receive derived random stream identifiers, so realms remain reproducible without replaying the parent stream.
- A **clock override** layers a mocked epoch and/or monotonic time over the configured providers, which is how fake timers reach `Date`, `Temporal.Now`, and `performance` at once.
- Timeouts, profiling, benchmarks, and other infrastructure continue to use the real clocks in `TimingUtils`.

## JavaScript Provider Modules
Expand Down Expand Up @@ -70,6 +71,14 @@ Engine.Execute;

For a fixed built-in profile, call `Engine.HostEnvironment.UseDeterministicProfile` instead of implementing providers. It supplies epoch and monotonic time `0`, `UTC`, and portable seeded SplitMix64 randomness.

## Mocked Clocks

A host environment can carry a **clock override**: a layer over the configured providers rather than a replacement for them. `OverrideClock` sets a mocked epoch time, a mocked monotonic time, or both; `ClearClockOverride` removes it; `RealEpochNanoseconds` still reads the provider underneath. This is the layer the [virtual timer queue](adr/0113-deterministic-virtual-timer-queue.md) installs a fake clock on, so `Date`, `Temporal.Now`, and `performance` all report the same simulated instant without any of them being patched.

The two halves are independent. Freezing the date alone — `vi.setSystemTime()` outside `vi.useFakeTimers()` — leaves monotonic time real, so `performance.now()` keeps measuring elapsed wall time. Under fake timers both are mocked, and `performance.now()` reports elapsed **virtual** time from the moment the clock was installed, which a `setSystemTime` jump does not move.

An override is not inherited by `ConfigureAsChildOf`, so a ShadowRealm child reads the real clock until something mocks one of its own.

## Default Compatibility

Without an explicit provider, the system host environment preserves each API's established default behavior. Temporal and Date observe the system time zone; `Intl.DateTimeFormat` retains GocciaScript's historical `UTC` default. An explicitly configured host time zone overrides both surfaces.
4 changes: 3 additions & 1 deletion docs/interpreter.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,9 @@ In the ECMAScript specification, the entire script is one macrotask. Microtasks
2. All `.then()` callbacks fire in FIFO order.
3. New microtasks enqueued during draining (e.g., chained `.then()` handlers) are processed in the same drain cycle.

This follows the ECMAScript specification's microtask ordering semantics. Thenable adoption (resolving a Promise with another Promise) is deferred by one microtask tick, matching the spec's PromiseResolveThenableJob. When `Resolve(innerPromise)` is called, instead of synchronously calling `SubscribeTo`, a `prtThenableResolve` microtask is enqueued. When this microtask drains, it calls `SubscribeTo` to adopt the inner Promise's state — resulting in a 2-tick deferral (one for the thenable resolve job, one for the settlement reaction). This ensures correct ordering relative to other microtasks. The only scenario where timing would differ from a full engine is with multiple macrotask sources (`setTimeout`, I/O callbacks, event handlers), which GocciaScript does not implement. If these are added in the future, they would require an event loop that repeatedly: (1) dequeues one macrotask, (2) drains the microtask queue, (3) repeats.
This follows the ECMAScript specification's microtask ordering semantics. Thenable adoption (resolving a Promise with another Promise) is deferred by one microtask tick, matching the spec's PromiseResolveThenableJob. When `Resolve(innerPromise)` is called, instead of synchronously calling `SubscribeTo`, a `prtThenableResolve` microtask is enqueued. When this microtask drains, it calls `SubscribeTo` to adopt the inner Promise's state — resulting in a 2-tick deferral (one for the thenable resolve job, one for the settlement reaction). This ensures correct ordering relative to other microtasks.

There is one macrotask source, and it is deliberately not an event loop: `Goccia.Timers.pas` holds a [virtual timer queue](adr/0113-deterministic-virtual-timer-queue.md) behind `setTimeout` and `setInterval`, installed in the test-runner profile. Nothing there waits on wall time. A timer runs only when a test advances the virtual clock (`vi.advanceTimersByTime` and friends) or, without fake timers, when the engine would otherwise have nothing left to do — an `await` on a promise a timer will settle, or the end-of-run idle drain. Each such step runs one timer and then drains this microtask queue, which is the macrotask-then-microtask ordering an event loop provides, minus the loop and minus real elapsed time. Other macrotask sources — I/O callbacks, event handlers — remain unimplemented.

**Integration points:**

Expand Down
Loading
Loading