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
@@ -1,21 +1,22 @@
1
- declare const _default: {
2
- throw: () => never;
3
- throwReference: {
4
- throw: () => unknown;
5
- };
6
- throwGroup: {
7
- throw: {
8
- inline: () => never;
9
- reference: () => unknown;
10
- nested: {
11
- deep: () => never;
12
- };
13
- };
14
- };
15
- };
16
- export default _default;
17
- export declare const namedExportTest: () => void;
18
- export declare const namedExportGroup: {
19
- nested: () => void;
20
- throw: () => never;
1
+ export declare const flat: () => void;
2
+ export declare const nested: () => void;
3
+ export declare const throwKey: () => void;
4
+ export declare const throwKeyFail: () => void;
5
+ export declare const mixedPassFail: () => void;
6
+ export declare const returnValueSubTree: () => void;
7
+ export declare const arrayKeys: () => void;
8
+ export declare const nonTestFilesSkipped: () => void;
9
+ export declare const multipleFiles: () => void;
10
+ export declare const throwByFunctionName: () => void;
11
+ export declare const namedExports: () => void;
12
+ export declare const defaultReporterOutput: () => void;
13
+ export declare const defaultReporterFailOutput: () => void;
14
+ export declare const githubReporterOutput: () => void;
15
+ export declare const helpers: {
16
+ isInteger: () => void;
17
+ isIdentifier: () => void;
18
+ fmtImport: () => void;
19
+ fmtPath: () => void;
20
+ fmtTerm: () => void;
21
+ ghEscape: () => void;
21
22
  };
@@ -1,36 +1,254 @@
1
- // A helper whose own `name` is not `'throw'`. The test framework should still
2
- // treat it as a pass-on-throw test when the enclosing key is `throw`.
3
- const helperThrows = () => { throw 'helper failure'; };
4
- export default {
5
- throw: () => {
6
- throw [() => { }, () => { }];
1
+ import { pure } from "../../types/effects/module.f.js";
2
+ import { emptyState } from "../../types/effects/node/virtual/module.f.js";
3
+ import { virtual } from "../../types/effects/node/virtual/module.f.js";
4
+ import { assert, assertEq } from "../module.f.js";
5
+ import { test, defaultReporter, fmtPath, fmtTerm, fmtImport, ghEscape, isInteger, isIdentifier, defaultTest, } from "./module.f.js";
6
+ const makeReporter = () => {
7
+ const events = [];
8
+ const reporter = {
9
+ result: (file, path, r) => { events.push(['result', file, [...path], r]); return pure(undefined); },
10
+ summary: (pass, fail, time) => { events.push(['summary', pass, fail, time]); return pure(undefined); },
11
+ test: defaultTest,
12
+ };
13
+ return [reporter, () => events];
14
+ };
15
+ const options = (initCwd, github = false) => ({
16
+ args: [],
17
+ env: { INIT_CWD: initCwd, ...(github ? { GITHUB_ACTION: 'true' } : {}) },
18
+ std: { stdout: { isTTY: false }, stderr: { isTTY: false } },
19
+ });
20
+ const ok0 = () => ({ result: ['ok', undefined], duration: 0 });
21
+ const fail0 = () => ({ result: ['error', 'oops'], duration: 0 });
22
+ const ok1 = () => ({ result: ['ok', undefined], duration: 1 });
23
+ const run = (dir, initCwd = '.') => {
24
+ const [reporter, getEvents] = makeReporter();
25
+ const state = { ...emptyState, root: dir };
26
+ const [, exitCode] = virtual(state)(test(reporter)(options(initCwd)));
27
+ return [getEvents(), exitCode];
28
+ };
29
+ // Runs the real `defaultReporter` and returns its captured stdout/stderr so the
30
+ // terminal and GitHub output formats can be asserted directly.
31
+ const runMain = (dir, github = false) => {
32
+ const state = { ...emptyState, root: dir };
33
+ const opts = options('.', github);
34
+ const [finalState, exitCode] = virtual(state)(test(defaultReporter(opts))(opts));
35
+ return [finalState.stdout, finalState.stderr, exitCode];
36
+ };
37
+ // flat object: two passing tests
38
+ export const flat = () => {
39
+ const [events, exit] = run({
40
+ 'a.test.f.ts': () => ({ a: ok0, b: ok1 }),
41
+ });
42
+ assertEq(exit, 0);
43
+ const [e0, e1, e2] = events;
44
+ assert(e0[0] === 'result' && e0[2][0] === 'a');
45
+ assert(e1[0] === 'result' && e1[2][0] === 'b');
46
+ assertEq(e2[0], 'summary');
47
+ const [, pass, fail] = e2;
48
+ assertEq(pass, 2);
49
+ assertEq(fail, 0);
50
+ };
51
+ // nested object: leaf tests carry the full path including the sub-tree key
52
+ export const nested = () => {
53
+ const [events, exit] = run({
54
+ 'n.test.f.ts': () => ({ math: { add: ok0, sub: ok0 } }),
55
+ });
56
+ assertEq(exit, 0);
57
+ const [e0, e1, e2] = events;
58
+ assert(e0[0] === 'result' && e0[2][1] === 'add');
59
+ assert(e1[0] === 'result' && e1[2][1] === 'sub');
60
+ assertEq(e2[0], 'summary');
61
+ const [, pass, fail] = e2;
62
+ assertEq(pass, 2);
63
+ assertEq(fail, 0);
64
+ };
65
+ // throw key: tests inside 'throw' pass on error result
66
+ export const throwKey = () => {
67
+ const [events, exit] = run({
68
+ 't.test.f.ts': () => ({ throw: { a: fail0 } }),
69
+ });
70
+ assertEq(exit, 0);
71
+ const [e0, e1] = events;
72
+ assert(e0[0] === 'result' && e0[2][0] === 'throw' && e0[2][1] === 'a');
73
+ assertEq(e1[0], 'summary');
74
+ const [, pass, fail] = e1;
75
+ assertEq(pass, 1);
76
+ assertEq(fail, 0);
77
+ };
78
+ // throw key fails when test does not throw (returns ok in throw context)
79
+ export const throwKeyFail = () => {
80
+ const [events, exit] = run({
81
+ 't.test.f.ts': () => ({ throw: { a: ok0 } }),
82
+ });
83
+ assertEq(exit, 1);
84
+ const [e0, e1] = events;
85
+ assertEq(e0[0], 'result');
86
+ const [, pass, fail] = e1;
87
+ assertEq(pass, 0);
88
+ assertEq(fail, 1);
89
+ };
90
+ // mixed pass/fail updates summary counts
91
+ export const mixedPassFail = () => {
92
+ const [events, exit] = run({
93
+ 'm.test.f.ts': () => ({ good: ok0, bad: fail0 }),
94
+ });
95
+ assertEq(exit, 1);
96
+ const summary = events[events.length - 1];
97
+ assertEq(summary[0], 'summary');
98
+ const [, pass, fail] = summary;
99
+ assertEq(pass, 1);
100
+ assertEq(fail, 1);
101
+ };
102
+ // return-value sub-tree: passing test's return value is walked
103
+ export const returnValueSubTree = () => {
104
+ const inner = () => ({ result: ['ok', undefined], duration: 0 });
105
+ const [events, exit] = run({
106
+ 'r.test.f.ts': () => ({
107
+ outer: () => ({
108
+ result: ['ok', { inner }],
109
+ duration: 0,
110
+ }),
111
+ }),
112
+ });
113
+ // outer passes, then inner (from return value) also passes
114
+ assertEq(exit, 0);
115
+ const passEvents = events.filter(e => e[0] === 'result');
116
+ assertEq(passEvents.length, 2);
117
+ const [p0, p1] = passEvents;
118
+ assertEq(p0[2][0], 'outer');
119
+ assertEq(p1[2][2], 'inner');
120
+ };
121
+ // integer-indexed array keys appear as numeric path segments
122
+ export const arrayKeys = () => {
123
+ const [events, exit] = run({
124
+ 'a.test.f.ts': () => ({ arr: [ok0, ok0] }),
125
+ });
126
+ assertEq(exit, 0);
127
+ const passEvents = events.filter(e => e[0] === 'result');
128
+ assertEq(passEvents.length, 2);
129
+ assertEq(passEvents[0][2][1], '0');
130
+ assertEq(passEvents[1][2][1], '1');
131
+ };
132
+ // non-test files are skipped (only files ending in test.f.ts/js are loaded)
133
+ export const nonTestFilesSkipped = () => {
134
+ const [events, exit] = run({
135
+ 'helper.ts': () => ({ a: ok0 }),
136
+ 'b.test.f.ts': () => ({ x: ok0 }),
137
+ });
138
+ assertEq(exit, 0);
139
+ const results = events.filter(e => e[0] === 'result');
140
+ assertEq(results.length, 1);
141
+ assertEq(results[0][1], './b.test.f.ts');
142
+ };
143
+ // multiple test files each produce result events
144
+ export const multipleFiles = () => {
145
+ const [events, exit] = run({
146
+ 'a.test.f.ts': () => ({ x: ok0 }),
147
+ 'b.test.f.ts': () => ({ y: ok0 }),
148
+ });
149
+ assertEq(exit, 0);
150
+ const results = events.filter(e => e[0] === 'result');
151
+ assertEq(results.length, 2);
152
+ const [, pass, fail] = events[events.length - 1];
153
+ assertEq(pass, 2);
154
+ assertEq(fail, 0);
155
+ };
156
+ // a function literally named `throw` is a throwing test even when its key is not `throw`
157
+ export const throwByFunctionName = () => {
158
+ // // `bun` calls the function `named` instead of `throw`
159
+ // const named = ({ throw: () => fail0() }).throw
160
+ const x = { throw: () => fail0() };
161
+ const [events, exit] = run({
162
+ 't.test.f.ts': () => ({ here: x.throw }),
163
+ });
164
+ assertEq(exit, 0);
165
+ const passEvents = events.filter(e => e[0] === 'result');
166
+ assertEq(passEvents.length, 1);
167
+ assertEq(passEvents[0][2][0], 'here');
168
+ };
169
+ // every module export — `default` and named — becomes a top-level path segment
170
+ export const namedExports = () => {
171
+ const [events, exit] = run({
172
+ 'e.test.f.ts': () => ({ default: ok0, helper: ok0 }),
173
+ });
174
+ assertEq(exit, 0);
175
+ const passEvents = events.filter(e => e[0] === 'result');
176
+ assertEq(passEvents.length, 2);
177
+ assertEq(passEvents[0][2][0], 'default');
178
+ assertEq(passEvents[1][2][0], 'helper');
179
+ };
180
+ // the default (non-GitHub) reporter formats module/pass/summary lines on stdout
181
+ export const defaultReporterOutput = () => {
182
+ const [stdout, stderr, exit] = runMain({
183
+ 'a.test.f.ts': () => ({ x: ok0 }),
184
+ });
185
+ assertEq(exit, 0);
186
+ assertEq(stderr, '');
187
+ assertEq(stdout, 'import("./a.test.f.ts").x(): ok, 0.0000 ms\n'
188
+ + 'Number of tests: pass: 1, fail: 0, total: 1\n'
189
+ + 'Time: 0.0000 ms\n');
190
+ };
191
+ // a failure on the non-GitHub reporter writes the error to stderr, not stdout
192
+ export const defaultReporterFailOutput = () => {
193
+ const [, stderr, exit] = runMain({
194
+ 'a.test.f.ts': () => ({ bad: fail0 }),
195
+ });
196
+ assertEq(exit, 1);
197
+ assertEq(stderr, 'import("./a.test.f.ts").bad(): error, 0.0000 ms\noops\n');
198
+ };
199
+ // the GitHub reporter emits an `::error` annotation with a percent-encoded
200
+ // title (the JSON path) and message
201
+ export const githubReporterOutput = () => {
202
+ const [, stderr, exit] = runMain({
203
+ 's.test.f.ts': () => ({ 'a:b,c%d': fail0 }),
204
+ }, true);
205
+ assertEq(exit, 1);
206
+ assertEq(stderr, '::error file=./s.test.f.ts,line=1,title=import("./s.test.f.ts")["a%3Ab%2Cc%25d"]()::oops\n');
207
+ };
208
+ // direct unit tests for the pure path-format helpers
209
+ export const helpers = {
210
+ isInteger: () => {
211
+ assert(isInteger('0'));
212
+ assert(isInteger('123'));
213
+ assert(!isInteger(''));
214
+ assert(!isInteger('01'));
215
+ assert(!isInteger('1a'));
216
+ assert(!isInteger('-1'));
7
217
  },
8
- // `throw` key with a function reference (function name is `helperThrows`).
9
- throwReference: {
10
- throw: helperThrows,
218
+ isIdentifier: () => {
219
+ assert(isIdentifier('abc'));
220
+ assert(isIdentifier('_x'));
221
+ assert(isIdentifier('$y'));
222
+ assert(isIdentifier('a1'));
223
+ assert(!isIdentifier(''));
224
+ assert(!isIdentifier('1a'));
225
+ assert(!isIdentifier('a-b'));
11
226
  },
12
- // `throw` key with a group: every leaf function under it must throw to pass.
13
- throwGroup: {
14
- throw: {
15
- inline: () => { throw 'inline failure'; },
16
- reference: helperThrows,
17
- nested: {
18
- deep: () => { throw 'deep failure'; },
19
- },
20
- },
227
+ fmtImport: () => {
228
+ assertEq(fmtImport('./a.test.f.ts', []), 'import("./a.test.f.ts")()');
229
+ assertEq(fmtImport('./a.test.f.ts', ['math', 'add']), 'import("./a.test.f.ts").math.add()');
230
+ assertEq(fmtImport('./a.test.f.ts', ['users', '3']), 'import("./a.test.f.ts").users[3]()');
231
+ assertEq(fmtImport('./a.test.f.ts', ['x', 'hello world']), 'import("./a.test.f.ts").x["hello world"]()');
232
+ assertEq(fmtImport('./a.test.f.ts', ['outer', null, 'inner']), 'import("./a.test.f.ts").outer().inner()');
21
233
  },
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
- }
234
+ fmtPath: () => {
235
+ assertEq(fmtPath([]), '');
236
+ assertEq(fmtPath(['math', 'add']), '.math.add');
237
+ assertEq(fmtPath(['users', '3', 'name']), '.users[3].name');
238
+ assertEq(fmtPath(['x', 'hello world']), '.x["hello world"]');
239
+ assertEq(fmtPath(['outer', null, 'inner']), '.outer().inner');
240
+ },
241
+ fmtTerm: () => {
242
+ assertEq(fmtTerm([]), '()');
243
+ assertEq(fmtTerm(['math', 'add']), '| | add');
244
+ assertEq(fmtTerm(['a', '0']), '| | 0');
245
+ assertEq(fmtTerm(['x', 'hello world']), '| | "hello world"');
246
+ },
247
+ ghEscape: () => {
248
+ assertEq(ghEscape('a%b'), 'a%25b');
249
+ assertEq(ghEscape('a:b'), 'a%3Ab');
250
+ assertEq(ghEscape('a,b'), 'a%2Cb');
251
+ assertEq(ghEscape('a\r\nb'), 'a%0D%0Ab');
252
+ assertEq(ghEscape('a%b:c,d'), 'a%25b%3Ac%2Cd');
34
253
  },
35
- throw: () => { throw 'expected to throw'; },
36
254
  };
@@ -5,13 +5,13 @@
5
5
  */
6
6
  import type { Primitive as JsonPrimitive } from '../json/module.f.ts';
7
7
  import { type Effect } from '../types/effects/module.f.ts';
8
- import { type Error, type WriteFile, type ReadFile } from '../types/effects/node/module.f.ts';
8
+ import { type WriteFile, type ReadFile, type Write } from '../types/effects/node/module.f.ts';
9
9
  export type Object = {
10
10
  readonly [k in string]: Unknown;
11
11
  };
12
12
  export type Array = readonly Unknown[];
13
13
  export type Primitive = JsonPrimitive | bigint | undefined;
14
14
  export type Unknown = Primitive | Object | Array;
15
- type CompileOp = ReadFile | WriteFile | Error;
15
+ type CompileOp = ReadFile | WriteFile | Write;
16
16
  export declare const compile: (args: readonly string[]) => Effect<CompileOp, number>;
17
17
  export {};
@@ -1,23 +1,26 @@
1
1
  import { transpile } from "./transpiler/module.f.js";
2
2
  import { stringify, stringifyAsTree } from "./serializer/module.f.js";
3
3
  import { sort } from "../types/object/module.f.js";
4
- import { encodeUtf8, toVec } from "../types/uint8array/module.f.js";
5
4
  import { pure } from "../types/effects/module.f.js";
6
- import { error, writeFile, } from "../types/effects/node/module.f.js";
5
+ import { writeFile, error, } from "../types/effects/node/module.f.js";
6
+ import { utf8 } from "../text/module.f.js";
7
7
  export const compile = args => {
8
8
  if (args.length < 2) {
9
- return error('Error: Requires 2 or more arguments').step(() => pure(1));
9
+ return error('Error: Requires 2 or more arguments')
10
+ .step(() => pure(1));
10
11
  }
11
12
  const inputFileName = args[0];
12
13
  const outputFileName = args[1];
13
14
  return transpile(inputFileName).step((result) => {
14
15
  if (result[0] === 'error') {
15
16
  const metadata = result[1].metadata;
16
- return error(`${metadata?.path}:${metadata?.line}:${metadata?.column} - error: ${result[1].message}`).step(() => pure(0));
17
+ return error(`${metadata?.path}:${metadata?.line}:${metadata?.column} - error: ${result[1].message}`)
18
+ .step(() => pure(0));
17
19
  }
18
20
  const content = outputFileName.endsWith('.json')
19
21
  ? stringifyAsTree(sort)(result[1])
20
22
  : stringify(sort)(result[1]);
21
- return writeFile(outputFileName, toVec(encodeUtf8(content))).step(() => pure(0));
23
+ return writeFile(outputFileName, utf8(content))
24
+ .step(() => pure(0));
22
25
  });
23
26
  };
package/fs/djs/test.f.js CHANGED
@@ -1,14 +1,13 @@
1
1
  import { compile } from "./module.f.js";
2
2
  import { virtual, emptyState } from "../types/effects/node/virtual/module.f.js";
3
- import { encodeUtf8, toVec, fromVec, decodeUtf8 } from "../types/uint8array/module.f.js";
4
3
  import { isVec } from "../types/bit_vec/module.f.js";
5
- const fileVec = (s) => toVec(encodeUtf8(s));
4
+ import { utf8, utf8ToString } from "../text/module.f.js";
6
5
  const readOutput = (root, path) => {
7
6
  const file = root[path];
8
7
  if (!isVec(file)) {
9
8
  throw `${path} is not a file`;
10
9
  }
11
- return decodeUtf8(fromVec(file));
10
+ return utf8ToString(file);
12
11
  };
13
12
  export default {
14
13
  tooFewArgs: {
@@ -32,7 +31,7 @@ export default {
32
31
  },
33
32
  },
34
33
  success: () => {
35
- const root = { 'input.f.js': fileVec('export default 42') };
34
+ const root = { 'input.f.js': utf8('export default 42') };
36
35
  const [state, code] = virtual({ ...emptyState, root })(compile(['input.f.js', 'output.f.js']));
37
36
  if (code !== 0) {
38
37
  throw code;
@@ -43,7 +42,7 @@ export default {
43
42
  }
44
43
  },
45
44
  jsonOutput: () => {
46
- const root = { 'input.f.js': fileVec('export default 42') };
45
+ const root = { 'input.f.js': utf8('export default 42') };
47
46
  const [state, code] = virtual({ ...emptyState, root })(compile(['input.f.js', 'output.json']));
48
47
  if (code !== 0) {
49
48
  throw code;
@@ -63,7 +62,7 @@ export default {
63
62
  }
64
63
  },
65
64
  parseError: () => {
66
- const root = { 'bad.f.js': fileVec('export default @') };
65
+ const root = { 'bad.f.js': utf8('export default @') };
67
66
  const [state, code] = virtual({ ...emptyState, root })(compile(['bad.f.js', 'output.f.js']));
68
67
  if (code !== 0) {
69
68
  throw code;