Skip to content

fix(fs): compiled AbortSignal (#985) + fd-op/chown error codes (#986) match interpreter#1011

Merged
nickna merged 2 commits into
mainfrom
wrk/fs-compiled-divergences-985-986
Jun 29, 2026
Merged

fix(fs): compiled AbortSignal (#985) + fd-op/chown error codes (#986) match interpreter#1011
nickna merged 2 commits into
mainfrom
wrk/fs-compiled-divergences-985-986

Conversation

@nickna

@nickna nickna commented Jun 29, 2026

Copy link
Copy Markdown
Owner

Fixes the two remaining interp↔compiled divergences from the fs epic (#968): both are compiled-only bugs where the compiled path diverged from the (correct) interpreter. Each is its own commit.

#986 — compiled fd-op + chown error codes

A stale file descriptor reported EINVAL (compiled) instead of EBADF (interp/Node), and chown/lchown on Windows reported EINVAL instead of ENOSYS.

Root cause (one bug, two symptoms): the fd table's Get/Close already threw $NodeError("EBADF", …), and FsChownSync/FsLchownSync already threw $NodeError("ENOSYS", …) on Windows — but both are thrown inside EmitWithFsErrorHandling's try block, whose catch-all routes every exception through ThrowNodeError. That helper maps by .NET exception type and fell through to the EINVAL default for any $NodeError, discarding the deliberate code (and doubling the message).

Fix: ThrowNodeError now detects an incoming $NodeError and preserves its own code/syscall/path and pre-formatted message verbatim, mirroring the interpreter's WrapFsOperation (which rethrows a NodeError as-is). One change fixes both halves; also cleans up the doubled message on the readlink EINVAL path.

#985 — compiled AbortSignal listener API + onabort on an any-typed receiver

signal.addEventListener('abort', cb) threw TypeError: undefined is not a function, and signal.onabort = cb never fired — but only when the signal flowed through an any parameter (the common case: stdlib facades, user helpers). At a statically-typed call site it worked.

Root cause: compiled $AbortSignal is a plain Dictionary whose methods are static $Runtime helpers, dispatched by the typed AbortSignalEmitter strategy — which only fires when the receiver's static type is AbortSignal. Through an any param, dispatch fell to generic dynamic member access: addEventListener resolved to undefined, and onabort = cb wrote a dead "onabort" dict key that FireAbortEvent (which reads the internal "_onabort" slot) never saw.

Fix (mirrors the existing dynamic GetProperty signal-property fallback, #224):

  • GetProperty: the signal branch now also returns $TSFunction wrappers for addEventListener/removeEventListener/throwIfAborted (target=null, _expectsThis=true via a __this first param), so InvokeMethodValue injects the receiver — the methods become callable from any context.
  • SetProperty: route onabort on a signal dict to AbortSignalSetOnAbort (the internal slot) instead of storing a dead key.
  • New AbortSignal{AddEventListener,RemoveEventListener,ThrowIfAborted}This __this-first wrappers adapt to the existing helpers.

All gated on UsesAbortController and the _reasonSet slot; standalone preserved (pure emitted IL, no SharpTS.dll dependency).

As a follow-up benefit, fs.promises.watch parked-abort now terminates promptly in compiled mode (#975): the facade's addEventListener('abort', finish) finally fires. Updated the stale facade comment accordingly.

Tests

Verification

  • dotnet test (full suite): 14496 / 0
  • TypeScript conformance: 31 / 0, unchanged
  • Test262 (both modes): 0 regressions (a flaky "new passes" drift in the compiled baseline varies run-to-run and is unrelated — the 11,384-entry baseline has zero abort/fs entries and my code is gated on UsesAbortController/fs paths ECMA never exercises; not baked into the baseline)
  • Standalone compiled DLLs run with no SharpTS.dll present

Closes #985
Closes #986

nickna added 2 commits June 29, 2026 01:15
The compiled fd table already threw $NodeError("EBADF") for a stale fd, and
FsChownSync/FsLchownSync threw $NodeError("ENOSYS") on Windows — but both are
thrown inside EmitWithFsErrorHandling's try block, whose catch-all routes every
exception through ThrowNodeError. That helper maps by .NET exception *type* and
fell through to the EINVAL default for any $NodeError, clobbering the deliberate
code (and producing a doubled message).

Fix: ThrowNodeError now detects an incoming $NodeError and preserves its own
code/syscall/path and pre-formatted message verbatim, mirroring the interpreter's
WrapFsOperation (which rethrows a NodeError as-is). One change fixes both the
EBADF and ENOSYS divergences; also cleans up the readlink EINVAL message.

Tests: tightened Fs_CallbackFd_BadFd_And_Chown_Invoke (badfd now asserts EBADF);
added Fs_SyncFdOps_BadFd_ReturnEBADF (fstat/ftruncate/read/write/close) and
Fs_SyncChownLchown_Windows_ReturnENOSYS. Byte-identical interp==compiled.
…d receiver (#985)

Compiled AbortSignal is a plain dictionary whose methods are static $Runtime
helpers; the typed AbortSignalEmitter strategy only fires when the receiver's
static type is AbortSignal. When a signal flows through an `any` parameter (the
common case — stdlib facades, user helpers), dispatch fell to generic dynamic
member access: addEventListener resolved to undefined ('undefined is not a
function'), and `signal.onabort = cb` wrote a plain 'onabort' dict key that
FireAbortEvent (reads '_onabort') never saw, so the handler never fired.

Fix mirrors the existing GetProperty signal-property fallback (#224):
- GetProperty: the signal branch now also returns $TSFunction wrappers for
  addEventListener/removeEventListener/throwIfAborted (target=null,
  _expectsThis=true via a '__this' first param), so InvokeMethodValue injects
  the receiver — making the methods callable from any context.
- SetProperty: route `onabort` on a signal dict to AbortSignalSetOnAbort (the
  internal '_onabort' slot) instead of storing a dead 'onabort' key.
- New AbortSignal{AddEventListener,RemoveEventListener,ThrowIfAborted}This
  '__this'-first wrappers adapt to the existing helpers.
All gated on UsesAbortController and the '_reasonSet' slot; standalone preserved
(pure emitted IL, no SharpTS.dll dependency).

This also makes fs.promises.watch parked-abort terminate promptly in compiled
mode (#975 follow-up): the facade's addEventListener('abort', finish) now fires.

Tests: dynamic-receiver onabort/addEventListener/removeEventListener/
throwIfAborted/aborted+reason in AbortControllerTests; deterministic parked-abort
case added to Fs_PromisesWatch_AsyncIteratorTerminatesOnAbort. Byte-identical
interp==compiled. Full suite 14496/0; TS conformance 31/0.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant