Skip to content

feat(fs): migrate to primitive:fs + TypeScript facade (#969)#987

Merged
nickna merged 2 commits into
mainfrom
wrk/issue-969-primitive-fs-facade
Jun 28, 2026
Merged

feat(fs): migrate to primitive:fs + TypeScript facade (#969)#987
nickna merged 2 commits into
mainfrom
wrk/issue-969-primitive-fs-facade

Conversation

@nickna

@nickna nickna commented Jun 28, 2026

Copy link
Copy Markdown
Owner

Part of the fs epic #968. Bottom of an 8-PR stack — review/merge bottom-up.

Flips fs/fs/promises from C# built-ins to TS facades over a primitive:fs seam (the os/process pattern). The C# impls are retained as the primitives; the facade re-exports the sync surface and derives callback APIs from the promise primitives — structurally closing #970 (compiled had no callback fs before). Also fixes an exposed interp fs/promises rejection bug. Standalone preserved. Full xUnit green; TS conformance unchanged.

Closes #969.

nickna added 2 commits June 27, 2026 14:41
…969)

Flip the user-facing `fs` and `fs/promises` modules from C#-backed built-ins
to TypeScript facades over a `primitive:fs` / `primitive:fs/promises` seam,
matching the pattern already used by os/process/readline/etc. The existing
C# implementations (FsModuleInterpreter, FsModuleEmitter, the RuntimeEmitter
Fs* IL helpers) are retained and re-keyed as the primitives; only the public
name flips to stdlib/node/fs.ts.

The facade re-exports the (mode-symmetric) sync surface from primitive:fs and
derives the callback-async forms (fs.readFile(p, cb), ...) in TS from the
promise primitives. This structurally closes the compiled callback-async gap
(#970): callback fs now compiles and runs in compiled mode, byte-identical to
the interpreter, instead of failing to compile. Compiled output stays
standalone (no SharpTS.dll dependency).

Optional trailing args are dispatched by arity in the facade (the proven
process.ts/nextTick pattern) because the primitives default by argument count
and spread-to-primitive is not expanded by the compiled emitter.

Part of epic #968. Full xUnit suite green (14414/0); TypeScript conformance
baseline unchanged. Phase 1 of #969 (sync port + flip); the readFile real-async
proof follows.
…allback parity tests (#969)

The TS facade derives fs's callback APIs from the promise primitives, which
exposed a pre-existing interpreter bug: FsPromisesModuleInterpreter faulted its
task with a raw NodeError, which the interpreter's promise settling does not
recognize as a guest rejection — so fs.promises errors (and, via the facade, the
callback fs.readFile(p, cb) error path) were silently dropped or surfaced as a
host abort instead of reaching .then/.catch/await/the callback.

Reject instead with the same guest error object the callback path already builds
(FsModuleInterpreter.CreateErrorObject -> { code, syscall, path, message }),
wrapped in SharpTSPromiseRejectedException. Now .then/.catch/await and the
derived callbacks all receive the error with .code, matching compiled mode. This
also fixes fs.promises rejection in the interpreter generally (not just readFile).

Async proof (this issue's "1 async proof"): genuinely backgrounding the compiled
readFile (Task.Factory.StartNew) is functionally correct and parity-clean, but
$Promise(Task) does not Ref the event loop for a pending task, so fire-and-forget
work races program exit (flaky). That event-loop ref-counting for adopted Tasks
is the core of #971; documented in-code and on the issue. readFile stays
deterministic Task.FromResult here.

Adds 5 dual-mode parity tests (callback success / buffer / write-then-read /
missing-file error / promise round-trip). Full xUnit suite green (14423/1, the
one failure an unrelated live-DNS network smoke test that passes on retry).
@nickna nickna merged commit ae57ed0 into main Jun 28, 2026
2 checks passed
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.

fs: adopt primitive:fs + TypeScript facade architecture (decision)

1 participant