functionalscript 0.17.0 → 0.19.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 (82) hide show
  1. package/fs/asn.1/module.f.js +7 -8
  2. package/fs/asn.1/test.f.js +11 -12
  3. package/fs/ci/common/module.f.d.ts +4 -5
  4. package/fs/ci/common/module.f.js +4 -4
  5. package/fs/ci/config/module.f.d.ts +5 -5
  6. package/fs/ci/config/module.f.js +5 -5
  7. package/fs/ci/test.f.js +2 -4
  8. package/fs/crypto/sign/module.f.js +3 -3
  9. package/fs/dev/module.f.d.ts +3 -3
  10. package/fs/dev/module.f.js +12 -9
  11. package/fs/dev/tf/module.d.ts +1 -1
  12. package/fs/dev/tf/module.f.d.ts +76 -7
  13. package/fs/dev/tf/module.f.js +166 -87
  14. package/fs/dev/tf/module.js +3 -53
  15. package/fs/dev/tf/scenarios/all.d.ts +1 -0
  16. package/fs/dev/tf/scenarios/all.js +3 -0
  17. package/fs/dev/tf/scenarios/fail.fail.f.d.ts +1 -0
  18. package/fs/dev/tf/scenarios/fail.fail.f.js +1 -0
  19. package/fs/dev/tf/scenarios/return-value.pass.f.d.ts +1 -0
  20. package/fs/dev/tf/scenarios/return-value.pass.f.js +2 -0
  21. package/fs/dev/tf/scenarios/throw.pass.f.d.ts +6 -0
  22. package/fs/dev/tf/scenarios/throw.pass.f.js +3 -0
  23. package/fs/dev/tf/test.f.d.ts +21 -20
  24. package/fs/dev/tf/test.f.js +254 -31
  25. package/fs/djs/module.f.d.ts +2 -2
  26. package/fs/djs/module.f.js +8 -5
  27. package/fs/djs/test.f.js +5 -6
  28. package/fs/djs/tokenizer-new/test.f.js +126 -78
  29. package/fs/djs/transpiler/module.f.js +2 -2
  30. package/fs/djs/transpiler/test.f.js +11 -12
  31. package/fs/fjs/module.f.d.ts +2 -7
  32. package/fs/fjs/module.f.js +16 -22
  33. package/fs/fjs/module.js +2 -2
  34. package/fs/io/module.d.ts +3 -3
  35. package/fs/io/module.f.d.ts +9 -2
  36. package/fs/io/module.f.js +13 -3
  37. package/fs/io/module.js +68 -17
  38. package/fs/path/module.f.d.ts +6 -0
  39. package/fs/path/module.f.js +6 -0
  40. package/fs/path/test.f.d.ts +3 -5
  41. package/fs/path/test.f.js +67 -49
  42. package/fs/sul/id/module.f.js +3 -4
  43. package/fs/sul/level/literal/module.f.js +3 -3
  44. package/fs/text/sgr/module.f.d.ts +9 -1
  45. package/fs/text/sgr/module.f.js +16 -5
  46. package/fs/text/sgr/test.f.js +16 -1
  47. package/fs/types/bit_vec/module.f.d.ts +9 -4
  48. package/fs/types/bit_vec/module.f.js +7 -9
  49. package/fs/types/btree/remove/module.f.d.ts +1 -1
  50. package/fs/types/btree/remove/module.f.js +7 -2
  51. package/fs/types/btree/set/module.f.d.ts +1 -1
  52. package/fs/types/btree/set/module.f.js +7 -2
  53. package/fs/types/btree/types/module.f.d.ts +1 -0
  54. package/fs/types/btree/types/module.f.js +1 -1
  55. package/fs/types/effects/node/module.f.d.ts +30 -17
  56. package/fs/types/effects/node/module.f.js +21 -2
  57. package/fs/types/effects/node/test.f.js +8 -5
  58. package/fs/types/effects/node/virtual/module.f.d.ts +12 -2
  59. package/fs/types/effects/node/virtual/module.f.js +31 -17
  60. package/fs/types/function/compare/module.f.d.ts +12 -0
  61. package/fs/types/function/compare/module.f.js +33 -0
  62. package/fs/types/function/operator/test.f.d.ts +10 -0
  63. package/fs/types/function/operator/test.f.js +81 -0
  64. package/fs/types/nullable/test.f.js +10 -1
  65. package/fs/types/range_map/module.f.js +3 -18
  66. package/fs/types/result/module.f.d.ts +4 -0
  67. package/fs/types/result/module.f.js +4 -0
  68. package/fs/types/result/test.f.d.ts +2 -4
  69. package/fs/types/result/test.f.js +24 -16
  70. package/fs/types/rtti/common/module.f.d.ts +10 -1
  71. package/fs/types/rtti/common/module.f.js +7 -2
  72. package/fs/types/rtti/parse/module.f.js +35 -46
  73. package/fs/types/rtti/validate/module.f.js +9 -12
  74. package/fs/types/sorted_list/module.f.d.ts +1 -2
  75. package/fs/types/sorted_list/module.f.js +8 -21
  76. package/fs/types/sorted_set/module.f.d.ts +1 -3
  77. package/fs/types/ts/test.f.d.ts +18 -0
  78. package/fs/types/ts/test.f.js +111 -0
  79. package/fs/types/uint8array/module.f.js +7 -1
  80. package/fs/types/uint8array/test.f.d.ts +1 -0
  81. package/fs/types/uint8array/test.f.js +5 -1
  82. package/package.json +4 -4
