functionalscript 0.17.0 → 0.18.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 (59) hide show
  1. package/fs/ci/common/module.f.d.ts +4 -5
  2. package/fs/ci/common/module.f.js +4 -4
  3. package/fs/ci/config/module.f.d.ts +4 -4
  4. package/fs/ci/config/module.f.js +4 -4
  5. package/fs/ci/test.f.js +2 -4
  6. package/fs/dev/module.f.d.ts +3 -3
  7. package/fs/dev/module.f.js +12 -9
  8. package/fs/dev/tf/module.d.ts +1 -0
  9. package/fs/dev/tf/module.f.d.ts +70 -6
  10. package/fs/dev/tf/module.f.js +135 -87
  11. package/fs/dev/tf/module.js +66 -17
  12. package/fs/dev/tf/test.f.d.ts +21 -20
  13. package/fs/dev/tf/test.f.js +249 -31
  14. package/fs/djs/module.f.d.ts +2 -2
  15. package/fs/djs/module.f.js +8 -5
  16. package/fs/djs/test.f.js +5 -6
  17. package/fs/djs/tokenizer-new/test.f.js +126 -78
  18. package/fs/djs/transpiler/module.f.js +2 -2
  19. package/fs/djs/transpiler/test.f.js +11 -12
  20. package/fs/fjs/module.f.d.ts +2 -7
  21. package/fs/fjs/module.f.js +16 -22
  22. package/fs/fjs/module.js +2 -2
  23. package/fs/io/module.d.ts +3 -3
  24. package/fs/io/module.f.d.ts +5 -2
  25. package/fs/io/module.f.js +14 -3
  26. package/fs/io/module.js +38 -17
  27. package/fs/path/module.f.d.ts +6 -0
  28. package/fs/path/module.f.js +6 -0
  29. package/fs/path/test.f.d.ts +3 -5
  30. package/fs/path/test.f.js +67 -49
  31. package/fs/text/sgr/module.f.d.ts +9 -1
  32. package/fs/text/sgr/module.f.js +16 -5
  33. package/fs/types/effects/node/module.f.d.ts +17 -17
  34. package/fs/types/effects/node/module.f.js +20 -2
  35. package/fs/types/effects/node/test.f.js +8 -5
  36. package/fs/types/effects/node/virtual/module.f.d.ts +11 -2
  37. package/fs/types/effects/node/virtual/module.f.js +30 -17
  38. package/fs/types/function/compare/module.f.d.ts +12 -0
  39. package/fs/types/function/compare/module.f.js +33 -0
  40. package/fs/types/function/operator/test.f.d.ts +10 -0
  41. package/fs/types/function/operator/test.f.js +81 -0
  42. package/fs/types/range_map/module.f.js +3 -18
  43. package/fs/types/result/module.f.d.ts +4 -0
  44. package/fs/types/result/module.f.js +4 -0
  45. package/fs/types/result/test.f.d.ts +2 -4
  46. package/fs/types/result/test.f.js +24 -16
  47. package/fs/types/rtti/common/module.f.d.ts +10 -1
  48. package/fs/types/rtti/common/module.f.js +7 -2
  49. package/fs/types/rtti/parse/module.f.js +35 -46
  50. package/fs/types/rtti/validate/module.f.js +9 -12
  51. package/fs/types/sorted_list/module.f.d.ts +1 -2
  52. package/fs/types/sorted_list/module.f.js +8 -21
  53. package/fs/types/sorted_set/module.f.d.ts +1 -3
  54. package/fs/types/ts/test.f.d.ts +18 -0
  55. package/fs/types/ts/test.f.js +111 -0
  56. package/fs/types/uint8array/module.f.js +7 -1
  57. package/fs/types/uint8array/test.f.d.ts +1 -0
  58. package/fs/types/uint8array/test.f.js +5 -1
  59. package/package.json +1 -1
package/fs/io/module.js CHANGED
@@ -10,12 +10,46 @@ import http from 'node:http';
10
10
  import childProcess from 'node:child_process';
11
11
  import fs from 'node:fs';
12
12
  import process from 'node:process';
13
- import { fromIo, run } from "./module.f.js";
14
13
  import { concat } from "../path/module.f.js";
