functionalscript 0.32.4 → 0.34.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 (90) hide show
  1. package/README.md +1 -1
  2. package/fs/asserts/module.f.d.ts +1 -1
  3. package/fs/asserts/module.f.js +4 -1
  4. package/fs/bnf/data/module.f.d.ts +0 -103
  5. package/fs/bnf/data/module.f.js +10 -246
  6. package/fs/bnf/data/proof.f.d.ts +0 -7
  7. package/fs/bnf/data/proof.f.js +3 -636
  8. package/fs/bnf/descent/module.f.d.ts +56 -0
  9. package/fs/bnf/descent/module.f.js +119 -0
  10. package/fs/bnf/descent/proof.f.d.ts +5 -0
  11. package/fs/bnf/descent/proof.f.js +296 -0
  12. package/fs/bnf/ll1/module.f.d.ts +72 -0
  13. package/fs/bnf/ll1/module.f.js +153 -0
  14. package/fs/bnf/ll1/proof.f.d.ts +9 -0
  15. package/fs/bnf/ll1/proof.f.js +358 -0
  16. package/fs/cas/cli/module.f.d.ts +6 -0
  17. package/fs/cas/cli/module.f.js +56 -0
  18. package/fs/cas/cli/proof.f.d.ts +14 -0
  19. package/fs/cas/cli/proof.f.js +149 -0
  20. package/fs/cas/mcp/module.f.d.ts +9 -17
  21. package/fs/cas/mcp/module.f.js +117 -83
  22. package/fs/cas/mcp/proof.f.d.ts +5 -1
  23. package/fs/cas/mcp/proof.f.js +79 -32
  24. package/fs/cas/module.f.d.ts +33 -27
  25. package/fs/cas/module.f.js +151 -126
  26. package/fs/cas/proof.f.d.ts +16 -13
  27. package/fs/cas/proof.f.js +255 -130
  28. package/fs/ci/config/module.f.d.ts +15 -15
  29. package/fs/ci/config/module.f.js +15 -15
  30. package/fs/ci/proof.f.js +8 -6
  31. package/fs/cli/proof.f.js +2 -12
  32. package/fs/djs/parser/module.f.d.ts +0 -5
  33. package/fs/djs/proof.f.js +5 -6
  34. package/fs/djs/serializer/module.f.d.ts +1 -3
  35. package/fs/djs/tokenizer-new/module.f.js +3 -2
  36. package/fs/djs/tokenizer-new/proof.f.d.ts +3 -0
  37. package/fs/djs/tokenizer-new/proof.f.js +458 -296
  38. package/fs/djs/transpiler/proof.f.js +10 -10
  39. package/fs/effects/list/module.f.d.ts +31 -0
  40. package/fs/effects/list/module.f.js +16 -0
  41. package/fs/effects/module.f.d.ts +11 -5
  42. package/fs/effects/module.f.js +16 -6
  43. package/fs/effects/node/module.f.d.ts +29 -3
  44. package/fs/effects/node/module.f.js +35 -1
  45. package/fs/effects/node/module.js +32 -18
  46. package/fs/effects/node/proof.f.js +36 -36
  47. package/fs/effects/node/virtual/module.f.d.ts +16 -2
  48. package/fs/effects/node/virtual/module.f.js +129 -20
  49. package/fs/effects/node/virtual/proof.f.d.ts +4 -0
  50. package/fs/effects/node/virtual/proof.f.js +47 -14
  51. package/fs/effects/proof.f.d.ts +4 -0
  52. package/fs/effects/proof.f.js +18 -1
  53. package/fs/emergent_testing/proof.f.js +3 -11
  54. package/fs/fjs/module.f.js +2 -8
  55. package/fs/fjs/proof.f.js +2 -11
  56. package/fs/html/module.f.js +6 -8
  57. package/fs/js/tokenizer/module.f.d.ts +5 -0
  58. package/fs/js/tokenizer/module.f.js +13 -0
  59. package/fs/json/serializer/module.f.js +0 -1
  60. package/fs/mcp/module.f.d.ts +1 -1
  61. package/fs/mime/module.f.d.ts +101 -5
  62. package/fs/mime/module.f.js +159 -6
  63. package/fs/mime/proof.f.d.ts +25 -0
  64. package/fs/mime/proof.f.js +193 -3
  65. package/fs/text/code_point/module.f.d.ts +39 -2
  66. package/fs/text/code_point/module.f.js +74 -2
  67. package/fs/text/code_point/proof.f.d.ts +8 -0
  68. package/fs/text/code_point/proof.f.js +72 -0
  69. package/fs/text/sgr/proof.f.js +2 -11
  70. package/fs/text/utf16/module.f.js +4 -26
  71. package/fs/text/utf8/module.f.d.ts +27 -0
  72. package/fs/text/utf8/module.f.js +8 -14
  73. package/fs/types/bigint/module.f.d.ts +8 -15
  74. package/fs/types/bigint/module.f.js +9 -22
  75. package/fs/types/bigint/proof.f.d.ts +0 -2
  76. package/fs/types/bigint/proof.f.js +6 -16
  77. package/fs/types/function/operator/module.f.d.ts +26 -0
  78. package/fs/types/list/module.f.d.ts +3 -0
  79. package/fs/types/result/module.d.ts +2 -4
  80. package/fs/types/result/module.js +9 -3
  81. package/package.json +3 -3
  82. package/issues/demo/data/data.f.js +0 -12
  83. package/issues/demo/data/shared.f.js +0 -3
  84. package/issues/demo/fs/app.js +0 -4
  85. package/issues/demo/fs/math.f.js +0 -4
  86. package/issues/demo/sample/proof.f.js +0 -13
  87. /package/{issues → todo}/031-json.f.d.ts +0 -0
  88. /package/{issues → todo}/031-json.f.js +0 -0
  89. /package/{issues → todo}/proof.f.d.ts +0 -0
  90. /package/{issues → todo}/proof.f.js +0 -0