@@ -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,18 @@
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;
13
+ export type Entity = Vec | Dir | JsModule;
4
14
  export type Dir = {
5
- readonly [name in string]?: Dir | Vec;
15
+ readonly [name in string]?: Entity;
6
16
  };
7
17
  export type State = {
8
18
  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,25 @@ 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
+ test: todo,
171
+ write: (state, stream, data) => {
172
+ const s = utf8ToString(data);
173
+ return [{ ...state, [stream]: `${state[stream]}${s}` }, undefined];
160
174
  },
161
175
  };
162
176
  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;
@@ -0,0 +1,81 @@
1
+ import { join, concat, logicalNot, strictEqual, addition, min, max, increment, foldToScan, reduceToScan, } from "./module.f.js";
2
+ export const joinTest = () => {
3
+ const result = join(', ')('world')('hello');
4
+ if (result !== 'hello, world') {
5
+ throw result;
6
+ }
7
+ };
8
+ export const concatTest = () => {
9
+ const result = concat('world')('hello');
10
+ if (result !== 'helloworld') {
11
+ throw result;
12
+ }
13
+ };
14
+ export const logicalNotTest = () => {
15
+ if (logicalNot(true) !== false) {
16
+ throw 'expected false';
17
+ }
18
+ if (logicalNot(false) !== true) {
19
+ throw 'expected true';
20
+ }
21
+ };
22
+ export const strictEqualTest = () => {
23
+ if (!strictEqual(1)(1)) {
24
+ throw 'expected true';
25
+ }
26
+ if (strictEqual(1)(2)) {
27
+ throw 'expected false';
28
+ }
29
+ };
30
+ export const additionTest = () => {
31
+ const result = addition(3)(4);
32
+ if (result !== 7) {
33
+ throw result;
34
+ }
35
+ };
36
+ export const minTest = () => {
37
+ if (min(3)(5) !== 3) {
38
+ throw 'min(3)(5)';
39
+ }
40
+ if (min(7)(2) !== 2) {
41
+ throw 'min(7)(2)';
42
+ }
43
+ };
44
+ export const maxTest = () => {
45
+ if (max(3)(5) !== 5) {
46
+ throw 'max(3)(5)';
47
+ }
48
+ if (max(7)(2) !== 7) {
49
+ throw 'max(7)(2)';
50
+ }
51
+ };
52
+ export const incrementTest = () => {
53
+ if (increment(4) !== 5) {
54
+ throw 'increment(4)';
55
+ }
56
+ if (increment(0) !== 1) {
57
+ throw 'increment(0)';
58
+ }
59
+ };
60
+ export const foldToScanTest = () => {
61
+ const scan = foldToScan(addition)(0);
62
+ const [v1, scan2] = scan(3);
63
+ if (v1 !== 3) {
64
+ throw v1;
65
+ }
66
+ const [v2] = scan2(4);
67
+ if (v2 !== 7) {
68
+ throw v2;
69
+ }
70
+ };
71
+ export const reduceToScanTest = () => {
72
+ const scan = reduceToScan(addition);
73
+ const [v0, scan2] = scan(10);
74
+ if (v0 !== 10) {
75
+ throw v0;
76
+ }
77
+ const [v1] = scan2(5);
78
+ if (v1 !== 15) {
79
+ throw v1;
80
+ }
81
+ };
@@ -1,4 +1,4 @@
1
- import { map, toOption } from "./module.f.js";
1
+ import { map, match, toOption } from "./module.f.js";
2
2
  export default [
3
3
  () => {
4
4
  const optionSq = map((v) => v * v);
@@ -20,5 +20,14 @@ export default [
20
20
  if (opt2.length !== 0) {
21
21
  throw opt2;
22
22
  }
23
+ },
24
+ () => {
25
+ const double = match((v) => v * 2)(() => -1);
26
+ if (double(3) !== 6) {
27
+ throw double(3);
28
+ }
29
+ if (double(null) !== -1) {
30
+ throw double(null);
31
+ }
23
32
  }
24
33
  ];
@@ -37,6 +37,7 @@
37
37
  import { genericMerge } from "../sorted_list/module.f.js";
38
38
  import { next } from "../list/module.f.js";
39
39
  import { cmp } from "../number/module.f.js";
40
+ import { bsearch } from "../function/compare/module.f.js";
40
41
  const reduceOp = ({ union, equal }) => state => ([aItem, aMax]) => ([bItem, bMax]) => {
41
42
  const sign = cmp(aMax)(bMax);
42
43
  const min = sign === 1 ? bMax : aMax;
@@ -59,24 +60,8 @@ const tailReduce = equal => state => tail => {
59
60
  };
60
61
  export const merge = op => genericMerge({ reduceOp: reduceOp(op), tailReduce: tailReduce(op.equal) })(null);
61
62
  export const get = def => value => rm => {
62
- const len = rm.length;
63
- let b = 0;
64
- let e = len - 1;
65
- while (true) {
66
- if (b >= len) {
67
- return def;
68
- }
69
- if (e - b < 0) {
70
- return rm[b][0];
71
- }
72
- const mid = b + (e - b >> 1);
73
- if (value <= rm[mid][1]) {
74
- e = mid - 1;
75
- }
76
- else {
77
- b = mid + 1;
78
- }
79
- }
63
+ const pos = bsearch(rm.length)(mid => value <= rm[mid][1] ? -1 : 1);
64
+ return pos < rm.length ? rm[pos][0] : def;
80
65
  };
81
66
  export const fromRange = def => ([a, b]) => v => [[def, a - 1], [v, b]];
82
67
  /**
@@ -51,3 +51,7 @@ export declare const error: <E>(e: E) => Error<E>;
51
51
  * @returns The value if the result is successful. Otherwise, throws the error.
52
52
  */
53
53
  export declare const unwrap: <T, E>([kind, v]: Result<T, E>) => T;
54
+ /**
55
+ * Swaps the `ok` and `error` cases of a result.
56
+ */
57
+ export declare const invert: <T, E>([k, v]: Result<T, E>) => Result<E, T>;
@@ -44,3 +44,7 @@ export const unwrap = ([kind, v]) => {
44
44
  }
45
45
  return v;
46
46
  };
47
+ /**
48
+ * Swaps the `ok` and `error` cases of a result.
49
+ */
50
+ export const invert = ([k, v]) => k === 'ok' ? error(v) : ok(v);
@@ -1,4 +1,2 @@
1
- declare const _default: {
2
- example: () => void;
3
- };
4
- export default _default;
1
+ export declare const example: () => void;
2
+ export declare const invertTest: () => void;
@@ -1,18 +1,26 @@
1
- import { error, ok, unwrap } from "./module.f.js";
2
- export default {
3
- example: () => {
4
- const success = ok(42);
5
- const failure = error('Something went wrong');
6
- if (unwrap(success) !== 42) {
7
- throw 'error';
8
- }
9
- const [kind, v] = failure;
10
- if (kind !== 'error') {
11
- throw 'error';
12
- }
13
- // `v` is inferred as `string` here
14
- if (v !== 'Something went wrong') {
15
- throw 'error';
16
- }
1
+ import { error, ok, unwrap, invert } from "./module.f.js";
2
+ export const example = () => {
3
+ const success = ok(42);
4
+ const failure = error('Something went wrong');
5
+ if (unwrap(success) !== 42) {
6
+ throw 'error';
7
+ }
8
+ const [kind, v] = failure;
9
+ if (kind !== 'error') {
10
+ throw 'error';
11
+ }
12
+ // `v` is inferred as `string` here
13
+ if (v !== 'Something went wrong') {
14
+ throw 'error';
15
+ }
16
+ };
17
+ export const invertTest = () => {
18
+ const [k0, v0] = invert(ok(42));
19
+ if (k0 !== 'error' || v0 !== 42) {
20
+ throw [k0, v0];
21
+ }
22
+ const [k1, v1] = invert(error('oops'));
23
+ if (k1 !== 'ok' || v1 !== 'oops') {
24
+ throw [k1, v1];
17
25
  }
18
26
  };
@@ -23,9 +23,10 @@
23
23
  * @module
24
24
  */
25
25
  import type { Primitive, Unknown } from '../../../djs/module.f.ts';
26
- import { type Info0, type Primitive0, type Struct, type Tuple, type Type } from '../module.f.ts';
26
+ import { type Info0, type Primitive0, type Struct, type Tag1, type Tuple, type Type } from '../module.f.ts';
27
27
  import { type Error, type Result as CommonResult } from '../../result/module.f.ts';
28
28
  import type { Ts } from '../ts/module.f.ts';
29
+ import { type ReadonlyRecord } from '../../object/module.f.ts';
29
30
  /** A path to a sub-value within the validated structure. Each step is an object key or stringified array index. */
30
31
  export type Path = readonly string[];
31
32
  /** Detailed validation failure: the offending `path` plus a short `message`. */
@@ -65,6 +66,14 @@ export type Visitor<R> = {
65
66
  readonly primitive0: (tag: Primitive0) => R;
66
67
  readonly unknown: () => R;
67
68
  };
69
+ /** Type guard narrowing `Unknown` to a specific container type `C`. */
70
+ export type IsContainer<C extends Unknown> = (value: Unknown) => value is C;
71
+ /** Maps a `Tag1` to its runtime container type. */
72
+ export type Container<K extends Tag1> = K extends 'array' ? ReadonlyArray<Unknown> : ReadonlyRecord<string, Unknown>;
73
+ /** `IsContainer` guard for arrays, shared by `validate` and `parse`. */
74
+ export declare const isArray: IsContainer<ReadonlyArray<Unknown>>;
75
+ /** `IsContainer` guard for records/structs, shared by `validate` and `parse`. */
76
+ export declare const isObject: IsContainer<ReadonlyRecord<string, Unknown>>;
68
77
  /**
69
78
  * Visits a schema `Type` by dispatching to the matching handler in `v`.
70
79
  *
@@ -1,6 +1,7 @@
1
1
  import {} from "../module.f.js";
2
2
  import { error, ok } from "../../result/module.f.js";
3
- import { isArray } from "../../array/module.f.js";
3
+ import { isArray as commonIsArray } from "../../array/module.f.js";
4
+ import { isObject as commonIsObject } from "../../object/module.f.js";
4
5
  /** Builds an error result with empty path and the given message. */
5
6
  export const verror = (message) => error({ path: [], message });
6
7
  /** Prepends `key` to the error's path, used to build the path bottom-up. */
@@ -18,8 +19,12 @@ export const constPrimitiveValidate = (rtti) => value => Object.is(rtti, value)
18
19
  ? ok(value)
19
20
  : verror('unexpected value');
20
21
  const visitConst = (v) => (c) => typeof c === 'object' && c !== null
21
- ? (isArray(c) ? v.tuple(c) : v.struct(c))
22
+ ? (commonIsArray(c) ? v.tuple(c) : v.struct(c))
22
23
  : v.constPrimitive(c);
24
+ /** `IsContainer` guard for arrays, shared by `validate` and `parse`. */
25
+ export const isArray = value => commonIsArray(value);
26
+ /** `IsContainer` guard for records/structs, shared by `validate` and `parse`. */
27
+ export const isObject = value => commonIsObject(value);
23
28
  /**
24
29
  * Visits a schema `Type` by dispatching to the matching handler in `v`.
25
30
  *
@@ -1,71 +1,60 @@
1
1
  import {} from "../module.f.js";
2
2
  import { ok } from "../../result/module.f.js";
3
- import { isArray as commonIsArray } from "../../array/module.f.js";
4
- import { isObject as commonIsObject } from "../../object/module.f.js";
3
+ import {} from "../../object/module.f.js";
5
4
  import { find, map as listMap } from "../../list/module.f.js";
6
- import { constPrimitiveValidate, prependPath, primitive0Validate, verror, visit, } from "../common/module.f.js";
5
+ import { constPrimitiveValidate, isArray, isObject, prependPath, primitive0Validate, verror, visit, } from "../common/module.f.js";
7
6
  export {} from "../common/module.f.js";
8
- const indexedFirstError = (results) => {
9
- // TODO: findIndex breaks type inference,
10
- // we should replace it with something else.
11
- const i = results.findIndex(r => r[0] === 'error');
12
- return i < 0 ? null : [i, results[i]];
13
- };
7
+ const { entries } = Object;
14
8
  const keyedFirstError = (results) => {
15
9
  const e = results.find(([, r]) => r[0] === 'error');
16
10
  return e === undefined ? null : [e[0], e[1]];
17
11
  };
18
- const arrayParse = (item) => value => {
19
- if (!commonIsArray(value)) {
20
- return verror('unexpected value');
21
- }
22
- if (value.length === 0) {
23
- return ok([]);
24
- }
25
- // Note: we shouldn't instantiate `itemParse` until we know the array is non-empty.
26
- // Otherwise, we can get infinite recursion on empty arrays for recursive schemas.
27
- const itemParse = parse(item);
28
- const results = value.map(itemParse);
29
- const err = indexedFirstError(results);
30
- return (err === null
31
- ? ok(results.map(r => r[1]))
32
- : prependPath(String(err[0]), err[1]));
33
- };
34
- const recordParse = (item) => value => {
35
- if (!commonIsObject(value)) {
12
+ const arrayRebuild = entries => entries.map(([, v]) => v);
13
+ const recordRebuild = entries => Object.fromEntries(entries);
14
+ /** Drops the `'ok'` tag from each result, yielding the rebuild's `[key, value]` entries. */
15
+ const okEntries = (results) => results.map(([k, r]) => [k, r[1]]);
16
+ /**
17
+ * Builds a parser for `array` or `record` schemas. Mirrors `validate`'s
18
+ * `containerValidate`, but rebuilds a fresh container from each item's parsed
19
+ * result instead of returning the value unchanged. The inner item parser is
20
+ * instantiated lazily (only when the container is non-empty) so recursive
21
+ * schemas don't recurse forever on empty containers.
22
+ */
23
+ const containerParse = (isContainer, rebuild) => (item) => value => {
24
+ if (!isContainer(value)) {
36
25
  return verror('unexpected value');
37
26
  }
38
- const entries = Object.entries(value);
39
- if (entries.length === 0) {
40
- return ok({});
27
+ const e = entries(value);
28
+ if (e.length === 0) {
29
+ return ok(rebuild([]));
41
30
  }
42
31
  const itemParse = parse(item);
43
- const results = entries.map(([k, v]) => [k, itemParse(v)]);
32
+ const results = e.map(([k, v]) => [k, itemParse(v)]);
44
33
  const err = keyedFirstError(results);
45
34
  return (err === null
46
- ? ok(Object.fromEntries(results.map(([k, r]) => [k, r[1]])))
35
+ ? ok(rebuild(okEntries(results)))
47
36
  : prependPath(err[0], err[1]));
48
37
  };
49
- const tupleParse = (rtti) => value => {
50
- if (!commonIsArray(value)) {
51
- return verror('unexpected value');
52
- }
53
- const results = rtti.map((t, i) => parse(t)(value[i]));
54
- const err = indexedFirstError(results);
55
- return (err === null
56
- ? ok(results.map(r => r[1]))
57
- : prependPath(String(err[0]), err[1]));
58
- };
59
- const structParse = (rtti) => value => {
60
- if (!commonIsObject(value)) {
38
+ const arrayParse = containerParse(isArray, arrayRebuild);
39
+ const recordParse = containerParse(isObject, recordRebuild);
40
+ /**
41
+ * Builds a parser for `Tuple` or `Struct` const schemas. Mirrors `validate`'s
42
+ * `constContainerValidate`: it iterates the schema's entries (so extra tuple
43
+ * elements and undeclared struct keys are dropped) and rebuilds the result
44
+ * from each parsed item.
45
+ */
46
+ const constContainerParse = (isContainer, getItem, rebuild) => (rtti) => value => {
47
+ if (!isContainer(value)) {
61
48
  return verror('unexpected value');
62
49
  }
63
- const results = Object.entries(rtti).map(([k, t]) => [k, parse(t)(value[k])]);
50
+ const results = entries(rtti).map(([k, t]) => [k, parse(t)(getItem(value, k))]);
64
51
  const err = keyedFirstError(results);
65
52
  return (err === null
66
- ? ok(Object.fromEntries(results.map(([k, r]) => [k, r[1]])))
53
+ ? ok(rebuild(okEntries(results)))
67
54
  : prependPath(err[0], err[1]));
68
55
  };
56
+ const tupleParse = constContainerParse(isArray, (value, k) => value[Number(k)], arrayRebuild);
57
+ const structParse = constContainerParse(isObject, (value, k) => value[k], recordRebuild);
69
58
  const findFirst = find(verror('no match'))((k) => k[0] === 'ok');
70
59
  const orParse = (rtti) => value => findFirst(listMap(t => parse(t)(value))(rtti));
71
60
  /**