@ttsc/wasm 0.30.4 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +3 -3
  2. package/build/build-wasm.cjs +5 -4
  3. package/build/go-build-cache.cjs +347 -0
  4. package/build/pack-prepare.cjs +3 -2
  5. package/cmd/ttsc-wasm/main.go +22 -10
  6. package/cmd/ttsc-wasm/main_wasm.go +2 -2
  7. package/dist/.ttsc-wasm-build.json +138 -58
  8. package/dist/go.mod +7 -5
  9. package/dist/ttsc.wasm +2 -2
  10. package/go.mod +7 -5
  11. package/go.sum +10 -10
  12. package/host/api.go +34 -41
  13. package/host/doc.go +7 -6
  14. package/host/expose.go +42 -0
  15. package/host/fountain.go +192 -22
  16. package/host/host.go +10 -25
  17. package/host/host_native.go +3 -5
  18. package/host/invoke.go +133 -0
  19. package/host/plugin.go +37 -72
  20. package/host/result.go +108 -0
  21. package/lib/src/BootTtscWorkerTerminationError.d.ts +20 -0
  22. package/lib/src/BootTtscWorkerTerminationError.js +20 -0
  23. package/lib/src/BootTtscWorkerTerminationError.js.map +1 -1
  24. package/lib/src/MemFSError.d.ts +21 -2
  25. package/lib/src/MemFSError.js +25 -2
  26. package/lib/src/MemFSError.js.map +1 -1
  27. package/lib/src/bootTtsc.d.ts +49 -2
  28. package/lib/src/bootTtsc.js +89 -23
  29. package/lib/src/bootTtsc.js.map +1 -1
  30. package/lib/src/createMemFS.d.ts +45 -0
  31. package/lib/src/createMemFS.js +269 -66
  32. package/lib/src/createMemFS.js.map +1 -1
  33. package/lib/src/parseResult.d.ts +19 -0
  34. package/lib/src/parseResult.js +19 -0
  35. package/lib/src/parseResult.js.map +1 -1
  36. package/lib/src/structures/IBootResult.d.ts +20 -2
  37. package/lib/src/structures/IBootTtscOptions.d.ts +28 -6
  38. package/lib/src/structures/IFileStats.d.ts +51 -0
  39. package/lib/src/structures/IMemFSHost.d.ts +89 -0
  40. package/lib/src/structures/ITtscApi.d.ts +154 -9
  41. package/lib/src/structures/ITtscBuildOpts.d.ts +18 -1
  42. package/lib/src/structures/ITtscCompileResult.d.ts +20 -2
  43. package/lib/src/structures/ITtscDiagnostic.d.ts +25 -1
  44. package/lib/src/structures/ITtscFileQuery.d.ts +19 -1
  45. package/lib/src/structures/ITtscFountainDiagnosticsResult.d.ts +20 -1
  46. package/lib/src/structures/ITtscNodeAtPositionResult.d.ts +18 -1
  47. package/lib/src/structures/ITtscNodeInfo.d.ts +23 -2
  48. package/lib/src/structures/ITtscPluginOpts.d.ts +24 -3
  49. package/lib/src/structures/ITtscPositionQuery.d.ts +21 -4
  50. package/lib/src/structures/ITtscReleaseSnapshotResult.d.ts +22 -2
  51. package/lib/src/structures/ITtscResult.d.ts +31 -5
  52. package/lib/src/structures/ITtscSnapshotHandle.d.ts +19 -1
  53. package/lib/src/structures/ITtscSnapshotResult.d.ts +22 -1
  54. package/lib/src/structures/ITtscSnapshotsResult.d.ts +19 -1
  55. package/lib/src/structures/ITtscSourceFileTextResult.d.ts +19 -1
  56. package/lib/src/structures/ITtscSourceFilesResult.d.ts +23 -2
  57. package/lib/src/structures/ITtscSymbolAtPositionResult.d.ts +18 -1
  58. package/lib/src/structures/ITtscSymbolDeclaration.d.ts +25 -2
  59. package/lib/src/structures/ITtscSymbolInfo.d.ts +19 -1
  60. package/lib/src/structures/ITtscTransformResult.d.ts +21 -3
  61. package/lib/src/structures/ITtscTypeAtPositionResult.d.ts +18 -1
  62. package/lib/src/structures/ITtscTypeInfo.d.ts +19 -1
  63. package/lib/src/structures/ITtscVersion.d.ts +28 -1
  64. package/lib/src/structures/IWasmExecFS.d.ts +310 -0
  65. package/package.json +4 -2
  66. package/shim-vendor/shim/ast/enums_gen.go +4 -2
  67. package/shim-vendor/shim/ast/go.mod +5 -5
  68. package/shim-vendor/shim/ast/go.sum +10 -10
  69. package/shim-vendor/shim/ast/lint.go +1088 -20
  70. package/shim-vendor/shim/ast/parent.go +38 -22
  71. package/shim-vendor/shim/ast/safe_text.go +48 -26
  72. package/shim-vendor/shim/ast/shim.go +1307 -20
  73. package/shim-vendor/shim/ast/test/next_jsdoc_comment_location_reaches_variable_statement_test.go +51 -0
  74. package/shim-vendor/shim/ast/test/node_text_joins_multi_hop_qualified_name_test.go +24 -9
  75. package/shim-vendor/shim/ast/test/node_text_joins_one_hop_qualified_name_test.go +9 -5
  76. package/shim-vendor/shim/ast/test/node_text_returns_empty_for_nil_test.go +13 -5
  77. package/shim-vendor/shim/ast/test/node_text_returns_identifier_text_test.go +12 -5
  78. package/shim-vendor/shim/ast/test/node_text_skips_empty_qualified_left_test.go +35 -5
  79. package/shim-vendor/shim/astnav/go.mod +5 -5
  80. package/shim-vendor/shim/astnav/go.sum +12 -10
  81. package/shim-vendor/shim/astnav/shim.go +9 -0
  82. package/shim-vendor/shim/bundled/go.mod +7 -1
  83. package/shim-vendor/shim/bundled/go.sum +12 -0
  84. package/shim-vendor/shim/checker/enums_gen.go +4 -2
  85. package/shim-vendor/shim/checker/extra-shim.json +6 -0
  86. package/shim-vendor/shim/checker/go.mod +5 -5
  87. package/shim-vendor/shim/checker/go.sum +10 -10
  88. package/shim-vendor/shim/checker/node_builder_fields_gen.go +19 -0
  89. package/shim-vendor/shim/checker/shim.go +827 -114
  90. package/shim-vendor/shim/compiler/emit_sourcemap.go +68 -29
  91. package/shim-vendor/shim/compiler/emitassemble.go +36 -2
  92. package/shim-vendor/shim/compiler/emithook.go +13 -3
  93. package/shim-vendor/shim/compiler/go.mod +5 -5
  94. package/shim-vendor/shim/compiler/go.sum +10 -10
  95. package/shim-vendor/shim/compiler/incremental.go +56 -20
  96. package/shim-vendor/shim/compiler/resolution.go +92 -9
  97. package/shim-vendor/shim/core/enums_gen.go +2 -2
  98. package/shim-vendor/shim/core/go.mod +6 -1
  99. package/shim-vendor/shim/core/go.sum +10 -0
  100. package/shim-vendor/shim/core/modulekind.go +9 -0
  101. package/shim-vendor/shim/core/shim.go +157 -6
  102. package/shim-vendor/shim/diagnosticwriter/go.mod +10 -1
  103. package/shim-vendor/shim/diagnosticwriter/go.sum +20 -0
  104. package/shim-vendor/shim/diagnosticwriter/lint.go +188 -10
  105. package/shim-vendor/shim/diagnosticwriter/shim.go +14 -0
  106. package/shim-vendor/shim/lsp/go.mod +5 -5
  107. package/shim-vendor/shim/lsp/go.sum +10 -10
  108. package/shim-vendor/shim/lsp/shim.go +78 -2
  109. package/shim-vendor/shim/parser/go.mod +10 -1
  110. package/shim-vendor/shim/parser/go.sum +22 -0
  111. package/shim-vendor/shim/printer/emithost.go +46 -1
  112. package/shim-vendor/shim/printer/go.mod +5 -5
  113. package/shim-vendor/shim/printer/go.sum +10 -10
  114. package/shim-vendor/shim/printer/shim.go +114 -7
  115. package/shim-vendor/shim/printer/sourcemap.go +55 -0
  116. package/shim-vendor/shim/scanner/go.mod +10 -1
  117. package/shim-vendor/shim/scanner/go.sum +20 -0
  118. package/shim-vendor/shim/scanner/regexp.go +19 -4
  119. package/shim-vendor/shim/scanner/shim.go +3 -0
  120. package/shim-vendor/shim/stringutil/extra-shim.json +1 -0
  121. package/shim-vendor/shim/stringutil/go.mod +5 -0
  122. package/shim-vendor/shim/stringutil/go.sum +2 -0
  123. package/shim-vendor/shim/stringutil/shim.go +119 -0
  124. package/shim-vendor/shim/transformers/go.mod +5 -5
  125. package/shim-vendor/shim/transformers/go.sum +12 -10
  126. package/shim-vendor/shim/transformers/shim.go +12 -3
  127. package/shim-vendor/shim/tsoptions/command_line_raw.go +44 -0
  128. package/shim-vendor/shim/tsoptions/go.mod +10 -1
  129. package/shim-vendor/shim/tsoptions/go.sum +22 -0
  130. package/shim-vendor/shim/tsoptions/shim.go +1 -1
  131. package/shim-vendor/shim/tspath/go.mod +1 -1
  132. package/shim-vendor/shim/tspath/go.sum +6 -0
  133. package/shim-vendor/shim/vfs/cachedvfs/go.mod +3 -1
  134. package/shim-vendor/shim/vfs/cachedvfs/go.sum +12 -0
  135. package/shim-vendor/shim/vfs/decode.go +26 -0
  136. package/shim-vendor/shim/vfs/decode_test.go +42 -0
  137. package/shim-vendor/shim/vfs/go.mod +7 -1
  138. package/shim-vendor/shim/vfs/go.sum +12 -0
  139. package/shim-vendor/shim/vfs/osvfs/go.mod +7 -1
  140. package/shim-vendor/shim/vfs/osvfs/go.sum +12 -0
  141. package/src/BootTtscWorkerTerminationError.ts +22 -0
  142. package/src/MemFSError.ts +29 -2
  143. package/src/bootTtsc.ts +104 -28
  144. package/src/createMemFS.ts +291 -64
  145. package/src/parseResult.ts +19 -0
  146. package/src/structures/IBootResult.ts +21 -2
  147. package/src/structures/IBootTtscOptions.ts +32 -6
  148. package/src/structures/IFileStats.ts +65 -0
  149. package/src/structures/IMemFSHost.ts +97 -0
  150. package/src/structures/ITtscApi.ts +154 -9
  151. package/src/structures/ITtscBuildOpts.ts +19 -1
  152. package/src/structures/ITtscCompileResult.ts +21 -2
  153. package/src/structures/ITtscDiagnostic.ts +32 -1
  154. package/src/structures/ITtscFileQuery.ts +19 -1
  155. package/src/structures/ITtscFountainDiagnosticsResult.ts +20 -1
  156. package/src/structures/ITtscNodeAtPositionResult.ts +18 -1
  157. package/src/structures/ITtscNodeInfo.ts +27 -2
  158. package/src/structures/ITtscPluginOpts.ts +28 -3
  159. package/src/structures/ITtscPositionQuery.ts +21 -4
  160. package/src/structures/ITtscReleaseSnapshotResult.ts +22 -2
  161. package/src/structures/ITtscResult.ts +34 -5
  162. package/src/structures/ITtscSnapshotHandle.ts +19 -1
  163. package/src/structures/ITtscSnapshotResult.ts +22 -1
  164. package/src/structures/ITtscSnapshotsResult.ts +19 -1
  165. package/src/structures/ITtscSourceFileTextResult.ts +19 -1
  166. package/src/structures/ITtscSourceFilesResult.ts +23 -2
  167. package/src/structures/ITtscSymbolAtPositionResult.ts +18 -1
  168. package/src/structures/ITtscSymbolDeclaration.ts +27 -2
  169. package/src/structures/ITtscSymbolInfo.ts +23 -1
  170. package/src/structures/ITtscTransformResult.ts +22 -3
  171. package/src/structures/ITtscTypeAtPositionResult.ts +18 -1
  172. package/src/structures/ITtscTypeInfo.ts +20 -1
  173. package/src/structures/ITtscVersion.ts +33 -1
  174. package/src/structures/IWasmExecFS.ts +336 -0