14
+ import { once } from 'node:events';
15
+ import { fromIo, runProgram } from "./module.f.js";
15
16
  import { error, ok } from "../types/result/module.f.js";
17
+ import { fromVec } from "../types/uint8array/module.f.js";
16
18
  const prefix = 'file:///';
17
19
  const { now } = Date;
20
+ /** Maps `WriteConsoles` names to the corresponding Node.js writable streams. */
21
+ const streams = {
22
+ stdout: process.stdout,
23
+ stderr: process.stderr,
24
+ };
25
+ /**
26
+ * Writes `data` to `stream` respecting Node.js backpressure.
27
+ *
28
+ * `stream.write()` returns `false` when the internal buffer is full; the data
29
+ * is already buffered at that point (no retry needed) but the caller must not
30
+ * issue more writes until the `'drain'` event fires. Waiting here throttles the
31
+ * producer to the speed of the OS consumer, preventing unbounded memory growth
32
+ * when many large messages arrive faster than they can be flushed.
33
+ *
34
+ * When the buffer is not full `write()` returns `true` and we return
35
+ * immediately, so large computations with occasional prints never stall.
36
+ *
37
+ * @see {@link https://nodejs.org/api/stream.html#writablewritechunk}
38
+ */
39
+ const writeAll = async (stream, data) => {
40
+ if (!stream.write(data)) {
41
+ await once(stream, 'drain');
42
+ }
43
+ };
18
44
  export const asyncImport = (v) => import(__rewriteRelativeImportExtension(v));
