@ttsc/wasm 0.19.3 → 0.20.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 (33) hide show
  1. package/README.md +4 -2
  2. package/build/build-wasm.cjs +131 -126
  3. package/dist/.ttsc-wasm-build.json +319 -0
  4. package/dist/go.mod +2 -0
  5. package/dist/ttsc.wasm +0 -0
  6. package/go.mod +2 -0
  7. package/host/fountain.go +48 -19
  8. package/lib/src/createMemFS.d.ts +6 -0
  9. package/lib/src/createMemFS.js +247 -95
  10. package/lib/src/createMemFS.js.map +1 -1
  11. package/lib/src/structures/ITtscApi.d.ts +8 -5
  12. package/lib/src/structures/ITtscNodeAtPositionResult.d.ts +1 -1
  13. package/lib/src/structures/ITtscNodeInfo.d.ts +1 -1
  14. package/lib/src/structures/ITtscPositionQuery.d.ts +4 -2
  15. package/lib/src/structures/ITtscSymbolAtPositionResult.d.ts +1 -1
  16. package/lib/src/structures/ITtscTypeAtPositionResult.d.ts +1 -1
  17. package/lib/src/structures/IWasmExecFS.d.ts +14 -4
  18. package/package.json +1 -1
  19. package/shim-vendor/shim/ast/shim.go +48 -3
  20. package/shim-vendor/shim/astnav/go.mod +14 -0
  21. package/shim-vendor/shim/astnav/go.sum +20 -0
  22. package/shim-vendor/shim/astnav/shim.go +15 -0
  23. package/shim-vendor/shim/checker/shim.go +100 -0
  24. package/shim-vendor/shim/core/shim.go +47 -0
  25. package/shim-vendor/shim/diagnosticwriter/lint.go +50 -0
  26. package/src/createMemFS.ts +264 -103
  27. package/src/structures/ITtscApi.ts +8 -5
  28. package/src/structures/ITtscNodeAtPositionResult.ts +1 -1
  29. package/src/structures/ITtscNodeInfo.ts +1 -1
  30. package/src/structures/ITtscPositionQuery.ts +4 -2
  31. package/src/structures/ITtscSymbolAtPositionResult.ts +1 -1
  32. package/src/structures/ITtscTypeAtPositionResult.ts +1 -1
  33. package/src/structures/IWasmExecFS.ts +14 -4
@@ -64,6 +64,12 @@ function normalize(p: string): string {
64
64
  * `globalThis.fs` before loading `wasm_exec.js`) and convenience helpers
65
65
  * (`writeFile`, `readFile`, `mkdirp`, …) for seeding source files and reading
66
66
  * compiler output without touching the real filesystem.
67
+ *
68
+ * Every successful mutation leaves a valid tree: `/` stays a directory, each
69
+ * proper ancestor of a node exists and is a directory, and a file has no
70
+ * descendants. An operation that cannot satisfy that throws (`writeFile`,
71
+ * `mkdirp`) or reports a POSIX error through its callback, having changed no
72
+ * node, byte, or descriptor.
67
73
  */
