functionalscript 0.14.7 → 0.15.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.
- package/fs/ci/bun/module.f.js +1 -0
- package/fs/ci/config/module.f.d.ts +2 -2
- package/fs/ci/config/module.f.js +2 -2
- package/fs/dev/module.f.d.ts +1 -0
- package/fs/dev/tf/module.f.js +10 -0
- package/fs/dev/tf/test.f.d.ts +5 -0
- package/fs/dev/tf/test.f.js +14 -0
- package/fs/io/module.f.js +6 -6
- package/fs/types/effects/mock/module.f.d.ts +1 -1
- package/fs/types/effects/mock/module.f.js +1 -1
- package/fs/types/effects/module.f.d.ts +5 -7
- package/fs/types/effects/module.f.js +1 -2
- package/fs/types/effects/module.js +1 -1
- package/fs/types/effects/node/module.f.d.ts +18 -22
- package/fs/types/effects/node/module.f.js +7 -7
- package/fs/types/effects/node/test.f.js +1 -1
- package/fs/types/effects/node/virtual/module.f.js +5 -5
- package/fs/types/rtti/module.f.d.ts +4 -8
- package/fs/types/rtti/module.f.js +5 -71
- package/fs/types/rtti/test.f.d.ts +0 -28
- package/fs/types/rtti/test.f.js +0 -107
- package/package.json +2 -2
package/fs/ci/bun/module.f.js
CHANGED
|
@@ -18,8 +18,8 @@ export declare const playwright = "1.60.0";
|
|
|
18
18
|
export declare const rust = "1.95.0";
|
|
19
19
|
export declare const node: {
|
|
20
20
|
readonly default: "26.1.0";
|
|
21
|
-
readonly others: readonly ["22.22.
|
|
21
|
+
readonly others: readonly ["22.22.3", "24.15.0"];
|
|
22
22
|
};
|
|
23
23
|
export declare const wasmtime = "44.0.1";
|
|
24
24
|
export declare const wasmer = "7.1.0";
|
|
25
|
-
export declare const tsgo = "7.0.0-dev.
|
|
25
|
+
export declare const tsgo = "7.0.0-dev.20260515.1";
|
package/fs/ci/config/module.f.js
CHANGED
|
@@ -24,11 +24,11 @@ export const rust = '1.95.0';
|
|
|
24
24
|
// https://nodejs.org/en/download
|
|
25
25
|
export const node = {
|
|
26
26
|
default: '26.1.0',
|
|
27
|
-
others: ['22.22.
|
|
27
|
+
others: ['22.22.3', '24.15.0'],
|
|
28
28
|
};
|
|
29
29
|
// https://github.com/bytecodealliance/wasmtime/releases
|
|
30
30
|
export const wasmtime = '44.0.1';
|
|
31
31
|
// https://github.com/wasmerio/wasmer/releases
|
|
32
32
|
export const wasmer = '7.1.0';
|
|
33
33
|
// https://www.npmjs.com/package/@typescript/native-preview?activeTab=versions
|
|
34
|
-
export const tsgo = '7.0.0-dev.
|
|
34
|
+
export const tsgo = '7.0.0-dev.20260515.1';
|
package/fs/dev/module.f.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export declare const assert: (v: boolean, msg?: unknown) => asserts v;
|
|
|
11
11
|
export declare const assertEq: <T>(a: T, b: T) => void;
|
|
12
12
|
export type Module = {
|
|
13
13
|
readonly default?: unknown;
|
|
14
|
+
readonly [k: string]: unknown;
|
|
14
15
|
};
|
|
15
16
|
export type ModuleMap = {
|
|
16
17
|
readonly [k in string]: Module;
|
package/fs/dev/tf/module.f.js
CHANGED
|
@@ -93,6 +93,16 @@ export const test = (input) => {
|
|
|
93
93
|
if (isTest(k)) {
|
|
94
94
|
log(`testing ${k}`);
|
|
95
95
|
[ts, state] = test('| ')(false)(v.default)([ts, state]);
|
|
96
|
+
// Non-default exports are walked as a sibling test group so
|
|
97
|
+
// a test file can spread its tests across multiple named
|
|
98
|
+
// exports (see issue 27 in `issues/README.md`). Skip exports
|
|
99
|
+
// that parseTestSet would treat as empty (constants, types,
|
|
100
|
+
// non-test helpers) to avoid noisy empty entries in output.
|
|
101
|
+
const others = Object.fromEntries(Object.entries(v).filter(([key, val]) => key !== 'default' && ((typeof val === 'function' && val.length === 0) ||
|
|
102
|
+
(typeof val === 'object' && val !== null))));
|
|
103
|
+
if (Object.keys(others).length !== 0) {
|
|
104
|
+
[ts, state] = test('| ')(false)(others)([ts, state]);
|
|
105
|
+
}
|
|
96
106
|
}
|
|
97
107
|
return [ts, state];
|
|
98
108
|
};
|
package/fs/dev/tf/test.f.d.ts
CHANGED
package/fs/dev/tf/test.f.js
CHANGED
|
@@ -20,3 +20,17 @@ export default {
|
|
|
20
20
|
},
|
|
21
21
|
},
|
|
22
22
|
};
|
|
23
|
+
// Non-default exports are walked as a sibling test group (see issue 27).
|
|
24
|
+
export const namedExportTest = () => {
|
|
25
|
+
if (2 + 2 !== 4) {
|
|
26
|
+
throw 'arithmetic broken';
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
export const namedExportGroup = {
|
|
30
|
+
nested: () => {
|
|
31
|
+
if ('a' + 'b' !== 'ab') {
|
|
32
|
+
throw 'string concat broken';
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
throw: () => { throw 'expected to throw'; },
|
|
36
|
+
};
|
package/fs/io/module.f.js
CHANGED
|
@@ -37,7 +37,7 @@ const collect = async (v) => {
|
|
|
37
37
|
};
|
|
38
38
|
export const fromIo = ({ console: { error, log }, fs: { promises: { mkdir, readFile, readdir, writeFile, rm } }, fetch, http: { createServer }, childProcess, }) => {
|
|
39
39
|
const result = asyncRun({
|
|
40
|
-
all: async (effects) => await Promise.all(effects.map(result)),
|
|
40
|
+
all: async (...effects) => await Promise.all(effects.map(result)),
|
|
41
41
|
error: async (message) => error(message),
|
|
42
42
|
log: async (message) => log(message),
|
|
43
43
|
fetch: async (url) => tc(async () => {
|
|
@@ -47,13 +47,13 @@ export const fromIo = ({ console: { error, log }, fs: { promises: { mkdir, readF
|
|
|
47
47
|
}
|
|
48
48
|
return toVec(new Uint8Array(await response.arrayBuffer()));
|
|
49
49
|
}),
|
|
50
|
-
mkdir:
|
|
50
|
+
mkdir: (...p) => tc(async () => { await mkdir(...p); }),
|
|
51
51
|
readFile: path => tc(async () => toVec(await readFile(path))),
|
|
52
|
-
readdir: (
|
|
52
|
+
readdir: (path, r) => tc(async () => (await readdir(path, { ...r, withFileTypes: true }))
|
|
53
53
|
.map(v => ({ name: v.name, parentPath: normalize(v.parentPath), isFile: v.isFile() }))),
|
|
54
|
-
writeFile: (
|
|
54
|
+
writeFile: (path, data) => tc(() => writeFile(path, fromVec(data))),
|
|
55
55
|
rm: path => tc(() => rm(path)),
|
|
56
|
-
exec: (
|
|
56
|
+
exec: (command, stdin) => new Promise(resolve => {
|
|
57
57
|
const child = childProcess.exec(command, (e, stdout, stderr) => resolve(e !== null ? ['error', e] : ok({ stdout, stderr })));
|
|
58
58
|
child.stdin?.end(stdin);
|
|
59
59
|
}),
|
|
@@ -75,7 +75,7 @@ export const fromIo = ({ console: { error, log }, fs: { promises: { mkdir, readF
|
|
|
75
75
|
const server = asNominal(createServer(nodeRl));
|
|
76
76
|
return server;
|
|
77
77
|
},
|
|
78
|
-
listen: async (
|
|
78
|
+
listen: async (server, port) => {
|
|
79
79
|
const s = asBase(server);
|
|
80
80
|
s.listen(port);
|
|
81
81
|
},
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { Effect, Operation, Pr } from "../module.f.ts";
|
|
7
7
|
export type MemOperationMap<O extends Operation, S> = {
|
|
8
|
-
readonly [K in O[0]]: (state: S, payload: Pr<O, K>[0]) => readonly [S, Pr<O, K>[1]];
|
|
8
|
+
readonly [K in O[0]]: (state: S, ...payload: Pr<O, K>[0]) => readonly [S, Pr<O, K>[1]];
|
|
9
9
|
};
|
|
10
10
|
export type RunInstance<O extends Operation, S> = (state: S) => <O1 extends O, T>(effect: Effect<O1, T>) => readonly [S, T];
|
|
11
11
|
export declare const run: <O extends Operation, S>(o: MemOperationMap<O, S>) => RunInstance<O, S>;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
|
-
export type Operation = readonly [string, (_: never) => unknown];
|
|
6
|
+
export type Operation = readonly [string, (..._: readonly never[]) => unknown];
|
|
7
7
|
export type Effect<O extends Operation, T> = {
|
|
8
8
|
value: Value<O, T>;
|
|
9
9
|
step: <Q extends Operation, R>(f: (p: T) => Effect<Q, R>) => Effect<O | Q, R>;
|
|
@@ -11,20 +11,18 @@ export type Effect<O extends Operation, T> = {
|
|
|
11
11
|
export type Value<O extends Operation, T> = Pure<T> | Do<O, T>;
|
|
12
12
|
export type Pure<T> = readonly [T];
|
|
13
13
|
export type DoKPR<O extends Operation, T, K extends string, PR extends readonly [unknown, unknown]> = readonly [K, PR[0], (_: PR[1]) => Effect<O, T>];
|
|
14
|
-
export type Pr<O extends Operation, K extends O[0]> = O extends readonly [K, (
|
|
14
|
+
export type Pr<O extends Operation, K extends O[0]> = O extends readonly [K, (...args: infer P) => infer R] ? readonly [P, R] : never;
|
|
15
15
|
export type DoK<O extends Operation, T, K extends O[0]> = DoKPR<O, T, K, Pr<O, K>>;
|
|
16
16
|
export type Do<O extends Operation, T> = DoK<O, T, O[0]>;
|
|
17
17
|
export declare const pure: <T>(v: T) => Effect<never, T>;
|
|
18
18
|
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>;
|
|
19
19
|
export type Param<O extends Operation> = F<O>[0];
|
|
20
20
|
export type Return<O extends Operation> = F<O>[1];
|
|
21
|
-
export declare const do_: <O extends Operation>(cmd: O[0]) => (param: Param<O>) => Effect<O, Return<O>>;
|
|
22
|
-
export declare const doRest: <O extends Operation>(cmd: O[0]) => (...param: Param<O>) => Effect<O, Return<O>>;
|
|
21
|
+
export declare const do_: <O extends Operation>(cmd: O[0]) => (...param: Param<O>) => Effect<O, Return<O>>;
|
|
23
22
|
export declare const begin: Effect<never, void>;
|
|
24
23
|
export type ToAsyncOperationMap<O extends Operation> = {
|
|
25
|
-
readonly [K in O[0]]: (payload: Pr<O, K>[0]) => Promise<Pr<O, K>[1]>;
|
|
24
|
+
readonly [K in O[0]]: (...payload: Pr<O, K>[0]) => Promise<Pr<O, K>[1]>;
|
|
26
25
|
};
|
|
27
26
|
export type F<O extends Operation> = Pr<O, O[0]>;
|
|
28
|
-
export type Func<O extends Operation> = (_: Param<O>) => Effect<O, Return<O>>;
|
|
29
|
-
export type RestFunc<O extends Operation> = (..._: Param<O>) => Effect<O, Return<O>>;
|
|
27
|
+
export type Func<O extends Operation> = (..._: Param<O>) => Effect<O, Return<O>>;
|
|
30
28
|
export type ListEffect<O extends Operation, T> = Effect<O, readonly [T, ListEffect<O, T>] | undefined>;
|
|
@@ -11,6 +11,5 @@ export const doFull = (cmd, param, cont) => ({
|
|
|
11
11
|
value: [cmd, param, cont],
|
|
12
12
|
step: (f) => doFull(cmd, param, x => cont(x).step(f)),
|
|
13
13
|
});
|
|
14
|
-
export const do_ = (cmd) => (param) => doFull(cmd, param, pure);
|
|
15
|
-
export const doRest = (cmd) => (...param) => do_(cmd)(param);
|
|
14
|
+
export const do_ = (cmd) => (...param) => doFull(cmd, param, pure);
|
|
16
15
|
export const begin = pure(undefined);
|
|
@@ -6,7 +6,7 @@ export const asyncRun = (map) => async (effect) => {
|
|
|
6
6
|
}
|
|
7
7
|
const [command, payload, continuation] = value;
|
|
8
8
|
const operation = map[command];
|
|
9
|
-
const result = await operation(payload);
|
|
9
|
+
const result = await operation(...payload);
|
|
10
10
|
effect = continuation(result);
|
|
11
11
|
}
|
|
12
12
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Vec } from '../../bit_vec/module.f.ts';
|
|
2
2
|
import type { Nominal } from '../../nominal/module.f.ts';
|
|
3
3
|
import type { Result } from '../../result/module.f.ts';
|
|
4
|
-
import { type Effect, type Func, type Operation, type
|
|
4
|
+
import { type Effect, type Func, type Operation, type ToAsyncOperationMap } from '../module.f.ts';
|
|
5
5
|
export type IoResult<T> = Result<T, unknown>;
|
|
6
|
-
export type All = ['all', <T>(
|
|
6
|
+
export type All = ['all', <T>(...effects: Effect<never, T>[]) => readonly T[]];
|
|
7
7
|
/**
|
|
8
8
|
* To run the operation `O` should be known by the runner/engine.
|
|
9
9
|
* This is the reason why we merge `O` with `All` in the resulted `Effect`.
|
|
@@ -13,15 +13,14 @@ export type All = ['all', <T>(_: readonly Effect<never, T>[]) => readonly T[]];
|
|
|
13
13
|
*/
|
|
14
14
|
export declare const all: <O extends Operation, T>(...a: readonly Effect<O, T>[]) => Effect<O | All, readonly T[]>;
|
|
15
15
|
export declare const both: <O0 extends Operation, T0>(a: Effect<O0, T0>) => <O1 extends Operation, T1>(b: Effect<O1, T1>) => Effect<O0 | O1 | All, readonly [T0, T1]>;
|
|
16
|
-
export type Fetch = ['fetch', (
|
|
16
|
+
export type Fetch = ['fetch', (url: string) => IoResult<Vec>];
|
|
17
17
|
export declare const fetch: Func<Fetch>;
|
|
18
18
|
export type MakeDirectoryOptions = {
|
|
19
19
|
readonly recursive: true;
|
|
20
20
|
};
|
|
21
|
-
export type
|
|
22
|
-
export
|
|
23
|
-
export
|
|
24
|
-
export type ReadFile = readonly ['readFile', (_: string) => IoResult<Vec>];
|
|
21
|
+
export type Mkdir = readonly ['mkdir', (path: string, options?: MakeDirectoryOptions) => IoResult<void>];
|
|
22
|
+
export declare const mkdir: Func<Mkdir>;
|
|
23
|
+
export type ReadFile = readonly ['readFile', (path: string) => IoResult<Vec>];
|
|
25
24
|
export declare const readFile: Func<ReadFile>;
|
|
26
25
|
/**
|
|
27
26
|
* Represents a directory entry (file or directory) in the filesystem
|
|
@@ -35,25 +34,22 @@ export type Dirent = {
|
|
|
35
34
|
export type ReaddirOptions = {
|
|
36
35
|
readonly recursive?: true;
|
|
37
36
|
};
|
|
38
|
-
export type
|
|
39
|
-
export
|
|
40
|
-
export
|
|
41
|
-
export
|
|
42
|
-
export type
|
|
43
|
-
export declare const writeFile: RestFunc<WriteFile>;
|
|
44
|
-
export type Rm = readonly ['rm', (_: string) => IoResult<void>];
|
|
37
|
+
export type Readdir = readonly ['readdir', (path: string, options: ReaddirOptions) => IoResult<readonly Dirent[]>];
|
|
38
|
+
export declare const readdir: Func<Readdir>;
|
|
39
|
+
export type WriteFile = readonly ['writeFile', (path: string, data: Vec) => IoResult<void>];
|
|
40
|
+
export declare const writeFile: Func<WriteFile>;
|
|
41
|
+
export type Rm = readonly ['rm', (path: string) => IoResult<void>];
|
|
45
42
|
export declare const rm: Func<Rm>;
|
|
46
43
|
export type ExecResult = {
|
|
47
44
|
readonly stdout: string;
|
|
48
45
|
readonly stderr: string;
|
|
49
46
|
};
|
|
50
|
-
export type
|
|
51
|
-
export
|
|
52
|
-
export declare const exec: RestFunc<Exec>;
|
|
47
|
+
export type Exec = readonly ['exec', (command: string, stdin?: string) => IoResult<ExecResult>];
|
|
48
|
+
export declare const exec: Func<Exec>;
|
|
53
49
|
export type Fs = Mkdir | ReadFile | Readdir | WriteFile | Rm | Exec;
|
|
54
|
-
export type Error = ['error', (
|
|
50
|
+
export type Error = ['error', (message: string) => void];
|
|
55
51
|
export declare const error: Func<Error>;
|
|
56
|
-
export type Log = ['log', (
|
|
52
|
+
export type Log = ['log', (message: string) => void];
|
|
57
53
|
export declare const log: Func<Log>;
|
|
58
54
|
export type Console = Log | Error;
|
|
59
55
|
export type Server = Nominal<'server', `160855c4f69310fece3273c1853ac32de43dee1eb41bf59d821917f8eebe9272`, unknown>;
|
|
@@ -72,10 +68,10 @@ export type ServerResponse = {
|
|
|
72
68
|
readonly body: Vec;
|
|
73
69
|
};
|
|
74
70
|
export type RequestListener<O extends Operation> = (_: IncomingMessage) => Effect<O, ServerResponse>;
|
|
75
|
-
export type CreateServer = ['createServer', (
|
|
71
|
+
export type CreateServer = ['createServer', (listener: RequestListener<Operation>) => Server];
|
|
76
72
|
export declare const createServer: <O extends Operation>(listener: RequestListener<O>) => Effect<O | CreateServer, Server>;
|
|
77
|
-
export type Listen = ['listen', (
|
|
78
|
-
export declare const listen:
|
|
73
|
+
export type Listen = ['listen', (server: Server, port: number) => void];
|
|
74
|
+
export declare const listen: Func<Listen>;
|
|
79
75
|
export type Http = CreateServer | Listen;
|
|
80
76
|
export type Forever = ['forever', () => never];
|
|
81
77
|
export declare const forever: Func<Forever>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { do_ } from "../module.f.js";
|
|
2
2
|
const doAll = do_('all');
|
|
3
3
|
/**
|
|
4
4
|
* To run the operation `O` should be known by the runner/engine.
|
|
@@ -7,17 +7,17 @@ const doAll = do_('all');
|
|
|
7
7
|
* @param a
|
|
8
8
|
* @returns
|
|
9
9
|
*/
|
|
10
|
-
export const all = (...a) => doAll(a);
|
|
10
|
+
export const all = (...a) => doAll(...a);
|
|
11
11
|
export const both = (a) => (b) => all(a, b);
|
|
12
12
|
export const fetch = do_('fetch');
|
|
13
|
-
export const mkdir =
|
|
13
|
+
export const mkdir = do_('mkdir');
|
|
14
14
|
export const readFile = do_('readFile');
|
|
15
|
-
export const readdir =
|
|
16
|
-
export const writeFile =
|
|
15
|
+
export const readdir = do_('readdir');
|
|
16
|
+
export const writeFile = do_('writeFile');
|
|
17
17
|
export const rm = do_('rm');
|
|
18
|
-
export const exec =
|
|
18
|
+
export const exec = do_('exec');
|
|
19
19
|
export const error = do_('error');
|
|
20
20
|
export const log = do_('log');
|
|
21
21
|
export const createServer = do_('createServer');
|
|
22
|
-
export const listen =
|
|
22
|
+
export const listen = do_('listen');
|
|
23
23
|
export const forever = do_('forever');
|
|
@@ -111,7 +111,7 @@ const rm = operation((dir, path) => {
|
|
|
111
111
|
});
|
|
112
112
|
const console = (name) => (state, payload) => [{ ...state, [name]: `${state[name]}${payload}\n` }, undefined];
|
|
113
113
|
const map = {
|
|
114
|
-
all: (state, a) => {
|
|
114
|
+
all: (state, ...a) => {
|
|
115
115
|
let e = [];
|
|
116
116
|
for (const i of a) {
|
|
117
117
|
const [ns, ei] = virtual(state)(i);
|
|
@@ -126,11 +126,11 @@ const map = {
|
|
|
126
126
|
const result = state.internet[url];
|
|
127
127
|
return result === undefined ? [state, error('not found')] : [state, ok(result)];
|
|
128
128
|
},
|
|
129
|
-
mkdir: (state,
|
|
129
|
+
mkdir: (state, path, p) => mkdir(p !== undefined)(state, path),
|
|
130
130
|
readFile,
|
|
131
|
-
readdir: (state,
|
|
132
|
-
writeFile: (state,
|
|
133
|
-
rm
|
|
131
|
+
readdir: (state, path, { recursive }) => readdir(path, recursive === true)(state, path),
|
|
132
|
+
writeFile: (state, path, payload) => writeFile(payload)(state, path),
|
|
133
|
+
rm,
|
|
134
134
|
exec: todo,
|
|
135
135
|
createServer: todo,
|
|
136
136
|
listen: todo,
|
|
@@ -63,14 +63,10 @@ export type Or<T extends readonly Type[]> = () => readonly ['or', ...T];
|
|
|
63
63
|
/**
|
|
64
64
|
* Constructs a schema that validates a value matching any of the given schemas.
|
|
65
65
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* -
|
|
69
|
-
*
|
|
70
|
-
* (e.g. `or(42, number)` → `or(number)`),
|
|
71
|
-
* - duplicate variants are deduplicated via `Object.is`.
|
|
72
|
-
*
|
|
73
|
-
* See `issues/130-or-optimization.md`.
|
|
66
|
+
* `or` is intentionally a lazy, allocation-free constructor: it captures its
|
|
67
|
+
* arguments in a thunk and does no flattening, deduplication, subset analysis,
|
|
68
|
+
* or canonical-form work. All such algebra lives on the serializable data form
|
|
69
|
+
* — see `issues/143-rtti-data.md`.
|
|
74
70
|
*/
|
|
75
71
|
export declare const or: <T extends readonly Type[]>(...types: T) => Or<T>;
|
|
76
72
|
/** Constructs a schema that validates a value matching `T` or `undefined`. */
|
|
@@ -19,81 +19,15 @@ const type1 = (key) => t => () => [key, t];
|
|
|
19
19
|
export const array = type1('array');
|
|
20
20
|
/** Constructs a schema that validates `{ readonly[K in string]: Ts<T> }`. */
|
|
21
21
|
export const record = type1('record');
|
|
22
|
-
/** Reads the tag of a thunk variant, or returns `null` for a `Const`. */
|
|
23
|
-
const variantTag = (t) => typeof t === 'function' ? t()[0] : null;
|
|
24
|
-
const isPrim0 = includes(primitive0List);
|
|
25
|
-
const flattenStep = ([visited, out], t) => {
|
|
26
|
-
if (typeof t === 'function' && !visited.has(t)) {
|
|
27
|
-
const nextVisited = new Set([...visited, t]);
|
|
28
|
-
const info = t();
|
|
29
|
-
if (info[0] === 'or') {
|
|
30
|
-
const [v, inner] = info.slice(1)
|
|
31
|
-
.reduce(flattenStep, [nextVisited, []]);
|
|
32
|
-
return [v, [...out, ...inner]];
|
|
33
|
-
}
|
|
34
|
-
return [nextVisited, [...out, t]];
|
|
35
|
-
}
|
|
36
|
-
return [visited, [...out, t]];
|
|
37
|
-
};
|
|
38
|
-
/**
|
|
39
|
-
* Walks `types` and produces a flat list of `or` variants:
|
|
40
|
-
*
|
|
41
|
-
* - Variants whose thunk resolves to `['or', ...]` are inlined.
|
|
42
|
-
* - Each thunk is resolved at most once; thunks reached a second time are
|
|
43
|
-
* kept as-is, so self-referential `or` schemas terminate.
|
|
44
|
-
*/
|
|
45
|
-
const flattenOr = (types) => types.reduce(flattenStep, [new Set(), []])[1];
|
|
46
|
-
const collectStep = ([u, p], t) => {
|
|
47
|
-
if (u) {
|
|
48
|
-
return [u, p];
|
|
49
|
-
}
|
|
50
|
-
const tag = variantTag(t);
|
|
51
|
-
if (tag === 'unknown') {
|
|
52
|
-
return [true, p];
|
|
53
|
-
}
|
|
54
|
-
return tag !== null && isPrim0(tag) ? [u, new Set([...p, tag])] : [u, p];
|
|
55
|
-
};
|
|
56
|
-
const dedupStep = ([primThunks, acc], t) => primThunks.has(typeof t) || acc.some(r => Object.is(r, t))
|
|
57
|
-
? [primThunks, acc]
|
|
58
|
-
: [primThunks, [...acc, t]];
|
|
59
|
-
/**
|
|
60
|
-
* Drops variants that are trivially subsumed by another variant.
|
|
61
|
-
*
|
|
62
|
-
* Trivial subset rules handled here:
|
|
63
|
-
* - any variant ⊆ `unknown` — if `unknown` is present, the entire union is
|
|
64
|
-
* `unknown`.
|
|
65
|
-
* - a primitive const ⊆ its primitive type thunk — `42 ⊆ number`,
|
|
66
|
-
* `'hi' ⊆ string`, `true ⊆ boolean`, `7n ⊆ bigint`.
|
|
67
|
-
*
|
|
68
|
-
* `Object.is` is used for deduplication, so `NaN` collapses with itself and
|
|
69
|
-
* `+0` and `-0` stay distinct — matching `constPrimitiveValidate`.
|
|
70
|
-
*
|
|
71
|
-
* Full structural subset (tuples/structs/`or`/recursive schemas) is left to
|
|
72
|
-
* a future change — see goals 1 and 3 of issue 130.
|
|
73
|
-
*/
|
|
74
|
-
const reduceOr = (types) => {
|
|
75
|
-
const flat = flattenOr(types);
|
|
76
|
-
const [hasUnknown, primThunks] = flat.reduce(collectStep, [false, new Set()]);
|
|
77
|
-
return hasUnknown
|
|
78
|
-
? [unknown]
|
|
79
|
-
: flat.reduce(dedupStep, [primThunks, []])[1];
|
|
80
|
-
};
|
|
81
22
|
/**
|
|
82
23
|
* Constructs a schema that validates a value matching any of the given schemas.
|
|
83
24
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* -
|
|
87
|
-
*
|
|
88
|
-
* (e.g. `or(42, number)` → `or(number)`),
|
|
89
|
-
* - duplicate variants are deduplicated via `Object.is`.
|
|
90
|
-
*
|
|
91
|
-
* See `issues/130-or-optimization.md`.
|
|
25
|
+
* `or` is intentionally a lazy, allocation-free constructor: it captures its
|
|
26
|
+
* arguments in a thunk and does no flattening, deduplication, subset analysis,
|
|
27
|
+
* or canonical-form work. All such algebra lives on the serializable data form
|
|
28
|
+
* — see `issues/143-rtti-data.md`.
|
|
92
29
|
*/
|
|
93
|
-
export const or = (...types) =>
|
|
94
|
-
const reduced = reduceOr(types);
|
|
95
|
-
return (() => ['or', ...reduced]);
|
|
96
|
-
};
|
|
30
|
+
export const or = (...types) => () => ['or', ...types];
|
|
97
31
|
/** Constructs a schema that validates a value matching `T` or `undefined`. */
|
|
98
32
|
export const option = (t) => or(t, undefined);
|
|
99
33
|
/** Schema that never matches any value — the empty union, corresponding to TypeScript's `never`. */
|
|
@@ -2,33 +2,5 @@ declare const _default: {
|
|
|
2
2
|
typeof: {
|
|
3
3
|
[k: string]: (() => void)[];
|
|
4
4
|
};
|
|
5
|
-
or: {
|
|
6
|
-
flatten: {
|
|
7
|
-
shallow: () => void;
|
|
8
|
-
deep: () => void;
|
|
9
|
-
manualOrThunk: () => void;
|
|
10
|
-
selfReferential: () => void;
|
|
11
|
-
};
|
|
12
|
-
unknownCollapse: {
|
|
13
|
-
withConst: () => void;
|
|
14
|
-
withThunk: () => void;
|
|
15
|
-
nested: () => void;
|
|
16
|
-
};
|
|
17
|
-
dropPrimitiveSubset: {
|
|
18
|
-
number: () => void;
|
|
19
|
-
boolean: () => void;
|
|
20
|
-
string: () => void;
|
|
21
|
-
bigint: () => void;
|
|
22
|
-
keepIfThunkAbsent: () => void;
|
|
23
|
-
mixed: () => void;
|
|
24
|
-
};
|
|
25
|
-
dedup: {
|
|
26
|
-
sameThunkReference: () => void;
|
|
27
|
-
samePrimitive: () => void;
|
|
28
|
-
nanCollapses: () => void;
|
|
29
|
-
signedZeroDistinct: () => void;
|
|
30
|
-
};
|
|
31
|
-
emptyStillNever: () => void;
|
|
32
|
-
};
|
|
33
5
|
};
|
|
34
6
|
export default _default;
|
package/fs/types/rtti/test.f.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { boolean, number, string, bigint, unknown, or, never } from "./module.f.js";
|
|
2
|
-
import { assertEq } from "../../dev/module.f.js";
|
|
3
1
|
const tests = {
|
|
4
2
|
undefined: [undefined],
|
|
5
3
|
boolean: [true, false],
|
|
@@ -9,115 +7,10 @@ const tests = {
|
|
|
9
7
|
object: [null, {}, []],
|
|
10
8
|
function: [() => undefined]
|
|
11
9
|
};
|
|
12
|
-
const variantsOf = (t) => {
|
|
13
|
-
if (typeof t !== 'function') {
|
|
14
|
-
throw 'expected an or thunk';
|
|
15
|
-
}
|
|
16
|
-
const info = t();
|
|
17
|
-
if (info[0] !== 'or') {
|
|
18
|
-
throw `expected an or thunk, got ${info[0]}`;
|
|
19
|
-
}
|
|
20
|
-
return info.slice(1);
|
|
21
|
-
};
|
|
22
10
|
export default {
|
|
23
11
|
typeof: Object.fromEntries(Object.entries(tests).map(([k, a]) => [k, a.map(v => () => {
|
|
24
12
|
if (typeof v !== k) {
|
|
25
13
|
throw `typeof ${v} !== ${k}`;
|
|
26
14
|
}
|
|
27
15
|
})])),
|
|
28
|
-
or: {
|
|
29
|
-
flatten: {
|
|
30
|
-
shallow: () => {
|
|
31
|
-
const v = variantsOf(or(or(boolean, number), string));
|
|
32
|
-
assertEq(v.length, 3);
|
|
33
|
-
assertEq(v[0], boolean);
|
|
34
|
-
assertEq(v[1], number);
|
|
35
|
-
assertEq(v[2], string);
|
|
36
|
-
},
|
|
37
|
-
deep: () => {
|
|
38
|
-
assertEq(variantsOf(or(or(or(boolean, number), string), bigint)).length, 4);
|
|
39
|
-
},
|
|
40
|
-
manualOrThunk: () => {
|
|
41
|
-
// Manually-constructed `() => ['or', ...]` thunks should also flatten.
|
|
42
|
-
const inner = () => ['or', boolean, number];
|
|
43
|
-
assertEq(variantsOf(or(inner, string)).length, 3);
|
|
44
|
-
},
|
|
45
|
-
selfReferential: () => {
|
|
46
|
-
// A self-referential `or` thunk terminates: it is expanded once,
|
|
47
|
-
// then kept as-is on the second encounter (via the visited set).
|
|
48
|
-
const t = (() => ['or', boolean, t]);
|
|
49
|
-
// expansion: t -> [boolean, t]; t (second) kept as-is; number kept.
|
|
50
|
-
assertEq(variantsOf(or(t, number)).length, 3);
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
unknownCollapse: {
|
|
54
|
-
withConst: () => {
|
|
55
|
-
const v = variantsOf(or(unknown, 42));
|
|
56
|
-
assertEq(v.length, 1);
|
|
57
|
-
assertEq(v[0], unknown);
|
|
58
|
-
},
|
|
59
|
-
withThunk: () => {
|
|
60
|
-
const v = variantsOf(or(number, unknown, string));
|
|
61
|
-
assertEq(v.length, 1);
|
|
62
|
-
assertEq(v[0], unknown);
|
|
63
|
-
},
|
|
64
|
-
nested: () => {
|
|
65
|
-
// nested `or` whose flattening surfaces an `unknown`
|
|
66
|
-
const v = variantsOf(or(or(boolean, unknown), 42));
|
|
67
|
-
assertEq(v.length, 1);
|
|
68
|
-
assertEq(v[0], unknown);
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
dropPrimitiveSubset: {
|
|
72
|
-
number: () => {
|
|
73
|
-
const v = variantsOf(or(42, number));
|
|
74
|
-
assertEq(v.length, 1);
|
|
75
|
-
assertEq(v[0], number);
|
|
76
|
-
},
|
|
77
|
-
boolean: () => {
|
|
78
|
-
const v = variantsOf(or(true, false, boolean));
|
|
79
|
-
assertEq(v.length, 1);
|
|
80
|
-
assertEq(v[0], boolean);
|
|
81
|
-
},
|
|
82
|
-
string: () => {
|
|
83
|
-
const v = variantsOf(or('hi', 'bye', string));
|
|
84
|
-
assertEq(v.length, 1);
|
|
85
|
-
assertEq(v[0], string);
|
|
86
|
-
},
|
|
87
|
-
bigint: () => {
|
|
88
|
-
const v = variantsOf(or(7n, bigint));
|
|
89
|
-
assertEq(v.length, 1);
|
|
90
|
-
assertEq(v[0], bigint);
|
|
91
|
-
},
|
|
92
|
-
keepIfThunkAbsent: () => {
|
|
93
|
-
// Without a matching primitive thunk, consts are kept.
|
|
94
|
-
assertEq(variantsOf(or(42, 'hello')).length, 2);
|
|
95
|
-
},
|
|
96
|
-
mixed: () => {
|
|
97
|
-
// `null`/`undefined` consts have no matching primitive thunk and remain.
|
|
98
|
-
assertEq(variantsOf(or(null, undefined, 42, number)).length, 3);
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
dedup: {
|
|
102
|
-
sameThunkReference: () => {
|
|
103
|
-
assertEq(variantsOf(or(boolean, boolean)).length, 1);
|
|
104
|
-
},
|
|
105
|
-
samePrimitive: () => {
|
|
106
|
-
assertEq(variantsOf(or(42, 42)).length, 1);
|
|
107
|
-
},
|
|
108
|
-
nanCollapses: () => {
|
|
109
|
-
// `Object.is(NaN, NaN)` is true, so duplicate `NaN` variants
|
|
110
|
-
// collapse — matching `constPrimitiveValidate` semantics.
|
|
111
|
-
assertEq(variantsOf(or(NaN, NaN)).length, 1);
|
|
112
|
-
},
|
|
113
|
-
signedZeroDistinct: () => {
|
|
114
|
-
// `Object.is(+0, -0)` is false, so they remain distinct.
|
|
115
|
-
assertEq(variantsOf(or(0, -0)).length, 2);
|
|
116
|
-
},
|
|
117
|
-
},
|
|
118
|
-
emptyStillNever: () => {
|
|
119
|
-
assertEq(variantsOf(or()).length, 0);
|
|
120
|
-
assertEq(variantsOf(never).length, 0);
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
16
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "functionalscript",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"**/*.js",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"website": "node --experimental-strip-types ./fs/fjs/module.ts r ./fs/website/module.f.ts"
|
|
19
19
|
},
|
|
20
20
|
"engines": {
|
|
21
|
-
"node": ">=
|
|
21
|
+
"node": ">=22"
|
|
22
22
|
},
|
|
23
23
|
"bin": {
|
|
24
24
|
"fjs": "fs/fjs/module.js"
|