45
+ export const tryCatch = f => {
46
+ try {
47
+ return ok(f());
48
+ }
49
+ catch (e) {
50
+ return error(e);
51
+ }
52
+ };
19
53
  export const io = {
20
54
  console,
21
55
  fs,
@@ -27,14 +61,7 @@ export const io = {
27
61
  },
28
62
  performance,
29
63
  fetch,
30
- tryCatch: f => {
31
- try {
32
- return ok(f());
33
- }
34
- catch (e) {
35
- return error(e);
36
- }
37
- },
64
+ tryCatch,
38
65
  asyncTryCatch: async (f) => {
39
66
  try {
40
67
  return ok(await f());
@@ -61,13 +88,7 @@ export const io = {
61
88
  }
62
89
  return { result, duration: after - before };
63
90
  },
91
+ write: (stream, data) => writeAll(streams[stream], fromVec(data)),
64
92
  };
65
- export const legacyRun = run(io);
66
- export const ioRun = (io) => {
67
- const r = fromIo(io);
68
- const { argv, env } = io.process;
69
- const options = { args: argv.slice(2), env };
70
- return p => r(p(options));
71
- };
72
- const effectRun = ioRun(io);
93
+ const effectRun = runProgram(io)(io.process.argv.slice(2));
73
94
  export default effectRun;
@@ -20,3 +20,9 @@ export declare const normalize: Unary<string, string>;
20
20
  * Concatenates two path fragments and returns a normalized path.
21
21
  */
22
22
  export declare const concat: Reduce<string>;
23
+ /**
24
+ * Returns `path` relative to `base` with a `./` prefix, or `path` unchanged
25
+ * if it does not start with `base` or `base` is empty.
26
+ * E.g. `relativize('/repo', '/repo/fs/a.ts')` → `'./fs/a.ts'`.
27
+ */
28
+ export declare const relativize: (base: string, path: string) => string;
@@ -46,3 +46,9 @@ export const concat = a => b => {
46
46
  const s = stringConcat([a, '/', b]);
47
47
  return normalize(s);
48
48
  };
49
+ /**
50
+ * Returns `path` relative to `base` with a `./` prefix, or `path` unchanged
51
+ * if it does not start with `base` or `base` is empty.
52
+ * E.g. `relativize('/repo', '/repo/fs/a.ts')` → `'./fs/a.ts'`.
53
+ */
54
+ export const relativize = (base, path) => base !== '' && path.startsWith(base) ? `.${path.slice(base.length)}` : path;
@@ -1,5 +1,3 @@
1
- declare const _default: {
2
- normalize: (() => void)[];
3
- concat: (() => void)[];
4
- };
5
- export default _default;
1
+ export declare const normalizeTest: (() => void)[];
2
+ export declare const concatTest: (() => void)[];
3
+ export declare const relativizeTest: (() => void)[];
package/fs/path/test.f.js CHANGED
@@ -1,49 +1,67 @@
1
- import { concat, normalize } from "./module.f.js";
2
- export default {
3
- normalize: [
4
- () => {
5
- const norm = normalize("dir/file.json");
6
- if (norm !== "dir/file.json") {
7
- throw norm;
8
- }
9
- },
10
- () => {
11
- const norm = normalize("dir//file.json");
12
- if (norm !== "dir/file.json") {
13
- throw norm;
14
- }
15
- },
16
- () => {
17
- const norm = normalize("../../dir/file.json");
18
- if (norm !== "../../dir/file.json") {
19
- throw norm;
20
- }
21
- },
22
- () => {
23
- const norm = normalize("../../dir/../file.json");
24
- if (norm !== "../../file.json") {
25
- throw norm;
26
- }
27
- },
28
- ],
29
- concat: [
30
- () => {
31
- const c = concat("a")("b");
32
- if (c !== "a/b") {
33
- throw c;
34
- }
35
- },
36
- () => {
37
- const c = concat("a///b/")("c");
38
- if (c !== "a/b/c") {
39
- throw c;
40
- }
41
- },
42
- () => {
43
- const c = concat("a/../b/..")("c");
44
- if (c !== "c") {
45
- throw c;
46
- }
47
- },
48
- ]
49
- };
1
+ import { concat, normalize, relativize } from "./module.f.js";
2
+ export const normalizeTest = [
3
+ () => {
4
+ const norm = normalize("dir/file.json");
5
+ if (norm !== "dir/file.json") {
6
+ throw norm;
7
+ }
8
+ },
9
+ () => {
10
+ const norm = normalize("dir//file.json");
11
+ if (norm !== "dir/file.json") {
12
+ throw norm;
13
+ }
14
+ },
15
+ () => {
16
+ const norm = normalize("../../dir/file.json");
17
+ if (norm !== "../../dir/file.json") {
18
+ throw norm;
19
+ }
20
+ },
21
+ () => {
22
+ const norm = normalize("../../dir/../file.json");
23
+ if (norm !== "../../file.json") {
24
+ throw norm;
25
+ }
26
+ },
27
+ ];
28
+ export const concatTest = [
29
+ () => {
30
+ const c = concat("a")("b");
31
+ if (c !== "a/b") {
32
+ throw c;
33
+ }
34
+ },
35
+ () => {
36
+ const c = concat("a///b/")("c");
37
+ if (c !== "a/b/c") {
38
+ throw c;
39
+ }
40
+ },
41
+ () => {
42
+ const c = concat("a/../b/..")("c");
43
+ if (c !== "c") {
44
+ throw c;
45
+ }
46
+ },
47
+ ];
48
+ export const relativizeTest = [
49
+ () => {
50
+ const r = relativize('/repo', '/repo/fs/a.ts');
51
+ if (r !== './fs/a.ts') {
52
+ throw r;
53
+ }
54
+ },
55
+ () => {
56
+ const r = relativize('/repo', '/other/a.ts');
57
+ if (r !== '/other/a.ts') {
58
+ throw r;
59
+ }
60
+ },
61
+ () => {
62
+ const r = relativize('', './fs/a.ts');
63
+ if (r !== './fs/a.ts') {
64
+ throw r;
65
+ }
66
+ },
67
+ ];
@@ -5,6 +5,8 @@
5
5
  * @module
6
6
  */
7
7
  import type { Io, Writable } from "../../io/module.f.ts";
8
+ import { type Write, type WriteConsoles, type NodeProgramOptions } from '../../types/effects/node/module.f.ts';
9
+ import { type Effect } from '../../types/effects/module.f.ts';
8
10
  export declare const backspace: string;
9
11
  type End = 'm';
10
12
  type Csi = (code: number | string) => string;
@@ -44,7 +46,7 @@ export type WriteText = (text: string) => WriteText;
44
46
  export declare const createConsoleText: (stdout: Stdout) => WriteText;
45
47
  export type CsiConsole = (s: string) => void;
46
48
  /**
47
- * Creates a TTY-aware console function.
49
+ * Creates a TTY-aware console function. Appends `\n` to each string before writing.
48
50
  *
49
51
  * For TTY destinations, ANSI SGR sequences are preserved.
50
52
  * For non-TTY destinations, ANSI SGR sequences are stripped.
@@ -57,4 +59,10 @@ export declare const console: ({ fs: { writeSync } }: Io) => (w: Writable) => Cs
57
59
  export declare const stdio: (io: Io) => CsiConsole;
58
60
  /** Writes to process stderr using a TTY-aware CSI console. */
59
61
  export declare const stderr: (io: Io) => CsiConsole;
62
+ /**
63
+ * Effect-based TTY-aware write. Strips ANSI SGR sequences when the target
64
+ * stream is not a TTY, then encodes to UTF-8 and emits a `Write` effect.
65
+ * Does NOT append `\n` — callers are responsible for line termination.
66
+ */
67
+ export declare const csiWrite: ({ std }: NodeProgramOptions) => (stream: WriteConsoles) => (s: string) => Effect<Write, void>;
60
68
  export {};
@@ -4,6 +4,9 @@
4
4
  *
5
5
  * @module
6
6
  */
7
+ import { write } from "../../types/effects/node/module.f.js";
8
+ import {} from "../../types/effects/module.f.js";
9
+ import { utf8 } from "../module.f.js";
7
10
  export const backspace = '\x08';
8
11
  const begin = '\x1b[';
9
12
  /**
@@ -46,8 +49,9 @@ export const createConsoleText = (stdout) => {
46
49
  };
47
50
  return f('');
48
51
  };
52
+ const str = (isTTY) => (s) => isTTY ? s : s.replace(/\x1b\[[0-9;]*m/g, '');
49
53
  /**
50
- * Creates a TTY-aware console function.
54
+ * Creates a TTY-aware console function. Appends `\n` to each string before writing.
51
55
  *
52
56
  * For TTY destinations, ANSI SGR sequences are preserved.
53
57
  * For non-TTY destinations, ANSI SGR sequences are stripped.
@@ -56,12 +60,19 @@ export const createConsoleText = (stdout) => {
56
60
  * @returns A function that targets a writable stream.
57
61
  */
58
62
  export const console = ({ fs: { writeSync } }) => (w) => {
59
- const { isTTY } = w;
60
- return isTTY
61
- ? (s) => writeSync(w.fd, s + '\n')
62
- : (s) => writeSync(w.fd, s.replace(/\x1b\[[0-9;]*m/g, '') + '\n');
63
+ const toStr = str(w.isTTY);
64
+ return (s) => writeSync(w.fd, toStr(s) + '\n');
63
65
  };
64
66
  /** Writes to process stdout using a TTY-aware CSI console. */
65
67
  export const stdio = (io) => console(io)(io.process.stdout);
66
68
  /** Writes to process stderr using a TTY-aware CSI console. */
67
69
  export const stderr = (io) => console(io)(io.process.stderr);
70
+ /**
71
+ * Effect-based TTY-aware write. Strips ANSI SGR sequences when the target
72
+ * stream is not a TTY, then encodes to UTF-8 and emits a `Write` effect.
73
+ * Does NOT append `\n` — callers are responsible for line termination.
74
+ */
75
+ export const csiWrite = ({ std }) => (stream) => {
76
+ const toStr = str(std[stream].isTTY);
77
+ return (s) => write(stream, utf8(toStr(s)));
78
+ };
@@ -1,12 +1,3 @@
1
- /**
2
- * Node.js effect operations: filesystem (`mkdir`, `readFile`, `readdir`,
3
- * `writeFile`, `rm`, `access`), networking (`fetch`, `createServer`, `listen`),
4
- * subprocess `exec`, console (`log`, `error`), `import_`, `now`, `sandbox`, `forever`,
5
- * and `all`/`both` parallelism; defines the `NodeOp`/`NodeProgram` types used
6
- * by the Node runner.
7
- *
8
- * @module
9
- */
10
1
  import type { Vec } from '../../bit_vec/module.f.ts';
11
2
  import type { Nominal } from '../../nominal/module.f.ts';
12
3
  import type { Result } from '../../result/module.f.ts';
@@ -58,11 +49,6 @@ export declare const exec: Func<Exec>;
58
49
  export type Access = readonly ['access', (path: string) => IoResult<void>];
59
50
  export declare const access: Func<Access>;
60
51
  export type Fs = Mkdir | ReadFile | Readdir | WriteFile | Rm | Exec | Access;
61
- export type Error = ['error', (message: string) => void];
62
- export declare const error: Func<Error>;
63
- export type Log = ['log', (message: string) => void];
64
- export declare const log: Func<Log>;
65
- export type Console = Log | Error;
66
52
  export type Server = Nominal<'server', `160855c4f69310fece3273c1853ac32de43dee1eb41bf59d821917f8eebe9272`, unknown>;
67
53
  export type Headers = {
68
54
  readonly [k in string]: string;
@@ -87,10 +73,18 @@ export type Http = CreateServer | Listen;
87
73
  export type Forever = ['forever', () => never];
88
74
  export declare const forever: Func<Forever>;
89
75
  export type Module = {
90
- readonly default: unknown;
76
+ readonly [k in string]: unknown;
91
77
  };
92
78
  export type Import = ['import', (path: string) => IoResult<Module>];
93
79
  export declare const import_: Func<Import>;
80
+ export type WriteConsoles = 'stdout' | 'stderr';
81
+ export type Write = readonly ['write', (stream: WriteConsoles, data: Vec) => void];
82
+ export declare const write: Func<Write>;
83
+ export type Console = (s: string) => Effect<Write, void>;
84
+ /** Writes a line to `stdout`. Replaces the retired `Log` effect. */
85
+ export declare const log: Console;
86
+ /** Writes a line to `stderr`. Replaces the retired `Error` effect. */
87
+ export declare const error: Console;
94
88
  export type Now = readonly ['now', () => number];
95
89
  export declare const now: Func<Now>;
96
90
  export type SandboxResult<T> = {
@@ -122,7 +116,7 @@ export type Sandbox = readonly ['sandbox', <T>(f: () => T) => SandboxResult<T>];
122
116
  * @see {@link SandboxResult}
123
117
  */
124
118
  export declare const sandbox: Func<Sandbox>;
125
- export type NodeOp = All | Fetch | Console | Fs | Http | Forever | Import | Now | Sandbox;
119
+ export type NodeOp = All | Fetch | Fs | Http | Forever | Import | Now | Sandbox | Write;
126
120
  export type NodeEffect<T> = Effect<NodeOp, T>;
127
121
  export type NodeOperationMap = ToAsyncOperationMap<NodeOp>;
128
122
  /**
@@ -134,5 +128,11 @@ export type Env = {
134
128
  export type NodeProgramOptions = {
135
129
  readonly args: readonly string[];
136
130
  readonly env: Env;
131
+ readonly std: {
132
+ readonly [k in WriteConsoles]: {
133
+ readonly isTTY: boolean;
134
+ };
135
+ };
137
136
  };
138
- export type NodeProgram = (options: NodeProgramOptions) => Effect<NodeOp, number>;
137
+ export type Program<O extends Operation> = (options: NodeProgramOptions) => Effect<O, number>;
138
+ export type NodeProgram = Program<NodeOp>;
@@ -1,3 +1,13 @@
1
+ /**
2
+ * Node.js effect operations: filesystem (`mkdir`, `readFile`, `readdir`,
3
+ * `writeFile`, `rm`, `access`), networking (`fetch`, `createServer`, `listen`),
4
+ * subprocess `exec`, `log`/`error` (wrappers over `write`), `import_`, `now`,
5
+ * `sandbox`, `forever`, and `all`/`both` parallelism; defines the
6
+ * `NodeOp`/`NodeProgram` types used by the Node runner.
7
+ *
8
+ * @module
9
+ */
10
+ import { utf8 } from "../../../text/module.f.js";
1
11
  import { do_ } from "../module.f.js";
2
12
  const doAll = do_('all');
3
13
  /**
@@ -17,12 +27,20 @@ export const writeFile = do_('writeFile');
17
27
  export const rm = do_('rm');
18
28
  export const exec = do_('exec');
19
29
  export const access = do_('access');
20
- export const error = do_('error');
21
- export const log = do_('log');
22
30
  export const createServer = do_('createServer');
23
31
  export const listen = do_('listen');
24
32
  export const forever = do_('forever');
25
33
  export const import_ = do_('import');
34
+ export const write = do_('write');
35
+ /**
36
+ * Encodes `s + '\n'` as UTF-8 and emits a `Write` effect to `stream`.
37
+ * Shared implementation for `log` and `error`.
38
+ */
39
+ const writeString = (stream) => (s) => write(stream, utf8(s + '\n'));
40
+ /** Writes a line to `stdout`. Replaces the retired `Log` effect. */
41
+ export const log = writeString('stdout');
42
+ /** Writes a line to `stderr`. Replaces the retired `Error` effect. */
43
+ export const error = writeString('stderr');
26
44
  export const now = do_('now');
27
45
  /**
28
46
  * Runs a plain synchronous function in an isolated, measured environment.
@@ -64,11 +64,11 @@ export default {
64
64
  throw result;
65
65
  }
66
66
  const tmp = state.root.tmp;
67
- if (tmp === undefined || isVec(tmp)) {
67
+ if (typeof tmp !== 'object') {
68
68
  throw state.root;
69
69
  }
70
70
  const cache = tmp.cache;
71
- if (cache === undefined || isVec(cache)) {
71
+ if (typeof cache !== 'object') {
72
72
  throw tmp;
73
73
  }
74
74
  },
@@ -294,7 +294,7 @@ export default {
294
294
  throw result;
295
295
  }
296
296
  const tmp = state.root.tmp;
297
- if (tmp === undefined || isVec(tmp)) {
297
+ if (typeof tmp !== 'object') {
298
298
  throw state.root;
299
299
  }
300
300
  if (tmp.cache !== undefined) {
@@ -333,8 +333,11 @@ export default {
333
333
  }
334
334
  },
335
335
  sandbox: {
336
+ // Virtual `sandbox` is now a pass-through: the function is expected
337
+ // to return a `SandboxResult` directly. Fixtures dictate the result
338
+ // (and `duration`) instead of the runner measuring.
336
339
  ok: () => {
337
- const [_, { result, duration }] = virtual(emptyState)(sandbox(() => 42));
340
+ const [_, { result, duration }] = virtual(emptyState)(sandbox(() => ({ result: ['ok', 42], duration: 0 })));
338
341
  if (result[0] !== 'ok') {
339
342
  throw result;
340
343
  }
@@ -347,7 +350,7 @@ export default {
347
350
  },
348
351
  error: () => {
349
352
  const err = new Error('fail');
350
- const [_, { result }] = virtual(emptyState)(sandbox(() => { throw err; }));
353
+ const [_, { result }] = virtual(emptyState)(sandbox(() => ({ result: ['error', err], duration: 0 })));
351
354
  if (result[0] !== 'error') {
352
355
  throw result;
353
356
  }
@@ -1,8 +1,17 @@
1
1
  import { type Vec } from '../../../bit_vec/module.f.ts';
2
2
  import { type RunInstance } from '../../mock/module.f.ts';
3
- import type { NodeOp } from '../module.f.ts';
3
+ import type { Module, NodeOp } from '../module.f.ts';
4
+ /**
5
+ * In-memory JS module entry. When `import_` is called on the path, the
6
+ * function is invoked and its return value is the module value (with a
7
+ * `default` export and optional named exports). Using a function (not a
8
+ * plain value) lets the entry be distinguished from `Vec`/`Dir` at runtime
9
+ * via `typeof === 'function'`, and lets the fixture compute the module on
10
+ * each import for closures/state.
11
+ */
12
+ export type JsModule = () => Module;
4
13
  export type Dir = {
5
- readonly [name in string]?: Dir | Vec;
14
+ readonly [name in string]?: Dir | Vec | JsModule;
6
15
  };
7
16
  export type State = {
8
17
  stdout: string;
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * @module
5
5
  */
6
- import { todo } from "../../../../dev/module.f.js";
6
+ import { assert, todo } from "../../../../dev/module.f.js";
7
7
  import { parse } from "../../../../path/module.f.js";
8
+ import { utf8ToString } from "../../../../text/module.f.js";
8
9
  import { isVec } from "../../../bit_vec/module.f.js";
9
10
  import { error, ok } from "../../../result/module.f.js";
10
11
  import { run } from "../../mock/module.f.js";
@@ -22,7 +23,7 @@ const operation = (op) => {
22
23
  }
23
24
  const [first, ...rest] = path;
24
25
  const subDir = dir[first];
25
- if (subDir === undefined || isVec(subDir)) {
26
+ if (typeof subDir !== 'object') {
26
27
  return op(dir, path);
27
28
  }
28
29
  const [newSubDir, r] = f(subDir, rest);
@@ -54,11 +55,24 @@ const readFile = readOperation((dir, path) => {
54
55
  return readFileError;
55
56
  }
56
57
  const file = dir[path[0]];
58
+ if (typeof file === 'function') {
59
+ throw new Error(`'${path[0]}' is a JsModule; readFile not supported`);
60
+ }
57
61
  if (!isVec(file)) {
58
62
  return error(`'${path[0]}' is not a file`);
59
63
  }
60
64
  return ok(file);
61
65
  });
66
+ const import_ = readOperation((dir, path) => {
67
+ if (path.length !== 1) {
68
+ return error('no such file');
69
+ }
70
+ const entry = dir[path[0]];
71
+ if (typeof entry !== 'function') {
72
+ return error(`'${path[0]}' is not a JsModule`);
73
+ }
74
+ return ok(entry());
75
+ });
62
76
  const writeFileError = error('invalid file');
63
77
  const writeFile = (payload) => operation((dir, path) => {
64
78
  if (path.length !== 1) {
@@ -85,7 +99,7 @@ const readdir = (base, recursive) => readOperation((dir, path) => {
85
99
  if (content === undefined) {
86
100
  continue;
87
101
  }
88
- const isFile = isVec(content);
102
+ const isFile = typeof content !== 'object';
89
103
  result = [...result, { name, parentPath, isFile }];
90
104
  if (!isFile && recursive) {
91
105
  result = [...result, ...f(`${parentPath}/${name}`, content)];
@@ -113,13 +127,12 @@ const rm = operation((dir, path) => {
113
127
  if (entry === undefined) {
114
128
  return [dir, error('no such file')];
115
129
  }
116
- if (!isVec(entry)) {
130
+ if (typeof entry === 'object') {
117
131
  return [dir, error('is a directory')];
118
132
  }
119
133
  const { [name]: _, ...rest } = dir;
120
134
  return [rest, okVoid];
121
135
  });
122
- const console = (name) => (state, payload) => [{ ...state, [name]: `${state[name]}${payload}\n` }, undefined];
123
136
  const map = {
124
137
  all: (state, ...a) => {
125
138
  let e = [];
@@ -130,8 +143,6 @@ const map = {
130
143
  }
131
144
  return [state, e];
132
145
  },
133
- error: console('stderr'),
134
- log: console('stdout'),
135
146
  fetch: (state, url) => {
136
147
  const result = state.internet[url];
137
148
  return result === undefined ? [state, error('not found')] : [state, ok(result)];
@@ -141,22 +152,24 @@ const map = {
141
152
  readdir: (state, path, { recursive }) => readdir(path, recursive === true)(state, path),
142
153
  writeFile: (state, path, payload) => writeFile(payload)(state, path),
143
154
  access,
144
- import: todo,
155
+ import: import_,
145
156
  rm,
146
157
  exec: todo,
147
158
  createServer: todo,
148
159
  listen: todo,
149
160
  forever: todo,
150
161
  now: (state) => [state, state.epochNs],
151
- sandbox: (state, f) => {
152
- let result;
153
- try {
154
- result = ok(f());
155
- }
156
- catch (e) {
157
- result = error(e);
158
- }
159
- return [state, { result, duration: 0 }];
162
+ // Virtual sandbox is a pass-through: the fixture's test function is
163
+ // expected to return a `SandboxResult` directly (encoding pass/fail and a
164
+ // chosen duration), so the handler invokes it without try/catch or clock
165
+ // reads. This makes test outcomes deterministic — fixtures dictate the
166
+ // result instead of the runner measuring real execution. A genuine
167
+ // exception in a fixture propagates loudly as a bug in the fixture.
168
+ // See: issues/156-tf-virtual-tests.md
169
+ sandbox: (state, f) => [state, f()],
170
+ write: (state, stream, data) => {
171
+ const s = utf8ToString(data);
172
+ return [{ ...state, [stream]: `${state[stream]}${s}` }, undefined];
160
173
  },
161
174
  };
162
175
  export const virtual = run(map);
@@ -6,6 +6,7 @@
6
6
  import type { Index3, Index5, Array2 } from '../../array/module.f.ts';
7
7
  export type Sign = -1 | 0 | 1;
8
8
  export type Compare<T> = (_: T) => Sign;
9
+ export type Cmp<T> = (a: T) => Compare<T>;
9
10
  export declare const index3: <T>(cmp: Compare<T>) => (value: T) => Index3;
10
11
  export declare const index5: <T>(cmp: Compare<T>) => (v2: Array2<T>) => Index5;
11
12
  export type Cmp1 = boolean | string | number | bigint;
@@ -23,3 +24,14 @@ export type Cmp2<A, B> = [
23
24
  B
24
25
  ] extends [bigint, bigint] ? bigint : never;
25
26
  export declare const cmp: <A extends Cmp1>(a: A) => <B extends Cmp2<A, B>>(b: B) => Sign;
27
+ /**
28
+ * Binary search over `[0, len)`. `probe(mid)` returns the sign of the search
29
+ * key relative to the element at `mid` (`-1` before, `0` at, `1` after). On a
30
+ * hit it returns the matching index; on a miss it returns the converged lower
31
+ * bound `b` (the insertion point), which may equal `len`.
32
+ *
33
+ * `probe` must be monotonic over `[0, len)`: scanning indices left to right its
34
+ * result is non-increasing — a run of `1`s, then `0`s, then `-1`s. A
35
+ * non-monotonic probe yields an undefined position.
36
+ */
37
+ export declare const bsearch: (len: number) => (probe: (mid: number) => Sign) => number;
@@ -4,3 +4,36 @@ export const index5 = cmp => ([v0, v1]) => {
4
4
  return (_0 <= 0 ? _0 + 1 : cmp(v1) + 3);
5
5
  };
6
6
  export const cmp = (a) => (b) => a < b ? -1 : a > b ? 1 : 0;
7
+ /**
8
+ * Binary search over `[0, len)`. `probe(mid)` returns the sign of the search
9
+ * key relative to the element at `mid` (`-1` before, `0` at, `1` after). On a
10
+ * hit it returns the matching index; on a miss it returns the converged lower
11
+ * bound `b` (the insertion point), which may equal `len`.
12
+ *
13
+ * `probe` must be monotonic over `[0, len)`: scanning indices left to right its
14
+ * result is non-increasing — a run of `1`s, then `0`s, then `-1`s. A
15
+ * non-monotonic probe yields an undefined position.
16
+ */
17
+ export const bsearch = (len) => (probe) => {
18
+ let b = 0;
19
+ let e = len - 1;
20
+ while (true) {
21
+ if (e < b) {
22
+ return b;
23
+ }
24
+ const mid = b + (e - b >> 1);
25
+ switch (probe(mid)) {
26
+ case -1: {
27
+ e = mid - 1;
28
+ break;
29
+ }
30
+ case 0: {
31
+ return mid;
32
+ }
33
+ case 1: {
34
+ b = mid + 1;
35
+ break;
36
+ }
37
+ }
38
+ }
39
+ };
@@ -0,0 +1,10 @@
1
+ export declare const joinTest: () => void;
2
+ export declare const concatTest: () => void;
3
+ export declare const logicalNotTest: () => void;
4
+ export declare const strictEqualTest: () => void;
5
+ export declare const additionTest: () => void;
6
+ export declare const minTest: () => void;
7
+ export declare const maxTest: () => void;
8
+ export declare const incrementTest: () => void;
9
+ export declare const foldToScanTest: () => void;
10
+ export declare const reduceToScanTest: () => void;