68
74
  export function createMemFS(): IMemFSHost {
69
75
  const nodes = new Map<string, INode>();
@@ -72,19 +78,53 @@ export function createMemFS(): IMemFSHost {
72
78
  const stdout = { buffer: "" };
73
79
  const stderr = { buffer: "" };
74
80
 
75
- const fdTable = new Map<
76
- number,
77
- { path: string; position: number; isStdout?: boolean; isStderr?: boolean }
78
- >();
81
+ /**
82
+ * One open descriptor.
83
+ *
84
+ * `node` retains the object opened even after its pathname is unlinked or
85
+ * replaced. `readable`, `writable`, and `append` are the access mode and
86
+ * `O_APPEND` flag captured at `open`; without them no descriptor mutation can
87
+ * distinguish an allowed operation from a forbidden one. `position` is the
88
+ * cursor a `position: null` read or write uses and advances.
89
+ */
90
+ interface IDescriptor {
91
+ path: string;
92
+ node?: INode;
93
+ position: number;
94
+ readable: boolean;
95
+ writable: boolean;
96
+ append: boolean;
97
+ isStdout?: boolean;
98
+ isStderr?: boolean;
99
+ }
100
+ const fdTable = new Map<number, IDescriptor>();
79
101
  let nextFd = 100;
80
102
  // Reserve 1/2 for stdout/stderr writeSync routing.
81
- fdTable.set(1, { path: "/dev/stdout", position: 0, isStdout: true });
82
- fdTable.set(2, { path: "/dev/stderr", position: 0, isStderr: true });
103
+ fdTable.set(1, {
104
+ path: "/dev/stdout",
105
+ position: 0,
106
+ readable: false,
107
+ writable: true,
108
+ append: true,
109
+ isStdout: true,
110
+ });
111
+ fdTable.set(2, {
112
+ path: "/dev/stderr",
113
+ position: 0,
114
+ readable: false,
115
+ writable: true,
116
+ append: true,
117
+ isStderr: true,
118
+ });
83
119
 
84
120
  // Pipe state. fs.pipe2 mints a pair of fds backed by a shared queue;
85
121
  // writes append, reads consume. The state is keyed by fd so a single Map
86
122
  // lookup in read/write/close can detect "this is a pipe end" without
87
123
  // changing the existing fdTable entries.
124
+ //
125
+ // These fds only ever come from a direct JavaScript `fs.pipe2` call. Go's
126
+ // wasm `os.Pipe` returns ENOSYS without crossing the `globalThis.fs` bridge,
127
+ // so the Go runtime never reaches this state — see IWasmExecFS.pipe2.
88
128
  interface IPipeState {
89
129
  buffers: Uint8Array[];
90
130
  pendingReaders: Array<{
@@ -143,21 +183,62 @@ export function createMemFS(): IMemFSHost {
143
183
  }
144
184
  }
145
185
 
146
- /** Silently create any missing ancestor directories for path `p`. */
147
- function ensureParentDirs(p: string): void {
148
- const segments = normalize(p).split("/").filter(Boolean);
149
- segments.pop();
186
+ /** Root-to-leaf path of every segment of `norm` (`/a/b` `["/a", "/a/b"]`). */
187
+ function pathChain(norm: string): string[] {
188
+ const chain: string[] = [];
150
189
  let cursor = "";
151
- for (const seg of segments) {
190
+ for (const seg of norm.split("/").filter(Boolean)) {
152
191
  cursor += "/" + seg;
153
- if (!nodes.has(cursor)) {
154
- nodes.set(cursor, {
155
- kind: "dir",
156
- data: new Uint8Array(),
157
- mtimeMs: Date.now(),
158
- });
159
- }
192
+ chain.push(cursor);
160
193
  }
194
+ return chain;
195
+ }
196
+
197
+ /**
198
+ * Validate that every path in root-to-leaf `chain` is or can become a
199
+ * directory, and report the ones that do not exist yet.
200
+ *
201
+ * A segment that exists as a file makes everything below it impossible: a
202
+ * file has no descendants, so creating one there would leave a node map that
203
+ * is not a tree. Validation is deliberately separate from creation so every
204
+ * caller can reject before touching anything, and so a rejected operation
205
+ * never leaves half a directory chain behind.
206
+ */
207
+ function missingDirs(chain: string[], syscall: string): string[] {
208
+ const missing: string[] = [];
209
+ for (const path of chain) {
210
+ const existing = nodes.get(path);
211
+ if (!existing) missing.push(path);
212
+ else if (existing.kind !== "dir")
213
+ throw new MemFSError("ENOTDIR", syscall, path);
214
+ }
215
+ return missing;
216
+ }
217
+
218
+ /** Materialize the directories `missingDirs` reported. */
219
+ function createDirs(paths: string[]): void {
220
+ for (const path of paths)
221
+ nodes.set(path, {
222
+ kind: "dir",
223
+ data: new Uint8Array(),
224
+ mtimeMs: Date.now(),
225
+ });
226
+ }
227
+
228
+ /**
229
+ * Validate that normalized path `norm` may hold a regular file, returning the
230
+ * node already there when one exists.
231
+ *
232
+ * A directory is never silently replaced by a file — including the root,
233
+ * which is a directory node like any other. POSIX answers `EISDIR`, and
234
+ * overwriting the node here would strand every descendant in the map behind a
235
+ * path `readdir` can no longer walk.
236
+ */
237
+ function assertFileTarget(norm: string, syscall: string): INode | undefined {
238
+ const existing = nodes.get(norm);
239
+ if (existing && existing.kind !== "file")
240
+ throw new MemFSError("EISDIR", syscall, norm);
241
+ return existing;
161
242
  }
162
243
 
163
244
  /** Absolute parent directory of a normalized path (`/` for a top-level path). */
@@ -185,6 +266,43 @@ export function createMemFS(): IMemFSHost {
185
266
  return next;
186
267
  }
187
268
 
269
+ /**
270
+ * Write `view` through descriptor `entry` and return the bytes stored.
271
+ *
272
+ * Three offsets are possible and only one of them is right per call: an
273
+ * `O_APPEND` descriptor always writes at end-of-file, an explicit `position`
274
+ * writes exactly there without disturbing the cursor (POSIX `pwrite`), and
275
+ * `position: null` writes at the cursor and advances it. A write that starts
276
+ * past end-of-file zero-fills the gap rather than silently relocating.
277
+ *
278
+ * A zero-byte write changes nothing at all, so a cursor sitting past
279
+ * end-of-file cannot extend the file by writing nothing into it.
280
+ */
281
+ function writeThroughDescriptor(
282
+ entry: IDescriptor,
283
+ view: Uint8Array,
284
+ position: number | null,
285
+ syscall: string,
286
+ ): number {
287
+ if (!entry.writable) throw new MemFSError("EBADF", syscall, entry.path);
288
+ const node = entry.node;
289
+ if (!node) throw new MemFSError("ENOENT", syscall, entry.path);
290
+ if (node.kind !== "file")
291
+ throw new MemFSError("EISDIR", syscall, entry.path);
292
+ const start = entry.append
293
+ ? node.data.byteLength
294
+ : (position ?? entry.position);
295
+ if (!Number.isInteger(start) || start < 0)
296
+ throw new MemFSError("EINVAL", syscall, entry.path);
297
+ if (view.byteLength === 0) return 0;
298
+ const end = start + view.byteLength;
299
+ if (end > node.data.byteLength) node.data = resizeFileData(node.data, end);
300
+ node.data.set(view, start);
301
+ node.mtimeMs = Date.now();
302
+ if (position === null || entry.append) entry.position = end;
303
+ return view.byteLength;
304
+ }
305
+
188
306
  /**
189
307
  * Move the entire subtree rooted at `src` to `dest`, overwriting any existing
190
308
  * `dest` node. Every descendant key is re-parented so no old-prefix node is
@@ -214,29 +332,26 @@ export function createMemFS(): IMemFSHost {
214
332
  }
215
333
 
216
334
  function mkdirp(p: string): void {
217
- const segments = normalize(p).split("/").filter(Boolean);
218
- let cursor = "";
219
- for (const seg of segments) {
220
- cursor += "/" + seg;
221
- const existing = nodes.get(cursor);
222
- if (!existing) {
223
- nodes.set(cursor, {
224
- kind: "dir",
225
- data: new Uint8Array(),
226
- mtimeMs: Date.now(),
227
- });
228
- } else if (existing.kind !== "dir") {
229
- throw new MemFSError("ENOTDIR", "mkdir", cursor);
230
- }
231
- }
335
+ // Validate the whole chain before creating any of it: a rejected mkdirp
336
+ // must not leave the prefix it had already walked past behind.
337
+ createDirs(missingDirs(pathChain(normalize(p)), "mkdir"));
232
338
  }
233
339
 
234
340
  function writeFile(p: string, data: string | Uint8Array): void {
235
341
  const norm = normalize(p);
236
- ensureParentDirs(norm);
342
+ // Resolve left to right the way POSIX does: an impossible ancestor is
343
+ // reported before the target, and both are checked before any mutation.
344
+ const missing = missingDirs(pathChain(norm).slice(0, -1), "open");
345
+ const existing = assertFileTarget(norm, "open");
237
346
  const bytes =
238
347
  typeof data === "string" ? encoder.encode(data) : new Uint8Array(data);
239
- nodes.set(norm, { kind: "file", data: bytes, mtimeMs: Date.now() });
348
+ createDirs(missing);
349
+ // Overwriting keeps the same node so descriptors already pointing at this
350
+ // file observe the replacement instead of a detached predecessor.
351
+ if (existing) {
352
+ existing.data = bytes;
353
+ existing.mtimeMs = Date.now();
354
+ } else nodes.set(norm, { kind: "file", data: bytes, mtimeMs: Date.now() });
240
355
  }
241
356
 
242
357
  function readFile(p: string): Uint8Array | null {
@@ -334,40 +449,19 @@ export function createMemFS(): IMemFSHost {
334
449
  console.error("[wasm]", line);
335
450
  return buf.length;
336
451
  }
337
- // Open-file fds (>= 100): append to the underlying file. Browser-hosted
338
- // tools use this path for ordinary virtual files and explicit outputs.
452
+ // Open-file fds (>= 100): write at the descriptor cursor, the way Node's
453
+ // own `writeSync` without a position does. Browser-hosted tools use this
454
+ // path for ordinary virtual files and explicit outputs.
455
+ //
456
+ // An unknown or already-closed fd throws instead of being diverted into
457
+ // the captured stderr: reporting the byte count of a write nobody
458
+ // performed made the caller continue on a false success and quietly
459
+ // contaminated a diagnostic channel consumers read.
339
460
  const entry = fdTable.get(fd);
340
- if (entry) {
341
- const node = nodes.get(entry.path);
342
- if (node && node.kind === "file") {
343
- // subarray(0) is a zero-copy view over the full incoming buffer; the
344
- // bytes are copied into `next` before writeSync returns.
345
- const incoming = buf.subarray(0);
346
- const existing = node.data;
347
- const next = new Uint8Array(
348
- existing.byteLength + incoming.byteLength,
349
- );
350
- next.set(existing, 0);
351
- next.set(incoming, existing.byteLength);
352
- node.data = next;
353
- node.mtimeMs = Date.now();
354
- entry.position = next.byteLength;
355
- return incoming.byteLength;
356
- }
357
- }
358
- // Unknown fd. Fall back to the stderr buffer so the bytes aren't lost
359
- // entirely (and surface as a console.error so the regression is
360
- // visible to whoever's looking).
361
- stderr.buffer += decoder.decode(buf);
362
- // eslint-disable-next-line no-console
363
- console.error(
364
- "[wasm] writeSync to unknown fd " +
365
- fd +
366
- " (" +
367
- buf.byteLength +
368
- " bytes); routed to stderr buffer",
369
- );
370
- return buf.length;
461
+ if (!entry) throw new MemFSError("EBADF", "write");
462
+ // subarray(0) is a zero-copy view over the full incoming buffer; the
463
+ // bytes are copied into the node before writeSync returns.
464
+ return writeThroughDescriptor(entry, buf.subarray(0), null, "write");
371
465
  },
372
466
 
373
467
  write(fd, buf, offset, length, position, callback) {
@@ -390,44 +484,83 @@ export function createMemFS(): IMemFSHost {
390
484
  callback(null, length);
391
485
  return;
392
486
  }
393
- if (position !== null && position !== 0) {
394
- callback(new MemFSError("ESPIPE", "write"), 0);
487
+ const view = buf.subarray(offset, offset + length);
488
+ // The stdout/stderr capture buffers are streams, not files: they have
489
+ // no seekable offset, so an explicit position is meaningless on them.
490
+ if (fd === 1 || fd === 2) {
491
+ if (position !== null && position !== 0)
492
+ throw new MemFSError("ESPIPE", "write");
493
+ callback(null, this.writeSync(fd, view));
395
494
  return;
396
495
  }
397
- const view = buf.subarray(offset, offset + length);
398
- const written = this.writeSync(fd, view);
399
- callback(null, written);
496
+ const entry = fdTable.get(fd);
497
+ if (!entry) throw new MemFSError("EBADF", "write");
498
+ callback(null, writeThroughDescriptor(entry, view, position, "write"));
400
499
  } catch (err) {
401
500
  callback(err as NodeJS.ErrnoException, 0);
402
501
  }
403
502
  },
404
503
 
504
+ // Every rejection happens before the first mutation and before a
505
+ // descriptor is minted, so a refused open leaves neither a half-created
506
+ // node nor a leaked fd behind.
405
507
  open(p, flags, _mode, callback) {
406
- const norm = normalize(p);
407
- const node = nodes.get(norm);
408
- const creating = (flags & (this.constants.O_CREAT ?? 0)) !== 0;
409
- if (!node) {
410
- if (!creating) {
411
- callback(new MemFSError("ENOENT", "open", norm), -1);
412
- return;
508
+ try {
509
+ const norm = normalize(p);
510
+ const creating = (flags & (this.constants.O_CREAT ?? 0)) !== 0;
511
+ const exclusive = (flags & (this.constants.O_EXCL ?? 0)) !== 0;
512
+ const truncating = (flags & (this.constants.O_TRUNC ?? 0)) !== 0;
513
+ const appending = (flags & (this.constants.O_APPEND ?? 0)) !== 0;
514
+ const directoryOnly = (flags & (this.constants.O_DIRECTORY ?? 0)) !== 0;
515
+ // The access mode is the low bits of `flags`: absent both means
516
+ // read-only, and `O_RDWR` alongside `O_WRONLY` still grants both.
517
+ const writeOnly = (flags & (this.constants.O_WRONLY ?? 0)) !== 0;
518
+ const readWrite = (flags & (this.constants.O_RDWR ?? 0)) !== 0;
519
+ const writable = writeOnly || readWrite;
520
+ const readable = !writeOnly || readWrite;
521
+
522
+ let node = nodes.get(norm);
523
+ if (!node) {
524
+ if (!creating) throw new MemFSError("ENOENT", "open", norm);
525
+ // `open` can only ever create a regular file, so a caller demanding a
526
+ // directory cannot be satisfied by creating one.
527
+ if (directoryOnly) throw new MemFSError("ENOTDIR", "open", norm);
528
+ // Creating the missing ancestor chain is the documented job of
529
+ // `writeFile` and `mkdirp`; the low-level `open` never promised it.
530
+ const missing = missingDirs(pathChain(norm).slice(0, -1), "open");
531
+ if (missing.length > 0)
532
+ throw new MemFSError("ENOENT", "open", missing[0]!);
533
+ node = { kind: "file", data: new Uint8Array(), mtimeMs: Date.now() };
534
+ nodes.set(norm, node);
535
+ } else {
536
+ if (creating && exclusive)
537
+ throw new MemFSError("EEXIST", "open", norm);
538
+ if (node.kind === "dir") {
539
+ // A directory opens read-only — Go stats the fd and lists the path
540
+ // that way. Writing to it or truncating it would turn it into a
541
+ // file and orphan every descendant.
542
+ if (writable || truncating)
543
+ throw new MemFSError("EISDIR", "open", norm);
544
+ } else if (directoryOnly)
545
+ throw new MemFSError("ENOTDIR", "open", norm);
413
546
  }
414
- ensureParentDirs(norm);
415
- nodes.set(norm, {
416
- kind: "file",
417
- data: new Uint8Array(),
418
- mtimeMs: Date.now(),
419
- });
420
- }
421
- const fd = nextFd++;
422
- fdTable.set(fd, { path: norm, position: 0 });
423
- if ((flags & (this.constants.O_TRUNC ?? 0)) !== 0) {
424
- nodes.set(norm, {
425
- kind: "file",
426
- data: new Uint8Array(),
427
- mtimeMs: Date.now(),
547
+ if (truncating) {
548
+ node.data = new Uint8Array();
549
+ node.mtimeMs = Date.now();
550
+ }
551
+ const fd = nextFd++;
552
+ fdTable.set(fd, {
553
+ path: norm,
554
+ node,
555
+ position: 0,
556
+ readable,
557
+ writable,
558
+ append: appending,
428
559
  });
560
+ callback(null, fd);
561
+ } catch (err) {
562
+ callback(err as NodeJS.ErrnoException, -1);
429
563
  }
430
- callback(null, fd);
431
564
  },
432
565
 
433
566
  close(fd, callback) {
@@ -475,16 +608,29 @@ export function createMemFS(): IMemFSHost {
475
608
  callback(new MemFSError("EBADF", "read"), 0);
476
609
  return;
477
610
  }
478
- const node = nodes.get(entry.path);
611
+ // A write-only descriptor is not a readable one. POSIX answers EBADF for
612
+ // an operation the descriptor's access mode never granted.
613
+ if (!entry.readable) {
614
+ callback(new MemFSError("EBADF", "read", entry.path), 0);
615
+ return;
616
+ }
617
+ const node = entry.node;
479
618
  if (!node || node.kind !== "file") {
480
619
  callback(new MemFSError("ENOENT", "read", entry.path), 0);
481
620
  return;
482
621
  }
483
622
  const start = position ?? entry.position;
623
+ if (!Number.isInteger(start) || start < 0) {
624
+ callback(new MemFSError("EINVAL", "read", entry.path), 0);
625
+ return;
626
+ }
484
627
  const end = Math.min(start + length, node.data.byteLength);
485
628
  const slice = node.data.subarray(start, end);
486
629
  buffer.set(slice, offset);
487
- if (position === null) entry.position = end;
630
+ // The cursor advances by the bytes actually read. Assigning `end` would
631
+ // rewind a cursor already past end-of-file (after `ftruncate`, say) back
632
+ // onto live bytes and make the next sequential write overwrite them.
633
+ if (position === null) entry.position = start + slice.byteLength;
488
634
  callback(null, slice.byteLength);
489
635
  },
490
636
 
@@ -498,7 +644,17 @@ export function createMemFS(): IMemFSHost {
498
644
 
499
645
  mkdir(p, _perm, callback) {
500
646
  try {
501
- mkdirp(p);
647
+ const norm = normalize(p);
648
+ if (nodes.has(norm)) throw new MemFSError("EEXIST", "mkdir", norm);
649
+ const parent = nodes.get(parentDir(norm));
650
+ if (!parent) throw new MemFSError("ENOENT", "mkdir", parentDir(norm));
651
+ if (parent.kind !== "dir")
652
+ throw new MemFSError("ENOTDIR", "mkdir", parentDir(norm));
653
+ nodes.set(norm, {
654
+ kind: "dir",
655
+ data: new Uint8Array(),
656
+ mtimeMs: Date.now(),
657
+ });
502
658
  callback(null);
503
659
  } catch (err) {
504
660
  callback(err as NodeJS.ErrnoException);
@@ -521,9 +677,9 @@ export function createMemFS(): IMemFSHost {
521
677
  },
522
678
 
523
679
  fstat(fd, callback) {
524
- // fstat against a pipe end returns a synthetic file-stat. Go's
525
- // os.Pipe-backed File uses fstat at construction time to populate
526
- // Stat_t; without this it errors and falls back to invalid fds.
680
+ // fstat against a pipe end returns a synthetic file-stat so a direct
681
+ // JavaScript caller that wraps the fd in a file-like abstraction can
682
+ // populate its stat fields. A pipe end has no node in the tree.
527
683
  if (pipes.has(fd)) {
528
684
  callback(
529
685
  null,
@@ -544,7 +700,8 @@ export function createMemFS(): IMemFSHost {
544
700
  return;
545
701
  }
546
702
  try {
547
- callback(null, statSync(entry.path));
703
+ if (!entry.node) throw new MemFSError("ENOENT", "fstat", entry.path);
704
+ callback(null, makeStats(entry.node));
548
705
  } catch (err) {
549
706
  callback(
550
707
  err as NodeJS.ErrnoException,
@@ -713,7 +870,11 @@ export function createMemFS(): IMemFSHost {
713
870
  callback(new MemFSError("EINVAL", "ftruncate"));
714
871
  return;
715
872
  }
716
- const node = nodes.get(entry.path);
873
+ if (!entry.writable) {
874
+ callback(new MemFSError("EBADF", "ftruncate", entry.path));
875
+ return;
876
+ }
877
+ const node = entry.node;
717
878
  if (!node || node.kind !== "file") {
718
879
  callback(new MemFSError("EINVAL", "ftruncate", entry.path));
719
880
  return;
@@ -83,18 +83,21 @@ export interface ITtscApi {
83
83
  opts: ITtscSnapshotHandle & { file?: string },
84
84
  ): Promise<ITtscResult>;
85
85
 
86
- /** Return the deepest AST node whose range covers `position` (byte offset). */
86
+ /**
87
+ * Return the syntax token touching `position` (byte offset), including
88
+ * punctuation. Returns `{node: null}` for whitespace and comments.
89
+ */
87
90
  getNodeAtPosition(opts: ITtscPositionQuery): Promise<ITtscResult>;
88
91
 
89
92
  /**
90
- * Run the TypeScript-Go type checker against the node at `position` and
91
- * return its printed type string + flags. Returns `{type: null}` when there
92
- * is no node at that position.
93
+ * Resolve the token at `position` and return the enclosing TypeScript-Go
94
+ * semantic node's printed type string + flags. Returns `{type: null}` when
95
+ * the position has no token or no type-bearing token.
93
96
  */
94
97
  getTypeAtPosition(opts: ITtscPositionQuery): Promise<ITtscResult>;
95
98
 
96
99
  /**
97
- * Resolve the symbol the node at `position` refers to, including up to 16
100
+ * Resolve the symbol the token at `position` refers to, including up to 16
98
101
  * declaration sites. Returns `{symbol: null}` when no symbol is bound.
99
102
  */
100
103
  getSymbolAtPosition(opts: ITtscPositionQuery): Promise<ITtscResult>;
@@ -2,6 +2,6 @@ import type { ITtscNodeInfo } from "./ITtscNodeInfo";
2
2
 
3
3
  /** Payload inside `ITtscResult.result` for `getNodeAtPosition`. */
4
4
  export interface ITtscNodeAtPositionResult {
5
- /** `null` when no node covers the position. */
5
+ /** `null` when no syntax token touches the position. */
6
6
  node: ITtscNodeInfo | null;
7
7
  }
@@ -1,4 +1,4 @@
1
- /** AST node shape returned by `getNodeAtPosition`. */
1
+ /** Syntax-token shape returned by `getNodeAtPosition`. */
2
2
  export interface ITtscNodeInfo {
3
3
  /** Numeric `ast.Kind` from TypeScript-Go. */
4
4
  kind: number;
@@ -6,8 +6,10 @@ import type { ITtscFileQuery } from "./ITtscFileQuery";
6
6
  */
7
7
  export interface ITtscPositionQuery extends ITtscFileQuery {
8
8
  /**
9
- * Byte offset into the file's source text. JS callers with a UTF-16
10
- * line/character pair must resolve it to a byte offset first.
9
+ * Byte offset into the file's source text. It must satisfy `0 <= position <
10
+ * sourceText's UTF-8 byte length`; the offset immediately after the final
11
+ * byte is out of range. JS callers with a UTF-16 line/character pair must
12
+ * resolve it to a byte offset first.
11
13
  */
12
14
  position: number;
13
15
  }
@@ -2,6 +2,6 @@ import type { ITtscSymbolInfo } from "./ITtscSymbolInfo";
2
2
 
3
3
  /** Payload inside `ITtscResult.result` for `getSymbolAtPosition`. */
4
4
  export interface ITtscSymbolAtPositionResult {
5
- /** `null` when the node has no associated symbol. */
5
+ /** `null` when no touching token has an associated symbol. */
6
6
  symbol: ITtscSymbolInfo | null;
7
7
  }
@@ -2,6 +2,6 @@ import type { ITtscTypeInfo } from "./ITtscTypeInfo";
2
2
 
3
3
  /** Payload inside `ITtscResult.result` for `getTypeAtPosition`. */
4
4
  export interface ITtscTypeAtPositionResult {
5
- /** `null` when the node has no associated type. */
5
+ /** `null` when no touching token has a type. */
6
6
  type: ITtscTypeInfo | null;
7
7
  }
@@ -135,10 +135,20 @@ export interface IWasmExecFS {
135
135
  callback: (err: NodeJS.ErrnoException | null) => void,
136
136
  ): void;
137
137
  /**
138
- * `pipe2` is what Go's wasm `os.Pipe()` calls. Returns two fds: a read end
139
- * and a write end. The host's stdout/stderr capture currently uses MemFS temp
140
- * files, but this keeps direct pipe callers compatible with the wasm fs
141
- * surface.
138
+ * JavaScript-only pipe emulation. Returns two fds: a read end and a write
139
+ * end.
140
+ *
141
+ * Installing this does **not** make Go's wasm `os.Pipe()` work. Under the
142
+ * pinned Go toolchain neither `GOOS=js` nor `GOOS=wasip1` has pipes:
143
+ * `os.Pipe` returns an `ENOSYS` syscall error and `syscall.Pipe` returns
144
+ * `ENOSYS`, and neither one crosses the `globalThis.fs` bridge, so no
145
+ * JavaScript method is ever consulted. A custom Go/wasm host cannot reach
146
+ * this shim.
147
+ *
148
+ * The host captures wasm output without pipes: process-level writes to fd 1
149
+ * and fd 2 land in `stdout.buffer` and `stderr.buffer` through `writeSync`,
150
+ * and a plugin's output is collected by the Go-side `host.InvokePlugin` into
151
+ * invocation-owned in-process buffers.
142
152
  */
143
153
  pipe2(
144
154
  flags: number,