@@ -30,6 +30,9 @@ interface INode {
30
30
  kind: "file" | "dir";
31
31
  data: Uint8Array;
32
32
  mtimeMs: number;
33
+
34
+ /** UTF-8 decoding of current bytes, cleared by every content mutation. */
35
+ text?: string;
33
36
  }
34
37
 
35
38
  const encoder = new TextEncoder();
@@ -70,13 +73,112 @@ function normalize(p: string): string {
70
73
  * descendants. An operation that cannot satisfy that throws (`writeFile`,
71
74
  * `mkdirp`) or reports a POSIX error through its callback, having changed no
72
75
  * node, byte, or descriptor.
76
+ *
77
+ * @evidence contracts/common.md#principled-implementation
78
+ * The owned Node-shaped fs bridge uses maps for virtual node/descriptor
79
+ * identity and preserves opened nodes independently from paths. The Go js/wasm
80
+ * callback interface is the authority; unsupported link and metadata mutation
81
+ * capabilities are explicit in IWasmExecFS.
82
+ * @evidence contracts/common.md#clear-and-simple-design
83
+ * One closure owns the tree, indexes and descriptor identities. Shared helpers
84
+ * centralize path validation, insertion/removal and byte mutation; separate
85
+ * stream captures and pipe queues reflect non-seekable lifetimes rather than
86
+ * adding filesystem nodes that would blur their operation contracts.
87
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts
88
+ * Virtual flag/errno values are bridge constants, not native-host guesses.
89
+ * The implementation owns its maps rather than patching foreign filesystem
90
+ * methods. Directory indexes, geometric file growth and linked pipe queues
91
+ * correct repeated global scans, full-buffer copies and queue shifts at their
92
+ * owners, without size exceptions or benchmark-specific paths.
93
+ * @evidence contracts/common.md#meaningful-documentation
94
+ * Native paragraphs explain installation, copying and tree ownership; helper
95
+ * and interface comments state cost and unsupported capabilities. This follows
96
+ * the documentation skill's paragraph, units and rationale guidance.
97
+ * @evidence contracts/performance.md#efficient-algorithms
98
+ * Directory lookup visits D direct entries and sorts them in O(D log D),
99
+ * emptiness uses the indexed child count, and rename visits only S subtree
100
+ * nodes plus F open descriptors. Growing file writes amortize copying through
101
+ * geometric capacity; pipe enqueue/dequeue change linked endpoints in O(1).
102
+ * Path normalization costs O(P) path characters; ancestor validation builds
103
+ * d prefixes with total length K, costing O(K + d) time and temporary space
104
+ * under expected constant-time map access after hashing each prefix. Output fragments
105
+ * append without flattening prior text; reading a dirty B-byte capture costs
106
+ * O(B), while an unchanged capture read reuses its materialized string.
107
+ * @evidence contracts/performance.md#reuse-equivalent-work
108
+ * Child membership is maintained once by node insertion/deletion rather than
109
+ * reconstructed for every listing. Text decoding is cached on node identity
110
+ * and invalidated by every content mutation. Open descriptors share their
111
+ * actual inode; public byte reads still copy to preserve caller isolation.
112
+ * Each output stream keeps its own incremental UTF-8 decoder, so a split
113
+ * character is decoded once and capture reads reuse the materialized string
114
+ * until another fragment arrives.
115
+ * @evidence contracts/performance.md#bound-retention-and-release-resources
116
+ * Each host owns its tree, indexes, captures and descriptors. Unlink/rmdir
117
+ * remove tree entries, close releases descriptors, and consumed pipe entries
118
+ * are detached. File write capacity stays at most twice logical length; truncate
119
+ * and replacement release spare storage. Total input and captured output have
120
+ * no fixed bound, so the caller owns deletion, resetStdio and host lifetime.
73
121
  */
74
122
  export function createMemFS(): IMemFSHost {
75
123
  const nodes = new Map<string, INode>();
124
+ const children = new Map<string, Set<string>>([["/", new Set()]]);
76
125
  nodes.set("/", { kind: "dir", data: new Uint8Array(), mtimeMs: Date.now() });
77
126
 
78
- const stdout = { buffer: "" };
79
- const stderr = { buffer: "" };
127
+ /** Keep parent membership and directory indexes with the owned node tree. */
128
+ function setNode(p: string, node: INode): void {
129
+ nodes.set(p, node);
130
+ children.get(parentDir(p))!.add(p);
131
+ if (node.kind === "dir" && !children.has(p)) children.set(p, new Set());
132
+ }
133
+
134
+ /** Remove a validated node and its index; open descriptors retain the node. */
135
+ function deleteNode(p: string): void {
136
+ nodes.delete(p);
137
+ children.get(parentDir(p))!.delete(p);
138
+ children.delete(p);
139
+ }
140
+
141
+ /** Decode stream fragments incrementally and materialize capture text on read. */
142
+ function createCapture() {
143
+ let streamDecoder = new TextDecoder();
144
+ let chunks: string[] = [];
145
+ let cached = "";
146
+ let dirty = false;
147
+ const append = (text: string): string => {
148
+ if (text.length > 0) {
149
+ chunks.push(text);
150
+ cached = "";
151
+ dirty = true;
152
+ }
153
+ return text;
154
+ };
155
+ return {
156
+ output: {
157
+ get buffer(): string {
158
+ if (dirty) {
159
+ cached = chunks.join("");
160
+ chunks = cached.length > 0 ? [cached] : [];
161
+ dirty = false;
162
+ }
163
+ return cached;
164
+ },
165
+ set buffer(value: string) {
166
+ streamDecoder = new TextDecoder();
167
+ chunks = value.length > 0 ? [value] : [];
168
+ cached = value;
169
+ dirty = false;
170
+ },
171
+ },
172
+ write(bytes: Uint8Array): string {
173
+ return append(streamDecoder.decode(bytes, { stream: true }));
174
+ },
175
+ flush(): void {
176
+ append(streamDecoder.decode());
177
+ },
178
+ };
179
+ }
180
+ const stdout = createCapture();
181
+ const stderr = createCapture();
80
182
 
81
183
  /**
82
184
  * One open descriptor.
@@ -125,9 +227,30 @@ export function createMemFS(): IMemFSHost {
125
227
  // These fds only ever come from a direct JavaScript `fs.pipe2` call. Go's
126
228
  // wasm `os.Pipe` returns ENOSYS without crossing the `globalThis.fs` bridge,
127
229
  // so the Go runtime never reaches this state — see IWasmExecFS.pipe2.
230
+ interface IQueueEntry<T> {
231
+ value: T;
232
+ next?: IQueueEntry<T>;
233
+ }
234
+ interface IQueue<T> {
235
+ head?: IQueueEntry<T>;
236
+ tail?: IQueueEntry<T>;
237
+ }
238
+ function enqueue<T>(queue: IQueue<T>, value: T): void {
239
+ const entry = { value };
240
+ if (queue.tail) queue.tail.next = entry;
241
+ else queue.head = entry;
242
+ queue.tail = entry;
243
+ }
244
+ function dequeue<T>(queue: IQueue<T>): T | undefined {
245
+ const entry = queue.head;
246
+ if (!entry) return undefined;
247
+ queue.head = entry.next;
248
+ if (!queue.head) queue.tail = undefined;
249
+ return entry.value;
250
+ }
128
251
  interface IPipeState {
129
- buffers: Uint8Array[];
130
- pendingReaders: Array<{
252
+ buffers: IQueue<Uint8Array>;
253
+ pendingReaders: IQueue<{
131
254
  buffer: Uint8Array;
132
255
  offset: number;
133
256
  length: number;
@@ -136,6 +259,7 @@ export function createMemFS(): IMemFSHost {
136
259
  readFd: number;
137
260
  writeFd: number;
138
261
  writeClosed: boolean;
262
+ readClosed: boolean;
139
263
  }
140
264
  const pipes = new Map<number, IPipeState>();
141
265
 
@@ -150,13 +274,13 @@ export function createMemFS(): IMemFSHost {
150
274
  length: number,
151
275
  ): number {
152
276
  let written = 0;
153
- while (state.buffers.length > 0 && written < length) {
154
- const chunk = state.buffers[0]!;
277
+ while (state.buffers.head && written < length) {
278
+ const chunk = state.buffers.head.value;
155
279
  const copyLen = Math.min(chunk.byteLength, length - written);
156
280
  buffer.set(chunk.subarray(0, copyLen), offset + written);
157
281
  written += copyLen;
158
- if (copyLen >= chunk.byteLength) state.buffers.shift();
159
- else state.buffers[0] = chunk.subarray(copyLen);
282
+ if (copyLen >= chunk.byteLength) dequeue(state.buffers);
283
+ else state.buffers.head.value = chunk.subarray(copyLen);
160
284
  }
161
285
  return written;
162
286
  }
@@ -167,10 +291,10 @@ export function createMemFS(): IMemFSHost {
167
291
  */
168
292
  function flushPipeReaders(state: IPipeState): void {
169
293
  while (
170
- state.pendingReaders.length > 0 &&
171
- (state.buffers.length > 0 || state.writeClosed)
294
+ state.pendingReaders.head &&
295
+ (state.buffers.head || state.writeClosed)
172
296
  ) {
173
- const reader = state.pendingReaders.shift()!;
297
+ const reader = dequeue(state.pendingReaders)!;
174
298
  const n = drainPipeInto(
175
299
  state,
176
300
  reader.buffer,
@@ -218,7 +342,7 @@ export function createMemFS(): IMemFSHost {
218
342
  /** Materialize the directories `missingDirs` reported. */
219
343
  function createDirs(paths: string[]): void {
220
344
  for (const path of paths)
221
- nodes.set(path, {
345
+ setNode(path, {
222
346
  kind: "dir",
223
347
  data: new Uint8Array(),
224
348
  mtimeMs: Date.now(),
@@ -249,23 +373,89 @@ export function createMemFS(): IMemFSHost {
249
373
 
250
374
  /** True when `dir` has at least one descendant node in the tree. */
251
375
  function hasChildren(dir: string): boolean {
252
- const prefix = dir === "/" ? "/" : dir + "/";
253
- for (const key of nodes.keys()) {
254
- if (key !== dir && key.startsWith(prefix)) return true;
376
+ return (children.get(dir)?.size ?? 0) > 0;
377
+ }
378
+
379
+ /**
380
+ * Allocate zero-filled storage for a file of `length` bytes.
381
+ *
382
+ * The engine refuses a length it cannot allocate with a `RangeError`, which
383
+ * carries no POSIX code and would escape a callback that promises one. That
384
+ * refusal is the virtual filesystem's maximum file size, so it is reported as
385
+ * `EFBIG`; the caller has not yet changed any node when this throws.
386
+ */
387
+ function allocateFileData(
388
+ length: number,
389
+ syscall: string,
390
+ path: string,
391
+ ): Uint8Array {
392
+ try {
393
+ return new Uint8Array(length);
394
+ } catch (error) {
395
+ if (error instanceof RangeError)
396
+ throw new MemFSError("EFBIG", syscall, path);
397
+ throw error;
255
398
  }
256
- return false;
257
399
  }
258
400
 
259
401
  /**
260
402
  * Grow or shrink a file's byte buffer to exactly `length`, zero-filling any
261
403
  * extension. Callers validate `length >= 0` first.
262
404
  */
263
- function resizeFileData(data: Uint8Array, length: number): Uint8Array {
264
- const next = new Uint8Array(length);
405
+ function resizeFileData(
406
+ data: Uint8Array,
407
+ length: number,
408
+ syscall: string,
409
+ path: string,
410
+ ): Uint8Array {
411
+ const next = allocateFileData(length, syscall, path);
265
412
  next.set(data.subarray(0, Math.min(length, data.byteLength)));
266
413
  return next;
267
414
  }
268
415
 
416
+ /** Grow writes geometrically; the public view still exposes only file bytes. */
417
+ function growFileData(
418
+ data: Uint8Array,
419
+ length: number,
420
+ syscall: string,
421
+ path: string,
422
+ ): Uint8Array {
423
+ const capacity = data.buffer.byteLength - data.byteOffset;
424
+ if (length <= capacity) {
425
+ const next = new Uint8Array(data.buffer, data.byteOffset, length);
426
+ next.fill(0, data.byteLength);
427
+ return next;
428
+ }
429
+ // Spare capacity is only an optimization: when the engine refuses the
430
+ // doubled size, the exact length still satisfies the write.
431
+ let next: Uint8Array;
432
+ if (data.byteLength * 2 > length) {
433
+ try {
434
+ next = allocateFileData(data.byteLength * 2, syscall, path);
435
+ } catch {
436
+ next = allocateFileData(length, syscall, path);
437
+ }
438
+ } else next = allocateFileData(length, syscall, path);
439
+ next.set(data);
440
+ return next.subarray(0, length);
441
+ }
442
+
443
+ /** Reject invalid byte slices before a write or queued read can mutate state. */
444
+ function validSlice(
445
+ buffer: Uint8Array,
446
+ offset: number,
447
+ length: number,
448
+ ): boolean {
449
+ return (
450
+ Number.isSafeInteger(offset) &&
451
+ Number.isSafeInteger(length) &&
452
+ offset >= 0 &&
453
+ length >= 0 &&
454
+ offset <= buffer.byteLength &&
455
+ length <= buffer.byteLength - offset
456
+ );
457
+ }
458
+
269
459
  /**
270
460
  * Write `view` through descriptor `entry` and return the bytes stored.
271
461
  *
@@ -292,12 +482,16 @@ export function createMemFS(): IMemFSHost {
292
482
  const start = entry.append
293
483
  ? node.data.byteLength
294
484
  : (position ?? entry.position);
295
- if (!Number.isInteger(start) || start < 0)
485
+ if (!Number.isSafeInteger(start) || start < 0)
296
486
  throw new MemFSError("EINVAL", syscall, entry.path);
297
487
  if (view.byteLength === 0) return 0;
298
488
  const end = start + view.byteLength;
299
- if (end > node.data.byteLength) node.data = resizeFileData(node.data, end);
489
+ if (!Number.isSafeInteger(end))
490
+ throw new MemFSError("EINVAL", syscall, entry.path);
491
+ if (end > node.data.byteLength)
492
+ node.data = growFileData(node.data, end, syscall, entry.path);
300
493
  node.data.set(view, start);
494
+ node.text = undefined;
301
495
  node.mtimeMs = Date.now();
302
496
  if (position === null || entry.append) entry.position = end;
303
497
  return view.byteLength;
@@ -311,19 +505,20 @@ export function createMemFS(): IMemFSHost {
311
505
  */
312
506
  function moveSubtree(src: string, dest: string): void {
313
507
  const srcPrefix = src + "/";
314
- const moves: Array<[string, INode]> = [];
315
- for (const [key, node] of nodes) {
316
- if (key === src) moves.push([dest, node]);
317
- else if (key.startsWith(srcPrefix))
318
- moves.push([dest + "/" + key.slice(srcPrefix.length), node]);
508
+ const moves: Array<[string, string, INode]> = [];
509
+ const pending = [src];
510
+ while (pending.length > 0) {
511
+ const key = pending.pop()!;
512
+ const target = key === src ? dest : dest + key.slice(src.length);
513
+ moves.push([key, target, nodes.get(key)!]);
514
+ for (const child of children.get(key) ?? []) pending.push(child);
319
515
  }
320
516
  // Delete the whole source subtree first so a nested overwrite cannot leave
321
517
  // a stale descendant behind, then reinsert at the destination prefix. Any
322
518
  // pre-existing `dest` node is replaced by the reinsert.
323
- for (const key of [...nodes.keys()]) {
324
- if (key === src || key.startsWith(srcPrefix)) nodes.delete(key);
325
- }
326
- for (const [key, node] of moves) nodes.set(key, node);
519
+ for (let i = moves.length - 1; i >= 0; --i) deleteNode(moves[i]![0]);
520
+ if (nodes.has(dest)) deleteNode(dest);
521
+ for (const [, key, node] of moves) setNode(key, node);
327
522
  for (const entry of fdTable.values()) {
328
523
  if (entry.path === src) entry.path = dest;
329
524
  else if (entry.path.startsWith(srcPrefix))
@@ -350,8 +545,9 @@ export function createMemFS(): IMemFSHost {
350
545
  // file observe the replacement instead of a detached predecessor.
351
546
  if (existing) {
352
547
  existing.data = bytes;
548
+ existing.text = undefined;
353
549
  existing.mtimeMs = Date.now();
354
- } else nodes.set(norm, { kind: "file", data: bytes, mtimeMs: Date.now() });
550
+ } else setNode(norm, { kind: "file", data: bytes, mtimeMs: Date.now() });
355
551
  }
356
552
 
357
553
  function readFile(p: string): Uint8Array | null {
@@ -361,8 +557,9 @@ export function createMemFS(): IMemFSHost {
361
557
  }
362
558
 
363
559
  function readFileText(p: string): string | null {
364
- const bytes = readFile(p);
365
- return bytes === null ? null : decoder.decode(bytes);
560
+ const node = nodes.get(normalize(p));
561
+ if (!node || node.kind !== "file") return null;
562
+ return (node.text ??= decoder.decode(node.data));
366
563
  }
367
564
 
368
565
  function exists(p: string): boolean {
@@ -400,25 +597,19 @@ export function createMemFS(): IMemFSHost {
400
597
  }
401
598
 
402
599
  /**
403
- * Return immediate children of directory `p`, sorted alphabetically.
600
+ * Return immediate children of directory `p`, sorted by UTF-16 code unit.
404
601
  *
405
- * Uses a linear scan over the node map and a `Set` to deduplicate nested
406
- * paths into direct-child names — O(n) in the number of total nodes.
602
+ * The maintained child index limits work to this directory's entries. Sorting
603
+ * provides deterministic output without scanning unrelated project files.
407
604
  */
408
605
  function readdirSync(p: string): string[] {
409
606
  const norm = normalize(p);
410
607
  const node = nodes.get(norm);
411
608
  if (!node) throw new MemFSError("ENOENT", "readdir", norm);
412
609
  if (node.kind !== "dir") throw new MemFSError("ENOTDIR", "readdir", norm);
413
- const prefix = norm === "/" ? "/" : norm + "/";
414
- const direct = new Set<string>();
415
- for (const key of nodes.keys()) {
416
- if (!key.startsWith(prefix) || key === norm) continue;
417
- const rest = key.slice(prefix.length);
418
- const cut = rest.indexOf("/");
419
- direct.add(cut === -1 ? rest : rest.slice(0, cut));
420
- }
421
- return [...direct].sort();
610
+ return [...children.get(norm)!]
611
+ .map((child) => child.slice(norm === "/" ? 1 : norm.length + 1))
612
+ .sort();
422
613
  }
423
614
 
424
615
  const fs: IWasmExecFS = {
@@ -434,12 +625,11 @@ export function createMemFS(): IMemFSHost {
434
625
 
435
626
  writeSync(fd, buf) {
436
627
  if (fd === 1) {
437
- stdout.buffer += decoder.decode(buf);
628
+ stdout.write(buf);
438
629
  return buf.length;
439
630
  }
440
631
  if (fd === 2) {
441
- const text = decoder.decode(buf);
442
- stderr.buffer += text;
632
+ const text = stderr.write(buf);
443
633
  // Surface wasm-side stderr to the host console in real-time so plugin
444
634
  // debug prints / Go panics aren't trapped inside the MemFS buffer.
445
635
  // Stripped at end of message for cleaner display.
@@ -466,6 +656,8 @@ export function createMemFS(): IMemFSHost {
466
656
 
467
657
  write(fd, buf, offset, length, position, callback) {
468
658
  try {
659
+ if (!validSlice(buf, offset, length))
660
+ throw new MemFSError("EINVAL", "write");
469
661
  // Pipe write: snapshot the data (caller may reuse `buf`) and queue.
470
662
  // Synchronously wake any blocked reader so the cooperative wasm
471
663
  // scheduler doesn't deadlock waiting for a future fs roundtrip.
@@ -475,10 +667,14 @@ export function createMemFS(): IMemFSHost {
475
667
  callback(new MemFSError("EBADF", "write"), 0);
476
668
  return;
477
669
  }
670
+ if (pipeState.readClosed) {
671
+ callback(new MemFSError("EPIPE", "write"), 0);
672
+ return;
673
+ }
478
674
  if (length > 0) {
479
675
  const copy = new Uint8Array(length);
480
676
  copy.set(buf.subarray(offset, offset + length));
481
- pipeState.buffers.push(copy);
677
+ enqueue(pipeState.buffers, copy);
482
678
  flushPipeReaders(pipeState);
483
679
  }
484
680
  callback(null, length);
@@ -531,7 +727,7 @@ export function createMemFS(): IMemFSHost {
531
727
  if (missing.length > 0)
532
728
  throw new MemFSError("ENOENT", "open", missing[0]!);
533
729
  node = { kind: "file", data: new Uint8Array(), mtimeMs: Date.now() };
534
- nodes.set(norm, node);
730
+ setNode(norm, node);
535
731
  } else {
536
732
  if (creating && exclusive)
537
733
  throw new MemFSError("EEXIST", "open", norm);
@@ -546,6 +742,7 @@ export function createMemFS(): IMemFSHost {
546
742
  }
547
743
  if (truncating) {
548
744
  node.data = new Uint8Array();
745
+ node.text = undefined;
549
746
  node.mtimeMs = Date.now();
550
747
  }
551
748
  const fd = nextFd++;
@@ -566,11 +763,18 @@ export function createMemFS(): IMemFSHost {
566
763
  close(fd, callback) {
567
764
  const pipeState = pipes.get(fd);
568
765
  if (pipeState) {
766
+ pipes.delete(fd);
569
767
  if (fd === pipeState.writeFd) {
570
768
  pipeState.writeClosed = true;
571
769
  flushPipeReaders(pipeState);
770
+ } else {
771
+ pipeState.readClosed = true;
772
+ pipeState.buffers = {};
773
+ while (pipeState.pendingReaders.head) {
774
+ const reader = dequeue(pipeState.pendingReaders)!;
775
+ reader.callback(new MemFSError("EBADF", "read"), 0);
776
+ }
572
777
  }
573
- pipes.delete(fd);
574
778
  callback(null);
575
779
  return;
576
780
  }
@@ -578,11 +782,17 @@ export function createMemFS(): IMemFSHost {
578
782
  callback(new MemFSError("EBADF", "close"));
579
783
  return;
580
784
  }
785
+ if (fd === 1) stdout.flush();
786
+ if (fd === 2) stderr.flush();
581
787
  if (fd > 2) fdTable.delete(fd);
582
788
  callback(null);
583
789
  },
584
790
 
585
791
  read(fd, buffer, offset, length, position, callback) {
792
+ if (!validSlice(buffer, offset, length)) {
793
+ callback(new MemFSError("EINVAL", "read"), 0);
794
+ return;
795
+ }
586
796
  // Pipe read: drain queued chunks; block (defer callback) on empty.
587
797
  // Empty + writeClosed → return 0 (EOF). Position is ignored for pipes.
588
798
  const pipeState = pipes.get(fd);
@@ -591,7 +801,11 @@ export function createMemFS(): IMemFSHost {
591
801
  callback(new MemFSError("EBADF", "read"), 0);
592
802
  return;
593
803
  }
594
- if (pipeState.buffers.length > 0) {
804
+ if (length === 0) {
805
+ callback(null, 0);
806
+ return;
807
+ }
808
+ if (pipeState.buffers.head) {
595
809
  const n = drainPipeInto(pipeState, buffer, offset, length);
596
810
  callback(null, n);
597
811
  return;
@@ -600,7 +814,7 @@ export function createMemFS(): IMemFSHost {
600
814
  callback(null, 0);
601
815
  return;
602
816
  }
603
- pipeState.pendingReaders.push({ buffer, offset, length, callback });
817
+ enqueue(pipeState.pendingReaders, { buffer, offset, length, callback });
604
818
  return;
605
819
  }
606
820
  const entry = fdTable.get(fd);
@@ -620,7 +834,7 @@ export function createMemFS(): IMemFSHost {
620
834
  return;
621
835
  }
622
836
  const start = position ?? entry.position;
623
- if (!Number.isInteger(start) || start < 0) {
837
+ if (!Number.isSafeInteger(start) || start < 0) {
624
838
  callback(new MemFSError("EINVAL", "read", entry.path), 0);
625
839
  return;
626
840
  }
@@ -650,7 +864,7 @@ export function createMemFS(): IMemFSHost {
650
864
  if (!parent) throw new MemFSError("ENOENT", "mkdir", parentDir(norm));
651
865
  if (parent.kind !== "dir")
652
866
  throw new MemFSError("ENOTDIR", "mkdir", parentDir(norm));
653
- nodes.set(norm, {
867
+ setNode(norm, {
654
868
  kind: "dir",
655
869
  data: new Uint8Array(),
656
870
  mtimeMs: Date.now(),
@@ -729,7 +943,7 @@ export function createMemFS(): IMemFSHost {
729
943
  callback(new MemFSError("EISDIR", "unlink", norm));
730
944
  return;
731
945
  }
732
- nodes.delete(norm);
946
+ deleteNode(norm);
733
947
  callback(null);
734
948
  },
735
949
 
@@ -805,7 +1019,7 @@ export function createMemFS(): IMemFSHost {
805
1019
  callback(new MemFSError("ENOTEMPTY", "rmdir", norm));
806
1020
  return;
807
1021
  }
808
- nodes.delete(norm);
1022
+ deleteNode(norm);
809
1023
  callback(null);
810
1024
  },
811
1025
 
@@ -851,7 +1065,13 @@ export function createMemFS(): IMemFSHost {
851
1065
  callback(new MemFSError("EINVAL", "truncate", norm));
852
1066
  return;
853
1067
  }
854
- node.data = resizeFileData(node.data, length);
1068
+ try {
1069
+ node.data = resizeFileData(node.data, length, "truncate", norm);
1070
+ } catch (err) {
1071
+ callback(err as NodeJS.ErrnoException);
1072
+ return;
1073
+ }
1074
+ node.text = undefined;
855
1075
  node.mtimeMs = Date.now();
856
1076
  callback(null);
857
1077
  },
@@ -883,7 +1103,13 @@ export function createMemFS(): IMemFSHost {
883
1103
  callback(new MemFSError("EINVAL", "ftruncate", entry.path));
884
1104
  return;
885
1105
  }
886
- node.data = resizeFileData(node.data, length);
1106
+ try {
1107
+ node.data = resizeFileData(node.data, length, "ftruncate", entry.path);
1108
+ } catch (err) {
1109
+ callback(err as NodeJS.ErrnoException);
1110
+ return;
1111
+ }
1112
+ node.text = undefined;
887
1113
  node.mtimeMs = Date.now();
888
1114
  callback(null);
889
1115
  },
@@ -891,11 +1117,12 @@ export function createMemFS(): IMemFSHost {
891
1117
  const readFd = nextFd++;
892
1118
  const writeFd = nextFd++;
893
1119
  const state: IPipeState = {
894
- buffers: [],
895
- pendingReaders: [],
1120
+ buffers: {},
1121
+ pendingReaders: {},
896
1122
  readFd,
897
1123
  writeFd,
898
1124
  writeClosed: false,
1125
+ readClosed: false,
899
1126
  };
900
1127
  pipes.set(readFd, state);
901
1128
  pipes.set(writeFd, state);
@@ -910,11 +1137,11 @@ export function createMemFS(): IMemFSHost {
910
1137
  readFileText,
911
1138
  exists,
912
1139
  mkdirp,
913
- stdout,
914
- stderr,
1140
+ stdout: stdout.output,
1141
+ stderr: stderr.output,
915
1142
  resetStdio() {
916
- stdout.buffer = "";
917
- stderr.buffer = "";
1143
+ stdout.output.buffer = "";
1144
+ stderr.output.buffer = "";
918
1145
  },
919
1146
  };
920
1147
  }
@@ -5,6 +5,25 @@ import type { ITtscResult } from "./structures/ITtscResult";
5
5
  *
6
6
  * The wasm returns JSON as a string because `js.ValueOf` does not handle large
7
7
  * nested maps efficiently. Callers JSON.parse exactly once at the boundary.
8
+ *
9
+ * Returns null for empty or invalid JSON. The generic parameter describes the
10
+ * expected payload; this function does not validate its schema or exit code.
11
+ *
12
+ * @evidence contracts/common.md#principled-implementation
13
+ * JSON.parse handles the native string envelope using the standard parser;
14
+ * the generic cast expresses the caller's expected endpoint payload.
15
+ * @evidence contracts/common.md#clear-and-simple-design
16
+ * One parser owns envelope decoding; endpoint selection and exit handling
17
+ * remain with the caller instead of introducing another result schema.
18
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts
19
+ * Parser failure returns explicit null rather than synthesizing an expected
20
+ * payload. The cast is documented as an expectation, not runtime validation.
21
+ * @evidence contracts/common.md#meaningful-documentation
22
+ * Separate JSDoc paragraphs explain the boundary and failure/schema limits,
23
+ * following the documentation skill's guidance to give each paragraph one role.
24
+ * @evidenceExclude contracts/performance.md#bound-retention-and-release-resources parseResult retains nothing; the decoded value transfers to the caller.
25
+ * @evidenceExclude contracts/performance.md#efficient-algorithms parseResult delegates to JSON.parse, linear in the string length, and chooses no algorithm of its own.
26
+ * @evidenceExclude contracts/performance.md#reuse-equivalent-work parseResult decodes one string per call and coordinates no repeated or concurrent requests.
8
27
  */
9
28
  export function parseResult<T>(result: ITtscResult): T | null {
10
29
  if (!result.result) return null;
@@ -1,10 +1,29 @@
1
1
  import type { IMemFSHost } from "./IMemFSHost";
2
2
  import type { ITtscApi } from "./ITtscApi";
3
3
 
4
- /** Handle returned by `bootTtsc` once the wasm is ready. */
4
+ /**
5
+ * Handle returned by `bootTtsc` once the wasm is ready.
6
+ *
7
+ * @evidence contracts/common.md#principled-implementation
8
+ * Pairing the typed bridge and the exact MemFS host preserves the runtime's
9
+ * filesystem identity instead of exposing the API separately from its inputs.
10
+ * @evidence contracts/common.md#clear-and-simple-design
11
+ * The result pairs the two objects needed to seed and invoke this runtime;
12
+ * loading options and mutable boot coordination remain outside the handle.
13
+ * @evidence contracts/common.md#prohibited-implementation-shortcuts
14
+ * The host is the booted runtime's actual backing object, not an unrelated
15
+ * replacement presented to a consumer after readiness.
16
+ * @evidence contracts/common.md#meaningful-documentation
17
+ * Native member JSDoc explains bridge and shared filesystem ownership, following
18
+ * the documentation skill's requirement for nonobvious usage context.
19
+ * @evidenceExclude contracts/performance.md#bound-retention-and-release-resources IBootResult is a data interface and acquires no handle, task or retained state.
20
+ * @evidenceExclude contracts/performance.md#efficient-algorithms IBootResult is a data interface and chooses no algorithm or processing strategy.
21
+ * @evidenceExclude contracts/performance.md#reuse-equivalent-work IBootResult is a data interface and coordinates no shared or repeated computation.
22
+ */
5
23
  export interface IBootResult {
6
- /** The typed API proxy bound by the wasm to `globalThis[apiName]`. */
24
+ /** The typed API object bound by the wasm to `globalThis[apiName]`. */
7
25
  api: ITtscApi;
26
+
8
27
  /** The MemFS instance shared with the wasm's virtual filesystem. */
9
28
  host: IMemFSHost;
10
29
  }