@@ -9,7 +9,7 @@ const run = (root) => (path) => {
9
9
  };
10
10
  export const proof = {
11
11
  parse: () => {
12
- const result = run({ a: utf8('export default 1') })('a');
12
+ const result = run({ a: [utf8('export default 1')] })('a');
13
13
  if (result[0] === 'error') {
14
14
  throw result[1];
15
15
  }
@@ -19,7 +19,7 @@ export const proof = {
19
19
  }
20
20
  },
21
21
  parseWithSubModule: () => {
22
- const result = run({ a: { b: utf8('import c from "c"\nexport default c'), c: utf8('export default 2') } })('a/b');
22
+ const result = run({ a: { b: [utf8('import c from "c"\nexport default c')], c: [utf8('export default 2')] } })('a/b');
23
23
  if (result[0] === 'error') {
24
24
  throw result[1];
25
25
  }
@@ -30,10 +30,10 @@ export const proof = {
30
30
  },
31
31
  parseWithSubModules: () => {
32
32
  const result = run({
33
- a: utf8('import b from "b"\nimport c from "c"\nexport default [b,c,b]'),
34
- b: utf8('import d from "d"\nexport default [0,d]'),
35
- c: utf8('import d from "d"\nexport default [1,d]'),
36
- d: utf8('export default 2'),
33
+ a: [utf8('import b from "b"\nimport c from "c"\nexport default [b,c,b]')],
34
+ b: [utf8('import d from "d"\nexport default [0,d]')],
35
+ c: [utf8('import d from "d"\nexport default [1,d]')],
36
+ d: [utf8('export default 2')],
37
37
  })('a');
38
38
  if (result[0] === 'error') {
39
39
  throw result[1];
@@ -44,7 +44,7 @@ export const proof = {
44
44
  }
45
45
  },
46
46
  parseWithFileNotFoundError: () => {
47
- const result = run({ a: utf8('import b from "b"\nexport default b') })('a');
47
+ const result = run({ a: [utf8('import b from "b"\nexport default b')] })('a');
48
48
  if (result[0] !== 'error') {
49
49
  throw result;
50
50
  }
@@ -54,9 +54,9 @@ export const proof = {
54
54
  },
55
55
  parseWithCycleError: () => {
56
56
  const result = run({
57
- a: utf8('import b from "b"\nimport c from "c"\nexport default [b,c,b]'),
58
- b: utf8('import c from "c"\nexport default c'),
59
- c: utf8('import b from "b"\nexport default b'),
57
+ a: [utf8('import b from "b"\nimport c from "c"\nexport default [b,c,b]')],
58
+ b: [utf8('import c from "c"\nexport default c')],
59
+ c: [utf8('import b from "b"\nexport default b')],
60
60
  })('a');
61
61
  if (result[0] !== 'error') {
62
62
  throw result;
@@ -0,0 +1,31 @@
1
+ import { type Effect, type Operation } from "../module.f.ts";
2
+ export type NonEmpty<O extends Operation, T> = {
3
+ readonly first: T;
4
+ readonly tail: List<O, T>;
5
+ };
6
+ /**
7
+ * The payload of a `List` effect: the next cons cell, or `undefined` at
8
+ * end-of-stream.
9
+ *
10
+ * Since `Pure<T>` is now itself a lazy thunk (`() => T`), the effect alone is the
11
+ * suspension point and the cell needs no extra wrapping thunk. `Effect<O, Next<O, T>>`
12
+ * is used directly in places where `List<O, T>` cannot be written as a return type
13
+ * (see {@link empty}).
14
+ */
15
+ export type Next<O extends Operation, T> = NonEmpty<O, T> | undefined;
16
+ export type List<O extends Operation, T> = Effect<O, Next<O, T>>;
17
+ /**
18
+ * The empty `List`: a pure end-of-stream marker (`undefined`).
19
+ *
20
+ * The explicit `Effect<O, Next<O, T>>` return type lets the contextual type drive the
21
+ * check, so the recursive payload type-checks without a cast. Construct streams through
22
+ * these two combinators.
23
+ *
24
+ * Note: we use `Effect<O, Next<O, T>>` because TypeScript can't convert `pure(...)` to
25
+ * `List<O, T>`.
26
+ */
27
+ export declare const empty: <O extends Operation, T>() => Effect<O, Next<O, T>>;
28
+ /**
29
+ * Prepends `head` to a `ListEffect` `tail`, as a pure cons cell. See {@link empty}.
30
+ */
31
+ export declare const nonEmpty: <O extends Operation, T>(first: T, tail: List<O, T>) => Effect<O, Next<O, T>>;
@@ -0,0 +1,16 @@
1
+ import { pure } from "../module.f.js";
2
+ /**
3
+ * The empty `List`: a pure end-of-stream marker (`undefined`).
4
+ *
5
+ * The explicit `Effect<O, Next<O, T>>` return type lets the contextual type drive the
6
+ * check, so the recursive payload type-checks without a cast. Construct streams through
7
+ * these two combinators.
8
+ *
9
+ * Note: we use `Effect<O, Next<O, T>>` because TypeScript can't convert `pure(...)` to
10
+ * `List<O, T>`.
11
+ */
12
+ export const empty = () => pure(undefined);
13
+ /**
14
+ * Prepends `head` to a `ListEffect` `tail`, as a pure cons cell. See {@link empty}.
15
+ */
16
+ export const nonEmpty = (first, tail) => pure({ first, tail });
@@ -10,12 +10,19 @@ export type Effect<O extends Operation, T> = {
10
10
  step: <Q extends Operation, R>(f: (p: T) => Effect<Q, R>) => Effect<O | Q, R>;
11
11
  };
12
12
  export type Value<O extends Operation, T> = Pure<T> | Do<O, T>;
13
- export type Pure<T> = readonly [T];
13
+ export type Pure<T> = () => T;
14
14
  export type DoKPR<O extends Operation, T, K extends string, PR extends readonly [unknown, unknown]> = readonly [K, PR[0], (_: PR[1]) => Effect<O, T>];
15
15
  export type Pr<O extends Operation, K extends O[0]> = O extends readonly [K, (...args: infer P) => infer R] ? readonly [P, R] : never;
16
16
  export type DoK<O extends Operation, T, K extends O[0]> = DoKPR<O, T, K, Pr<O, K>>;
17
17
  export type Do<O extends Operation, T> = DoK<O, T, O[0]>;
18
18
  export declare const pure: <T>(v: T) => Effect<never, T>;
19
+ /**
20
+ * A lazy pure effect. Like {@link pure}, but takes a thunk and evaluates it on
21
+ * demand instead of holding an already-computed value. Use this to produce the
22
+ * next item of a list without instantiating the rest — the thunk runs only when
23
+ * the effect is decoded (or stepped into).
24
+ */
25
+ export declare const lazy: <T>(t: () => T) => Effect<never, T>;
19
26
  export declare const doFull: <O extends Operation, T, K extends O[0]>(cmd: K, param: Pr<O, K>[0], cont: (input: Pr<O, K>[1]) => Effect<O, T>) => Effect<O, T>;
20
27
  export type Param<O extends Operation> = F<O>[0];
21
28
  export type Return<O extends Operation> = F<O>[1];
@@ -53,10 +60,10 @@ export type Decoded<O extends Operation, T> = {
53
60
  /**
54
61
  * Decodes an effect's next step: a pure result, or a command to perform.
55
62
  *
56
- * This is the only function that knows how `Value` is laid out (a length-1
57
- * tuple for `Pure`, a `[command, payload, continuation]` tuple for `Do`).
63
+ * This is the only function that knows how `Value` is laid out (a thunk
64
+ * `() => T` for `Pure`, a `[command, payload, continuation]` tuple for `Do`).
58
65
  * Interpreters and proofs must go through `decode` (or `match`) instead of
59
- * inspecting the tuple, so the representation can change without touching
66
+ * inspecting the value, so the representation can change without touching
60
67
  * them.
61
68
  */
62
69
  export declare const decode: <O extends Operation, T>({ value }: Effect<O, T>) => Decoded<O, T>;
@@ -82,4 +89,3 @@ export type ToAsyncOperationMap<O extends Operation> = {
82
89
  };
83
90
  export type F<O extends Operation> = Pr<O, O[0]>;
84
91
  export type Func<O extends Operation> = (..._: Param<O>) => Effect<O, Return<O>>;
85
- export type ListEffect<O extends Operation, T> = Effect<O, readonly [T, ListEffect<O, T>] | undefined>;
@@ -5,9 +5,19 @@
5
5
  */
6
6
  import { fold } from "../types/list/module.f.js";
7
7
  export const pure = (v) => ({
8
- value: [v],
8
+ value: () => v,
9
9
  step: f => f(v)
10
10
  });
11
+ /**
12
+ * A lazy pure effect. Like {@link pure}, but takes a thunk and evaluates it on
13
+ * demand instead of holding an already-computed value. Use this to produce the
14
+ * next item of a list without instantiating the rest — the thunk runs only when
15
+ * the effect is decoded (or stepped into).
16
+ */
17
+ export const lazy = (t) => ({
18
+ value: t,
19
+ step: f => f(t())
20
+ });
11
21
  export const doFull = (cmd, param, cont) => ({
12
22
  value: [cmd, param, cont],
13
23
  step: (f) => doFull(cmd, param, x => cont(x).step(f)),
@@ -32,14 +42,14 @@ export const forEachStep = (f) => (items) => foldStep((item) => () => f(item))(u
32
42
  /**
33
43
  * Decodes an effect's next step: a pure result, or a command to perform.
34
44
  *
35
- * This is the only function that knows how `Value` is laid out (a length-1
36
- * tuple for `Pure`, a `[command, payload, continuation]` tuple for `Do`).
45
+ * This is the only function that knows how `Value` is laid out (a thunk
46
+ * `() => T` for `Pure`, a `[command, payload, continuation]` tuple for `Do`).
37
47
  * Interpreters and proofs must go through `decode` (or `match`) instead of
38
- * inspecting the tuple, so the representation can change without touching
48
+ * inspecting the value, so the representation can change without touching
39
49
  * them.
40
50
  */
41
- export const decode = ({ value }) => value.length === 1
42
- ? { done: true, result: value[0] }
51
+ export const decode = ({ value }) => typeof value === 'function'
52
+ ? { done: true, result: value() }
43
53
  : { done: false, command: value[0], payload: value[1], continuation: value[2] };
44
54
  /**
45
55
  * Decodes an effect's next step and dispatches its command to `map`,
@@ -1,9 +1,10 @@
1
- import type { Vec } from '../../types/bit_vec/module.f.ts';
1
+ import { type Vec } from '../../types/bit_vec/module.f.ts';
2
2
  import type { MemOp } from '../memory/module.f.ts';
3
3
  import type { Nominal } from '../../types/nominal/module.f.ts';
4
4
  import { type Result } from '../../types/result/module.f.ts';
5
5
  import type { StringMap } from '../../types/object/module.f.ts';
6
6
  import { type Effect, type Func, type Operation, type ToAsyncOperationMap } from '../module.f.ts';
7
+ import type { List } from '../list/module.f.ts';
7
8
  export type IoResult<T> = Result<T, unknown>;
8
9
  /**
9
10
  * True if `e` is a "file or directory does not exist" (`ENOENT`) error.
@@ -81,7 +82,32 @@ export type Exec = readonly ['exec', (command: string, stdin?: string) => IoResu
81
82
  export declare const exec: Func<Exec>;
82
83
  export type Access = readonly ['access', (path: string) => IoResult<void>];
83
84
  export declare const access: Func<Access>;
84
- export type Fs = Mkdir | ReadFile | ReadBytes | Readdir | WriteFile | Rm | Rename | Exec | Access;
85
+ /**
86
+ * Creates `path` as an empty file with `O_CREAT|O_EXCL` — fails if it already
87
+ * exists. This is the exclusive create that claims a staging name in the
88
+ * lock-free upload ([staging-lease.md](../../../issues/cas/staging-lease.md));
89
+ * with 256 random bits in the name `EEXIST` never happens in practice, so it
90
+ * is just a sanity guard.
91
+ */
92
+ export type CreateExclusive = readonly ['createExclusive', (path: string) => IoResult<void>];
93
+ export declare const createExclusive: Func<CreateExclusive>;
94
+ /**
95
+ * Writes the **entire** `data` vector to an **existing** `path` at byte `offset`
96
+ * (positional write). The mirror of {@link readBytes}: it never creates the file
97
+ * (a missing path is `ENOENT`), and it writes every byte or returns an error —
98
+ * the runner loops over short writes — so a later size check can never pass over
99
+ * a hole. Bounded to ≤128 KiB per call, like `readBytes`.
100
+ */
101
+ export type WriteBytes = readonly ['writeBytes', (path: string, offset: number, data: Vec) => IoResult<void>];
102
+ export declare const writeBytes: Func<WriteBytes>;
103
+ export declare const writeFromStream: <O extends Operation>(path: string, e: List<O, IoResult<Vec>>) => Effect<O | WriteBytes | CreateExclusive, IoResult<void>>;
104
+ /** File metadata returned by {@link stat}. Only `size` (in bytes) for now. */
105
+ export type FileStat = {
106
+ readonly size: number;
107
+ };
108
+ export type Stat = readonly ['stat', (path: string) => IoResult<FileStat>];
109
+ export declare const stat: Func<Stat>;
110
+ export type Fs = Mkdir | ReadFile | ReadBytes | Readdir | WriteFile | Rm | Rename | Exec | Access | CreateExclusive | WriteBytes | Stat;
85
111
  export type Server = Nominal<'server', `160855c4f69310fece3273c1853ac32de43dee1eb41bf59d821917f8eebe9272`, unknown>;
86
112
  export type Headers = StringMap<string, string>;
87
113
  export type IncomingMessage = {
@@ -216,7 +242,7 @@ export type TestContext = {
216
242
  /** Effect operation that registers a named test with the active `TestContext`. */
217
243
  export type Test = readonly ['test', (ctx: TestContext, name: string, expectFailure: boolean, test: (t: TestContext) => Effect<Test | All | Await, void>) => void];
218
244
  export declare const test: Func<Test>;
219
- export type NodeOp = All | Await | Fetch | Fs | Http | Forever | Import | MemOp | Now | RandomInt | Read | Sandbox | Write | Test;
245
+ export type NodeOp = Access | All | Await | Fetch | Fs | Http | Forever | Import | MemOp | Now | RandomInt | Read | Sandbox | Write | Test;
220
246
  export type NodeEffect<T> = Effect<NodeOp, T>;
221
247
  /**
222
248
  * Writes an error line to `stderr` and yields exit code `1`. The canonical
@@ -12,7 +12,8 @@ import { utf8, utf8ToString } from "../../text/module.f.js";
12
12
  import { toCodePointList } from "../../text/utf8/module.f.js";
13
13
  import { codePointListToString } from "../../text/utf16/module.f.js";
14
14
  import { reverse } from "../../types/list/module.f.js";
15
- import { ok } from "../../types/result/module.f.js";
15
+ import { length } from "../../types/bit_vec/module.f.js";
16
+ import { ok, error as resultError } from "../../types/result/module.f.js";
16
17
  import { do_, pure } from "../module.f.js";
17
18
  /**
18
19
  * True if `e` is a "file or directory does not exist" (`ENOENT`) error.
@@ -53,6 +54,39 @@ export const readBytes = do_('readBytes');
53
54
  export const randomInt = do_('randomInt');
54
55
  export const exec = do_('exec');
55
56
  export const access = do_('access');
57
+ export const createExclusive = do_('createExclusive');
58
+ export const writeBytes = do_('writeBytes');
59
+ const writeLoop = (path) => {
60
+ const f = (offset, e) => e.step(r => {
61
+ if (r === undefined) {
62
+ return pure(ok(undefined));
63
+ }
64
+ const { first: [t, v], tail } = r;
65
+ if (t === 'error') {
66
+ return pure(resultError(v));
67
+ }
68
+ const lenV = length(v);
69
+ if ((lenV & 7n) !== 0n) {
70
+ return pure(resultError('invalid buffer size'));
71
+ }
72
+ return writeBytes(path, offset, v)
73
+ .step((r) => {
74
+ if (r[0] === 'error') {
75
+ return pure(r);
76
+ }
77
+ return f(offset + Number(lenV >> 3n), tail);
78
+ });
79
+ });
80
+ return f;
81
+ };
82
+ export const writeFromStream = (path, e) => createExclusive(path)
83
+ .step(([r, v]) => {
84
+ if (r === 'error') {
85
+ return pure(resultError(v));
86
+ }
87
+ return writeLoop(path)(0, e);
88
+ });
89
+ export const stat = do_('stat');
56
90
  export const createServer = do_('createServer');
57
91
  export const listen = do_('listen');
58
92
  export const forever = do_('forever');
@@ -35,6 +35,7 @@ import { memoryOperationMap } from "./memory/module.js";
35
35
  import {} from "./module.f.js";
36
36
  import { asBase, asNominal } from "../../types/nominal/module.f.js";
37
37
  import { error, ok } from "../../types/result/module.f.js";
38
+ import { asyncTryCatch } from "../../types/result/module.js";
38
39
  import { fromVec, listToVec, toVec } from "../../types/uint8array/module.f.js";
39
40
  import { maxLengthBytes } from "../../types/bit_vec/module.f.js";
40
41
  /**
@@ -44,14 +45,6 @@ import { maxLengthBytes } from "../../types/bit_vec/module.f.js";
44
45
  * requires them present; this local view keeps the narrowing in one place.
45
46
  */
46
47
  const createServer = http.createServer;
47
- const tc = async (f) => {
48
- try {
49
- return ok(await f());
50
- }
51
- catch (e) {
52
- return error(e);
53
- }
54
- };
55
48
  const collect = async (v) => {
56
49
  let result = [];
57
50
  for await (const a of v) {
@@ -156,15 +149,15 @@ const readStdinByte = async () => {
156
149
  const runNodeEffect = asyncRun({
157
150
  ...memoryOperationMap(),
158
151
  all: async (...effects) => await Promise.all(effects.map(runNodeEffect)),
159
- fetch: async (url) => tc(async () => {
152
+ fetch: async (url) => asyncTryCatch(async () => {
160
153
  const response = await fetch(url);
161
154
  if (!response.ok) {
162
155
  throw new Error(`Fetch error: ${response.status} ${response.statusText}`);
163
156
  }
164
157
  return toVec(new Uint8Array(await response.arrayBuffer()));
165
158
  }),
166
- mkdir: (...p) => tc(async () => { await mkdir(...p); }),
167
- readFile: path => tc(async () => {
159
+ mkdir: (...p) => asyncTryCatch(async () => { await mkdir(...p); }),
160
+ readFile: path => asyncTryCatch(async () => {
168
161
  const fileStats = await stat(path);
169
162
  // if the file is too big, toVec should fail anyway but in this case we don't want to load the file.
170
163
  if (fileStats.size > maxFileSizeBytes) {
@@ -172,16 +165,16 @@ const runNodeEffect = asyncRun({
172
165
  }
173
166
  return toVec(await readFile(path));
174
167
  }),
175
- readdir: (path, r) => tc(async () => (await readdir(path, { ...r, withFileTypes: true }))
168
+ readdir: (path, r) => asyncTryCatch(async () => (await readdir(path, { ...r, withFileTypes: true }))
176
169
  .map(v => ({
177
170
  name: v.name,
178
171
  parentPath: normalize(v.parentPath),
179
172
  isFile: v.isFile()
180
173
  }))),
181
- writeFile: (path, data) => tc(() => writeFile(path, fromVec(data))),
182
- rm: path => tc(() => rm(path)),
183
- rename: (src, dst) => tc(() => rename(src, dst)),
184
- readBytes: (path, offset, size) => tc(async () => {
174
+ writeFile: (path, data) => asyncTryCatch(() => writeFile(path, fromVec(data))),
175
+ rm: path => asyncTryCatch(() => rm(path)),
176
+ rename: (src, dst) => asyncTryCatch(() => rename(src, dst)),
177
+ readBytes: (path, offset, size) => asyncTryCatch(async () => {
185
178
  if (offset < 0) {
186
179
  throw new Error(`Offset ${offset} is negative`);
187
180
  }
@@ -199,8 +192,29 @@ const runNodeEffect = asyncRun({
199
192
  }
200
193
  }),
201
194
  randomInt: async () => crypto.randomInt(2 ** 32),
202
- access: path => tc(() => access(path)),
203
- import: path => tc(() => asyncImport(path)),
195
+ access: path => asyncTryCatch(() => access(path)),
196
+ createExclusive: path => asyncTryCatch(async () => {
197
+ const fh = await open(path, 'wx');
198
+ await fh.close();
199
+ }),
200
+ writeBytes: (path, offset, data) => asyncTryCatch(async () => {
201
+ const fh = await open(path, 'r+');
202
+ try {
203
+ const buffer = fromVec(data);
204
+ // Loop over short writes so the whole Vec lands — a partial pwrite would
205
+ // leave a hole the publish-time size check could pass over.
206
+ let written = 0;
207
+ while (written < buffer.length) {
208
+ const { bytesWritten } = await fh.write(buffer, written, buffer.length - written, offset + written);
209
+ written += bytesWritten;
210
+ }
211
+ }
212
+ finally {
213
+ await fh.close();
214
+ }
215
+ }),
216
+ stat: path => asyncTryCatch(async () => ({ size: (await stat(path)).size })),
217
+ import: path => asyncTryCatch(() => asyncImport(path)),
204
218
  exec: (command, stdin) => new Promise(resolve => {
205
219
  const child = exec(command, (e, stdout, stderr) => resolve(e !== null ? ['error', e] : ok({ stdout, stderr })));
206
220
  child.stdin?.end(stdin);
@@ -51,7 +51,7 @@ export const proof = {
51
51
  throw result;
52
52
  }
53
53
  const a = state.root.a;
54
- if (a === undefined || isVec(a)) {
54
+ if (a === undefined || Array.isArray(a)) {
55
55
  throw a;
56
56
  }
57
57
  },
@@ -61,11 +61,11 @@ export const proof = {
61
61
  throw result;
62
62
  }
63
63
  const tmp = state.root.tmp;
64
- if (typeof tmp !== 'object') {
64
+ if (typeof tmp !== 'object' || Array.isArray(tmp)) {
65
65
  throw state.root;
66
66
  }
67
67
  const cache = tmp.cache;
68
- if (typeof cache !== 'object') {
68
+ if (typeof cache !== 'object' || Array.isArray(cache)) {
69
69
  throw tmp;
70
70
  }
71
71
  },
@@ -84,7 +84,7 @@ export const proof = {
84
84
  const initial = {
85
85
  ...emptyState,
86
86
  root: {
87
- hello: vec8(0x2an),
87
+ hello: [vec8(0x2an)],
88
88
  },
89
89
  };
90
90
  const [state, [t, result]] = virtual(initial)(readFile('hello'));
@@ -104,7 +104,7 @@ export const proof = {
104
104
  nested: () => {
105
105
  const [_, [tag, result]] = virtual({
106
106
  ...emptyState,
107
- root: { tmp: { cache: vec8(0x15n) } }
107
+ root: { tmp: { cache: [vec8(0x15n)] } }
108
108
  })(readFile('tmp/cache'));
109
109
  if (tag === 'error') {
110
110
  throw result;
@@ -133,7 +133,7 @@ export const proof = {
133
133
  const initial = {
134
134
  ...emptyState,
135
135
  root: {
136
- smallFile: vec8(0x2an),
136
+ smallFile: [vec8(0x2an)],
137
137
  },
138
138
  };
139
139
  const [_, [t, result]] = virtual(initial)(readFile('smallFile'));
@@ -149,7 +149,7 @@ export const proof = {
149
149
  ok: () => {
150
150
  const [_, [t, result]] = virtual({
151
151
  ...emptyState,
152
- root: { hello: utf8('Hello, world!') },
152
+ root: { hello: [utf8('Hello, world!')] },
153
153
  })(readUtf8File('hello'));
154
154
  if (t !== 'ok') {
155
155
  throw result;
@@ -170,9 +170,9 @@ export const proof = {
170
170
  const [_, [t, result]] = virtual({
171
171
  ...emptyState,
172
172
  root: {
173
- file: vec8(0x2an),
173
+ file: [vec8(0x2an)],
174
174
  dir: {
175
- a: empty
175
+ a: [empty]
176
176
  },
177
177
  },
178
178
  })(readdir('', { recursive: true }));
@@ -192,9 +192,9 @@ export const proof = {
192
192
  const [_, [t, result]] = virtual({
193
193
  ...emptyState,
194
194
  root: {
195
- file: vec8(0x2an),
195
+ file: [vec8(0x2an)],
196
196
  dir: {
197
- a: empty
197
+ a: [empty]
198
198
  },
199
199
  },
200
200
  })(readdir('', {}));
@@ -216,7 +216,7 @@ export const proof = {
216
216
  nested: () => {
217
217
  const [_, [t, result]] = virtual({
218
218
  ...emptyState,
219
- root: { tmp: { cache: vec8(0x15n) } }
219
+ root: { tmp: { cache: [vec8(0x15n)] } }
220
220
  })(readdir('tmp', { recursive: true }));
221
221
  if (t !== 'ok') {
222
222
  throw result;
@@ -249,10 +249,10 @@ export const proof = {
249
249
  throw result;
250
250
  }
251
251
  const file = state.root.hello;
252
- if (!isVec(file)) {
252
+ if (!Array.isArray(file)) {
253
253
  throw file;
254
254
  }
255
- if (uint(file) !== 0x2an) {
255
+ if (uint(file[0]) !== 0x2an) {
256
256
  throw file;
257
257
  }
258
258
  },
@@ -260,17 +260,17 @@ export const proof = {
260
260
  const [state, [t, result]] = virtual({
261
261
  ...emptyState,
262
262
  root: {
263
- hello: vec8(0x15n),
263
+ hello: [vec8(0x15n)],
264
264
  },
265
265
  })(writeFile('hello', vec8(0x2an)));
266
266
  if (t !== 'ok') {
267
267
  throw result;
268
268
  }
269
269
  const file = state.root.hello;
270
- if (!isVec(file)) {
270
+ if (!Array.isArray(file)) {
271
271
  throw file;
272
272
  }
273
- if (uint(file) !== 0x2an) {
273
+ if (uint(file[0]) !== 0x2an) {
274
274
  throw file;
275
275
  }
276
276
  },
@@ -300,7 +300,7 @@ export const proof = {
300
300
  throw result;
301
301
  }
302
302
  const tmp = state.root.tmp;
303
- if (tmp === undefined || isVec(tmp)) {
303
+ if (tmp === undefined || Array.isArray(tmp)) {
304
304
  throw tmp;
305
305
  }
306
306
  },
@@ -311,10 +311,10 @@ export const proof = {
311
311
  throw result;
312
312
  }
313
313
  const file = state.root.hello;
314
- if (!isVec(file)) {
314
+ if (!Array.isArray(file)) {
315
315
  throw file;
316
316
  }
317
- if (utf8ToString(file) !== 'Hello, world!') {
317
+ if (utf8ToString(file[0]) !== 'Hello, world!') {
318
318
  throw file;
319
319
  }
320
320
  },
@@ -322,7 +322,7 @@ export const proof = {
322
322
  one: () => {
323
323
  const [state, [t, result]] = virtual({
324
324
  ...emptyState,
325
- root: { hello: vec8(0x2an) },
325
+ root: { hello: [vec8(0x2an)] },
326
326
  })(rm('hello'));
327
327
  if (t !== 'ok') {
328
328
  throw result;
@@ -334,13 +334,13 @@ export const proof = {
334
334
  nested: () => {
335
335
  const [state, [t, result]] = virtual({
336
336
  ...emptyState,
337
- root: { tmp: { cache: vec8(0x15n) } },
337
+ root: { tmp: { cache: [vec8(0x15n)] } },
338
338
  })(rm('tmp/cache'));
339
339
  if (t !== 'ok') {
340
340
  throw result;
341
341
  }
342
342
  const tmp = state.root.tmp;
343
- if (typeof tmp !== 'object') {
343
+ if (typeof tmp !== 'object' || Array.isArray(tmp)) {
344
344
  throw state.root;
345
345
  }
346
346
  if (tmp.cache !== undefined) {
@@ -376,8 +376,8 @@ export const proof = {
376
376
  const [_, results] = virtual({
377
377
  ...emptyState,
378
378
  root: {
379
- a: vec8(0x2an),
380
- b: vec8(0x15n),
379
+ a: [vec8(0x2an)],
380
+ b: [vec8(0x15n)],
381
381
  },
382
382
  })(both(readFile('a'))(readFile('b')));
383
383
  if (results[0][0] !== 'ok') {
@@ -446,7 +446,7 @@ export const proof = {
446
446
  fileOverFile: () => {
447
447
  const [state, [t, result]] = virtual({
448
448
  ...emptyState,
449
- root: { src: vec8(0x2an), dst: vec8(0x15n) },
449
+ root: { src: [vec8(0x2an)], dst: [vec8(0x15n)] },
450
450
  })(rename('src', 'dst'));
451
451
  if (t !== 'ok') {
452
452
  throw result;
@@ -454,23 +454,23 @@ export const proof = {
454
454
  if (state.root.src !== undefined) {
455
455
  throw state.root;
456
456
  }
457
- if (!isVec(state.root.dst)) {
457
+ if (!Array.isArray(state.root.dst)) {
458
458
  throw state.root;
459
459
  }
460
- if (uint(state.root.dst) !== 0x2an) {
460
+ if (uint(state.root.dst[0]) !== 0x2an) {
461
461
  throw state.root;
462
462
  }
463
463
  },
464
464
  nestedRename: () => {
465
465
  const [state, [t, result]] = virtual({
466
466
  ...emptyState,
467
- root: { tmp: { src: vec8(0x2an) } },
467
+ root: { tmp: { src: [vec8(0x2an)] } },
468
468
  })(rename('tmp/src', 'tmp/dst'));
469
469
  if (t !== 'ok') {
470
470
  throw result;
471
471
  }
472
472
  const tmp = state.root.tmp;
473
- if (typeof tmp !== 'object') {
473
+ if (typeof tmp !== 'object' || Array.isArray(tmp)) {
474
474
  throw state.root;
475
475
  }
476
476
  if (tmp.src !== undefined) {
@@ -480,12 +480,12 @@ export const proof = {
480
480
  dirOverFile: () => {
481
481
  const [state, [t, result]] = virtual({
482
482
  ...emptyState,
483
- root: { src: {}, dst: vec8(0x15n) },
483
+ root: { src: {}, dst: [vec8(0x15n)] },
484
484
  })(rename('src', 'dst'));
485
485
  if (t !== 'error') {
486
486
  throw result;
487
487
  }
488
- if (!isVec(state.root.dst)) {
488
+ if (!Array.isArray(state.root.dst)) {
489
489
  throw state.root;
490
490
  }
491
491
  },
@@ -500,8 +500,8 @@ export const proof = {
500
500
  simple: () => {
501
501
  const [_, [t, result]] = virtual({
502
502
  ...emptyState,
503
- root: { file: vec8(0xabcdn) },
504
- })(readBytes('file', 0, 2));
503
+ root: { file: [vec8(0xabn)] },
504
+ })(readBytes('file', 0, 1));
505
505
  if (t !== 'ok') {
506
506
  throw result;
507
507
  }
@@ -512,7 +512,7 @@ export const proof = {
512
512
  withOffset: () => {
513
513
  const [_, [t, result]] = virtual({
514
514
  ...emptyState,
515
- root: { file: vec8(0xabcdn) },
515
+ root: { file: [vec8(0xabn), vec8(0xcdn)] },
516
516
  })(readBytes('file', 1, 1));
517
517
  if (t !== 'ok') {
518
518
  throw result;
@@ -524,7 +524,7 @@ export const proof = {
524
524
  oversizeChunk: () => {
525
525
  const [_, [t, result]] = virtual({
526
526
  ...emptyState,
527
- root: { file: vec8(0x2an) },
527
+ root: { file: [vec8(0x2an)] },
528
528
  })(readBytes('file', 0, Number(2n ** 32n)));
529
529
  if (t !== 'error') {
530
530
  throw result;