functionalscript 0.19.0 → 0.20.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 (219) hide show
  1. package/fs/asn.1/{test.f.d.ts → proof.f.d.ts} +1 -2
  2. package/fs/asn.1/{test.f.js → proof.f.js} +1 -1
  3. package/fs/base128/proof.f.d.ts +1 -0
  4. package/fs/base128/{test.f.js → proof.f.js} +1 -1
  5. package/fs/bnf/data/{test.f.d.ts → proof.f.d.ts} +1 -2
  6. package/fs/bnf/data/{test.f.js → proof.f.js} +1 -1
  7. package/fs/bnf/proof.f.d.ts +3 -0
  8. package/fs/bnf/{test.f.js → proof.f.js} +1 -1
  9. package/fs/cas/module.f.js +2 -12
  10. package/fs/cas/proof.f.d.ts +1 -0
  11. package/fs/cas/proof.f.js +1 -0
  12. package/fs/cbase32/{test.f.d.ts → proof.f.d.ts} +1 -2
  13. package/fs/cbase32/{test.f.js → proof.f.js} +1 -1
  14. package/fs/ci/node/module.f.js +12 -7
  15. package/fs/ci/{test.f.d.ts → proof.f.d.ts} +1 -2
  16. package/fs/ci/{test.f.js → proof.f.js} +1 -1
  17. package/fs/crypto/hmac/{test.f.d.ts → proof.f.d.ts} +1 -2
  18. package/fs/crypto/hmac/{test.f.js → proof.f.js} +1 -1
  19. package/fs/crypto/secp/{test.f.d.ts → proof.f.d.ts} +1 -2
  20. package/fs/crypto/secp/{test.f.js → proof.f.js} +1 -1
  21. package/fs/crypto/sha2/{test.f.d.ts → proof.f.d.ts} +1 -2
  22. package/fs/crypto/sha2/{test.f.js → proof.f.js} +1 -1
  23. package/fs/crypto/sign/{test.f.d.ts → proof.f.d.ts} +1 -2
  24. package/fs/crypto/sign/{test.f.js → proof.f.js} +1 -1
  25. package/fs/dev/module.f.d.ts +28 -2
  26. package/fs/dev/module.f.js +38 -22
  27. package/fs/dev/{test.f.d.ts → proof.f.d.ts} +5 -2
  28. package/fs/dev/{test.f.js → proof.f.js} +25 -2
  29. package/fs/dev/tf/module.f.d.ts +63 -5
  30. package/fs/dev/tf/module.f.js +77 -20
  31. package/fs/dev/tf/{test.f.d.ts → proof.f.d.ts} +26 -0
  32. package/fs/dev/tf/{test.f.js → proof.f.js} +76 -34
  33. package/fs/dev/tf/scenarios/async-subtests.fail.d.ts +4 -0
  34. package/fs/dev/tf/scenarios/async-subtests.fail.js +7 -0
  35. package/fs/dev/tf/scenarios/async-subtests.pass.d.ts +4 -0
  36. package/fs/dev/tf/scenarios/async-subtests.pass.js +7 -0
  37. package/fs/dev/tf/scenarios/async.fail.d.ts +1 -0
  38. package/fs/dev/tf/scenarios/async.fail.js +4 -0
  39. package/fs/dev/tf/scenarios/async.pass.d.ts +1 -0
  40. package/fs/dev/tf/scenarios/async.pass.js +3 -0
  41. package/fs/dev/tf/scenarios/thenable.pass.d.ts +3 -0
  42. package/fs/dev/tf/scenarios/thenable.pass.js +9 -0
  43. package/fs/dev/tf/scenarios/thenable2.pass.f.d.ts +3 -0
  44. package/fs/dev/tf/scenarios/thenable2.pass.f.js +3 -0
  45. package/fs/dev/version/proof.f.d.ts +3 -0
  46. package/fs/dev/version/{test.f.js → proof.f.js} +1 -1
  47. package/fs/djs/ast/{test.f.d.ts → proof.f.d.ts} +1 -2
  48. package/fs/djs/ast/{test.f.js → proof.f.js} +1 -1
  49. package/fs/djs/parser/{test.f.d.ts → proof.f.d.ts} +1 -2
  50. package/fs/djs/parser/{test.f.js → proof.f.js} +1 -1
  51. package/fs/djs/{test.f.d.ts → proof.f.d.ts} +1 -2
  52. package/fs/djs/{test.f.js → proof.f.js} +1 -1
  53. package/fs/djs/serializer/module.f.d.ts +2 -2
  54. package/fs/djs/serializer/module.f.js +47 -79
  55. package/fs/djs/serializer/{test.f.d.ts → proof.f.d.ts} +1 -2
  56. package/fs/djs/serializer/{test.f.js → proof.f.js} +8 -8
  57. package/fs/djs/tokenizer/{test.f.d.ts → proof.f.d.ts} +1 -2
  58. package/fs/djs/tokenizer/{test.f.js → proof.f.js} +1 -1
  59. package/fs/djs/tokenizer-new/{test.f.d.ts → proof.f.d.ts} +1 -2
  60. package/fs/djs/tokenizer-new/{test.f.js → proof.f.js} +1 -1
  61. package/fs/djs/transpiler/module.f.d.ts +15 -0
  62. package/fs/djs/transpiler/module.f.js +10 -2
  63. package/fs/djs/transpiler/{test.f.d.ts → proof.f.d.ts} +1 -2
  64. package/fs/djs/transpiler/{test.f.js → proof.f.js} +1 -1
  65. package/fs/fsc/{test.f.d.ts → proof.f.d.ts} +1 -2
  66. package/fs/fsc/{test.f.js → proof.f.js} +1 -1
  67. package/fs/fsm/proof.f.d.ts +4 -0
  68. package/fs/fsm/{test.f.js → proof.f.js} +1 -1
  69. package/fs/html/{test.f.d.ts → proof.f.d.ts} +1 -2
  70. package/fs/html/{test.f.js → proof.f.js} +1 -1
  71. package/fs/io/module.d.ts +1 -1
  72. package/fs/io/module.f.d.ts +3 -2
  73. package/fs/io/module.f.js +4 -3
  74. package/fs/io/module.js +19 -11
  75. package/fs/js/tokenizer/{test.f.d.ts → proof.f.d.ts} +1 -2
  76. package/fs/js/tokenizer/{test.f.js → proof.f.js} +1 -1
  77. package/fs/json/parser/{test.f.d.ts → proof.f.d.ts} +1 -2
  78. package/fs/json/parser/{test.f.js → proof.f.js} +1 -1
  79. package/fs/json/{test.f.d.ts → proof.f.d.ts} +1 -2
  80. package/fs/json/{test.f.js → proof.f.js} +1 -1
  81. package/fs/json/serializer/{test.f.d.ts → proof.f.d.ts} +1 -2
  82. package/fs/json/serializer/{test.f.js → proof.f.js} +1 -1
  83. package/fs/json/tokenizer/{test.f.d.ts → proof.f.d.ts} +1 -2
  84. package/fs/json/tokenizer/{test.f.js → proof.f.js} +1 -1
  85. package/fs/path/proof.f.d.ts +5 -0
  86. package/fs/path/{test.f.js → proof.f.js} +4 -3
  87. package/fs/sul/id/{test.f.d.ts → proof.f.d.ts} +1 -2
  88. package/fs/sul/id/{test.f.js → proof.f.js} +1 -1
  89. package/fs/sul/level/hash/{test.f.d.ts → proof.f.d.ts} +1 -2
  90. package/fs/sul/level/hash/{test.f.js → proof.f.js} +1 -1
  91. package/fs/sul/level/literal/{test.f.d.ts → proof.f.d.ts} +1 -2
  92. package/fs/sul/level/literal/{test.f.js → proof.f.js} +1 -1
  93. package/fs/sul/{test.f.d.ts → proof.f.d.ts} +1 -2
  94. package/fs/sul/{test.f.js → proof.f.js} +1 -1
  95. package/fs/text/ascii/proof.f.d.ts +3 -0
  96. package/fs/text/ascii/{test.f.js → proof.f.js} +1 -1
  97. package/fs/text/code_point/module.f.d.ts +28 -0
  98. package/fs/text/code_point/module.f.js +31 -0
  99. package/fs/text/{test.f.d.ts → proof.f.d.ts} +1 -2
  100. package/fs/text/{test.f.js → proof.f.js} +1 -1
  101. package/fs/text/sgr/proof.f.d.ts +1 -0
  102. package/fs/text/sgr/{test.f.js → proof.f.js} +1 -1
  103. package/fs/text/utf16/module.f.js +3 -53
  104. package/fs/text/utf16/{test.f.d.ts → proof.f.d.ts} +1 -2
  105. package/fs/text/utf16/{test.f.js → proof.f.js} +1 -1
  106. package/fs/text/utf8/module.f.js +3 -25
  107. package/fs/text/utf8/{test.f.d.ts → proof.f.d.ts} +1 -2
  108. package/fs/text/utf8/{test.f.js → proof.f.js} +1 -1
  109. package/fs/types/array/{test.f.d.ts → proof.f.d.ts} +1 -2
  110. package/fs/types/array/{test.f.js → proof.f.js} +1 -1
  111. package/fs/types/bigfloat/{test.f.d.ts → proof.f.d.ts} +1 -2
  112. package/fs/types/bigfloat/{test.f.js → proof.f.js} +1 -1
  113. package/fs/types/bigint/{test.f.d.ts → proof.f.d.ts} +1 -2
  114. package/fs/types/bigint/{test.f.js → proof.f.js} +1 -1
  115. package/fs/types/bit_vec/{test.f.d.ts → proof.f.d.ts} +1 -2
  116. package/fs/types/bit_vec/{test.f.js → proof.f.js} +1 -1
  117. package/fs/types/btree/find/proof.f.d.ts +1 -0
  118. package/fs/types/btree/find/{test.f.js → proof.f.js} +1 -1
  119. package/fs/types/btree/{test.f.d.ts → proof.f.d.ts} +1 -2
  120. package/fs/types/btree/{test.f.js → proof.f.js} +1 -1
  121. package/fs/types/btree/remove/proof.f.d.ts +4 -0
  122. package/fs/types/btree/remove/{test.f.js → proof.f.js} +1 -1
  123. package/fs/types/btree/set/proof.f.d.ts +1 -0
  124. package/fs/types/btree/set/{test.f.js → proof.f.js} +1 -1
  125. package/fs/types/btree/types/module.f.d.ts +8 -0
  126. package/fs/types/btree/types/module.f.js +8 -0
  127. package/fs/types/byte_set/{test.f.d.ts → proof.f.d.ts} +1 -2
  128. package/fs/types/byte_set/{test.f.js → proof.f.js} +1 -1
  129. package/fs/types/effects/module.f.d.ts +17 -0
  130. package/fs/types/effects/module.f.js +17 -0
  131. package/fs/types/effects/node/module.f.d.ts +54 -5
  132. package/fs/types/effects/node/module.f.js +4 -1
  133. package/fs/types/effects/node/{test.f.d.ts → proof.f.d.ts} +1 -2
  134. package/fs/types/effects/node/{test.f.js → proof.f.js} +1 -1
  135. package/fs/types/effects/node/virtual/module.f.js +1 -0
  136. package/fs/types/effects/proof.f.d.ts +11 -0
  137. package/fs/types/effects/proof.f.js +57 -0
  138. package/fs/types/function/compare/proof.f.d.ts +1 -0
  139. package/fs/types/function/compare/{test.f.js → proof.f.js} +1 -1
  140. package/fs/types/function/operator/proof.f.d.ts +12 -0
  141. package/fs/types/function/operator/{test.f.js → proof.f.js} +11 -10
  142. package/fs/types/function/proof.f.d.ts +1 -0
  143. package/fs/types/function/{test.f.js → proof.f.js} +1 -1
  144. package/fs/types/list/{test.f.d.ts → proof.f.d.ts} +1 -2
  145. package/fs/types/list/{test.f.js → proof.f.js} +1 -1
  146. package/fs/types/map/proof.f.d.ts +4 -0
  147. package/fs/types/map/{test.f.js → proof.f.js} +1 -1
  148. package/fs/types/monoid/{test.f.d.ts → proof.f.d.ts} +1 -2
  149. package/fs/types/monoid/{test.f.js → proof.f.js} +1 -1
  150. package/fs/types/nibble_set/{test.f.d.ts → proof.f.d.ts} +1 -2
  151. package/fs/types/nibble_set/{test.f.js → proof.f.js} +1 -1
  152. package/fs/types/nominal/proof.f.d.ts +4 -0
  153. package/fs/types/nominal/{test.f.js → proof.f.js} +1 -1
  154. package/fs/types/nullable/proof.f.d.ts +1 -0
  155. package/fs/types/nullable/{test.f.js → proof.f.js} +1 -1
  156. package/fs/types/number/{test.f.d.ts → proof.f.d.ts} +1 -2
  157. package/fs/types/number/{test.f.js → proof.f.js} +1 -1
  158. package/fs/types/object/{test.f.d.ts → proof.f.d.ts} +1 -2
  159. package/fs/types/object/{test.f.js → proof.f.js} +1 -1
  160. package/fs/types/ordered_map/{test.f.d.ts → proof.f.d.ts} +1 -2
  161. package/fs/types/ordered_map/{test.f.js → proof.f.js} +1 -1
  162. package/fs/types/patricia_trie/{test.f.d.ts → proof.f.d.ts} +1 -2
  163. package/fs/types/patricia_trie/{test.f.js → proof.f.js} +1 -1
  164. package/fs/types/prime_field/{test.f.d.ts → proof.f.d.ts} +1 -2
  165. package/fs/types/prime_field/{test.f.js → proof.f.js} +1 -1
  166. package/fs/types/range/proof.f.d.ts +1 -0
  167. package/fs/types/range/{test.f.js → proof.f.js} +1 -1
  168. package/fs/types/range_map/{test.f.d.ts → proof.f.d.ts} +1 -2
  169. package/fs/types/range_map/{test.f.js → proof.f.js} +1 -1
  170. package/fs/types/result/proof.f.d.ts +5 -0
  171. package/fs/types/result/{test.f.js → proof.f.js} +18 -2
  172. package/fs/types/rtti/parse/{test.f.d.ts → proof.f.d.ts} +1 -2
  173. package/fs/types/rtti/parse/{test.f.js → proof.f.js} +1 -1
  174. package/fs/types/rtti/{test.f.d.ts → proof.f.d.ts} +1 -2
  175. package/fs/types/rtti/{test.f.js → proof.f.js} +1 -1
  176. package/fs/types/rtti/ts/{test.f.d.ts → proof.f.d.ts} +1 -2
  177. package/fs/types/rtti/ts/{test.f.js → proof.f.js} +1 -1
  178. package/fs/types/rtti/validate/{test.f.d.ts → proof.f.d.ts} +1 -2
  179. package/fs/types/rtti/validate/{test.f.js → proof.f.js} +1 -1
  180. package/fs/types/sorted_list/{test.f.d.ts → proof.f.d.ts} +1 -2
  181. package/fs/types/sorted_list/{test.f.js → proof.f.js} +1 -1
  182. package/fs/types/sorted_set/{test.f.d.ts → proof.f.d.ts} +1 -2
  183. package/fs/types/sorted_set/{test.f.js → proof.f.js} +1 -1
  184. package/fs/types/string/{test.f.d.ts → proof.f.d.ts} +1 -2
  185. package/fs/types/string/{test.f.js → proof.f.js} +1 -1
  186. package/fs/types/string_set/{test.f.d.ts → proof.f.d.ts} +1 -2
  187. package/fs/types/string_set/{test.f.js → proof.f.js} +1 -1
  188. package/fs/types/ts/{test.f.d.ts → proof.f.d.ts} +20 -0
  189. package/fs/types/ts/{test.f.js → proof.f.js} +1 -0
  190. package/fs/types/uint8array/{test.f.d.ts → proof.f.d.ts} +1 -2
  191. package/fs/types/uint8array/{test.f.js → proof.f.js} +1 -1
  192. package/issues/demo/sample/{test.f.js → proof.f.js} +1 -1
  193. package/issues/{test.f.d.ts → proof.f.d.ts} +1 -2
  194. package/issues/{test.f.js → proof.f.js} +1 -1
  195. package/nanvm-lib/tests/{test.f.d.ts → proof.f.d.ts} +1 -2
  196. package/nanvm-lib/tests/{test.f.js → proof.f.js} +1 -1
  197. package/nanvm-lib/tests/vm/{test.f.d.ts → proof.f.d.ts} +1 -2
  198. package/nanvm-lib/tests/vm/{test.f.js → proof.f.js} +1 -1
  199. package/package.json +2 -2
  200. package/fs/base128/test.f.d.ts +0 -2
  201. package/fs/bnf/test.f.d.ts +0 -4
  202. package/fs/cas/test.f.d.ts +0 -2
  203. package/fs/cas/test.f.js +0 -1
  204. package/fs/dev/version/test.f.d.ts +0 -4
  205. package/fs/fsm/test.f.d.ts +0 -5
  206. package/fs/path/test.f.d.ts +0 -3
  207. package/fs/text/ascii/test.f.d.ts +0 -4
  208. package/fs/text/sgr/test.f.d.ts +0 -2
  209. package/fs/types/btree/find/test.f.d.ts +0 -2
  210. package/fs/types/btree/remove/test.f.d.ts +0 -5
  211. package/fs/types/btree/set/test.f.d.ts +0 -2
  212. package/fs/types/function/compare/test.f.d.ts +0 -2
  213. package/fs/types/function/operator/test.f.d.ts +0 -10
  214. package/fs/types/function/test.f.d.ts +0 -2
  215. package/fs/types/map/test.f.d.ts +0 -5
  216. package/fs/types/nominal/test.f.d.ts +0 -5
  217. package/fs/types/nullable/test.f.d.ts +0 -2
  218. package/fs/types/range/test.f.d.ts +0 -2
  219. package/fs/types/result/test.f.d.ts +0 -2
@@ -1,14 +1,20 @@
1
1
  /**
2
2
  * Test-framework helpers for running and reporting FunctionalScript tests.
3
3
  *
4
+ * Two parallel execution paths:
5
+ * - `runModule` / `Reporter<O>` — self-hosted Effects runner used by `fjs t`;
6
+ * sandboxes each leaf call individually and accumulates `TestState`.
7
+ * - `registerModule` / `TestContext` — registers tests with an external
8
+ * framework (Node `--test`, Bun, Playwright) at import time; the framework
9
+ * owns scheduling and pass/fail counting.
10
+ *
4
11
  * @module
5
12
  */
6
13
  import { reset, fgGreen, fgRed, bold, csiWrite } from "../../text/sgr/module.f.js";
7
- import { all, sandbox, test } from "../../types/effects/node/module.f.js";
8
- import { pure, do_ } from "../../types/effects/module.f.js";
9
- import { loadModuleMap } from "../module.f.js";
14
+ import { all, awaitIfPromise, sandbox, test } from "../../types/effects/node/module.f.js";
15
+ import { pure } from "../../types/effects/module.f.js";
16
+ import { loadModuleMap, shouldLoad } from "../module.f.js";
10
17
  import { invert } from "../../types/result/module.f.js";
11
- export const isTest = (s) => s.endsWith('test.f.js') || s.endsWith('test.f.ts');
12
18
  const addPass = (delta) => (ts) => ({ ...ts, time: ts.time + delta, pass: ts.pass + 1 });
13
19
  const addFail = (delta) => (ts) => ({ ...ts, time: ts.time + delta, fail: ts.fail + 1 });
14
20
  const timeFormat = (a) => {
@@ -20,6 +26,14 @@ const timeFormat = (a) => {
20
26
  const e = x.substring(s);
21
27
  return `${b}.${e} ms`;
22
28
  };
29
+ /**
30
+ * Converts an arbitrary JS value into a `TestSet`.
31
+ *
32
+ * - Zero-argument functions become a `TestEntry`; the `throws` flag is set if
33
+ * `throws` is already `true` or the function's `.name === 'throw'`.
34
+ * - Non-null objects become an array of `[key, value]` pairs to recurse into.
35
+ * - All other values (including functions with parameters) produce an empty array.
36
+ */
23
37
  export const parseTestSet = (throws, x) => {
24
38
  switch (typeof x) {
25
39
  case 'function': {
@@ -51,19 +65,28 @@ export const collectTests = (path, throws, v) => {
51
65
  }
52
66
  return [[path, set]];
53
67
  };
68
+ /**
69
+ * Registers all tests reachable from module export `v` (keyed by `k`) with
70
+ * the given `TestContext`.
71
+ *
72
+ * Unlike `runModule`, which sandboxes only the leaf function, `registerModule`
73
+ * lets the external framework own scheduling: each registered test callback
74
+ * calls `fn`, then recursively registers any sub-trees returned by the function.
75
+ * This is the correct model for Node `--test`, Bun, and Playwright, where tests
76
+ * must be declared upfront and the framework drives execution.
77
+ */
54
78
  export const registerModule = (ctx, k, v) => {
55
- const registerOne = (ctx, [path, { fn, throws }]) => test(ctx, fmtImport(k, path), throws, (t) => {
79
+ const registerOne = (ctx, [path, { fn, throws }]) => test(ctx, fmtImport(k, path), throws, (t) => awaitIfPromise(fn())
80
+ .step(resolved => {
56
81
  if (throws) {
57
- fn();
58
82
  return pure(undefined);
59
83
  }
60
- const r = fn();
61
- const sub = collectTests([...path, null], false, r);
84
+ const sub = collectTests([...path, null], false, resolved);
62
85
  if (sub.length === 0) {
63
86
  return pure(undefined);
64
87
  }
65
88
  return all(...sub.map(e => registerOne(t, e))).step(() => pure(undefined));
66
- });
89
+ }));
67
90
  const tests = collectTests([], false, v);
68
91
  if (tests.length === 0) {
69
92
  return pure(undefined);
@@ -99,16 +122,33 @@ const runModule = ({ result, test }) => (k, v) => (ts) => {
99
122
  .step(delta => pure(mergeState(ts, delta)));
100
123
  };
101
124
  const { entries } = Object;
125
+ /**
126
+ * Runs all test modules in `moduleMap` whose names pass `isTest`, accumulates
127
+ * pass/fail/time via `reporter`, and returns an exit code (0 = all passed,
128
+ * 1 = at least one failure).
129
+ */
102
130
  export const runModuleMap = (reporter) => (moduleMap) => {
103
131
  const { summary } = reporter;
104
- const modules = entries(moduleMap).filter(([k]) => isTest(k));
105
- return modules.reduce((acc, [k, v]) => acc.step(runModule(reporter)(k, v)), pure({ time: 0, pass: 0, fail: 0 }))
132
+ const modules = entries(moduleMap)
133
+ .flatMap(([k, v]) => v.proof !== undefined ? [[k, v.proof]] : []);
134
+ return all(...modules.map(([k, v]) => runModule(reporter)(k, v)(zero)))
135
+ .step(m => pure(m.reduce(mergeState, zero)))
106
136
  .step(ts => summary(ts.pass, ts.fail, ts.time)
107
137
  .step(() => pure(ts.fail !== 0 ? 1 : 0)));
108
138
  };
139
+ /**
140
+ * Discovers all test modules via `loadModuleMap`, then runs them through
141
+ * `runModuleMap`. The composed effect is a `NodeProgram` entry point for the
142
+ * `fjs t` test runner.
143
+ */
109
144
  export const testAll = (reporter) => options => loadModuleMap(options.env).step(runModuleMap(reporter));
110
- export const registerModuleMap = (ctx, moduleMap) => {
111
- const modules = entries(moduleMap).filter(([k]) => isTest(k));
145
+ /**
146
+ * Registers all modules in `moduleMap` that export a `proof` property with
147
+ * `ctx`. Delegates to `registerModule` for each matching entry.
148
+ */
149
+ const registerModuleMap = (ctx) => (moduleMap) => {
150
+ const modules = entries(moduleMap)
151
+ .flatMap(([k, v]) => v.proof !== undefined ? [[k, v.proof]] : []);
112
152
  if (modules.length === 0) {
113
153
  return pure(undefined);
114
154
  }
@@ -116,7 +156,9 @@ export const registerModuleMap = (ctx, moduleMap) => {
116
156
  };
117
157
  const isAlpha = (c) => (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c === '_' || c === '$';
118
158
  const isDigit = (c) => c >= '0' && c <= '9';
159
+ /** Returns `true` if `s` is a non-negative decimal integer without a leading zero. */
119
160
  export const isInteger = (s) => s.length > 0 && [...s].every(isDigit) && (s === '0' || s[0] !== '0');
161
+ /** Returns `true` if `s` is a valid JS identifier (ASCII subset: `[A-Za-z_$][A-Za-z0-9_$]*`). */
120
162
  export const isIdentifier = (s) => s.length > 0 && isAlpha(s[0]) && [...s.slice(1)].every(c => isAlpha(c) || isDigit(c));
121
163
  const fmtKey = (k) => k === null ? '()'
122
164
  : isInteger(k) ? `[${k}]`
@@ -131,10 +173,10 @@ const fmtKey = (k) => k === null ? '()'
131
173
  export const fmtPath = (path) => path.reduce((acc, k) => acc + fmtKey(k), '');
132
174
  /**
133
175
  * Formats a fully-qualified test identifier as a JS-like expression, e.g.
134
- * `import("./math.test.f.ts").add()` or `import("./a.test.f.ts").users[3].name()`.
176
+ * `import("./math.proof.f.ts").add()` or `import("./a.proof.f.ts").users[3].name()`.
135
177
  * Self-contained per line — suitable for parallel output and as a CLI filter argument.
136
178
  */
137
- export const fmtImport = (file, path) => `import(${JSON.stringify(file)})${fmtPath(path)}()`;
179
+ export const fmtImport = (file, path) => `import(${JSON.stringify(file)}).proof${fmtPath(path)}()`;
138
180
  /**
139
181
  * Renders a key chain for terminal output: `| ` per level of depth, followed
140
182
  * by the last segment formatted as a bare integer, a bare identifier, or a
@@ -160,6 +202,10 @@ export const ghEscape = (s) => s.replaceAll('%', '%25')
160
202
  .replaceAll(',', '%2C')
161
203
  .replaceAll('\r', '%0D')
162
204
  .replaceAll('\n', '%0A');
205
+ /**
206
+ * Default `Reporter.test` implementation: sandboxes `fn` once and inverts the
207
+ * result when `throws` is `true` (caught error → pass, clean return → fail).
208
+ */
163
209
  export const defaultTest = (file, path, { fn, throws }) => sandbox(fn)
164
210
  .step(r => pure(throws ? { ...r, result: invert(r.result) } : r));
165
211
  const fmtResultLine = (file, path, color, label, duration) => `${fmtImport(file, path)}: ${color}${label}${reset}, ${timeFormat(duration)}`;
@@ -195,9 +241,20 @@ export const defaultReporter = (options) => {
195
241
  test: defaultTest,
196
242
  };
197
243
  };
244
+ /** The `fjs t` entry point: runs all tests using `defaultReporter`. */
198
245
  export const main = options => testAll(defaultReporter(options))(options);
199
- export const register = o => loadModuleMap(o.env)
200
- .step(m => registerModuleMap(o.engine === 'bun' ? o.bunTestContext :
201
- o.engine === 'playwright' ? o.playwrightTestContext :
202
- o.testContext, m))
203
- .step(() => pure(0));
246
+ /**
247
+ * Entry point for external test frameworks (Node `--test`, Bun, Playwright).
248
+ *
249
+ * Discovers test modules via `loadModuleMap`, then registers each with the
250
+ * framework-appropriate `TestContext` selected from `NodeProgramOptions`
251
+ * based on the detected `engine`.
252
+ */
253
+ export const register = o => {
254
+ const r = registerModuleMap(o.engine === 'bun' ? o.bunTestContext :
255
+ o.engine === 'playwright' ? o.playwrightTestContext :
256
+ o.testContext);
257
+ return loadModuleMap(o.env)
258
+ .step(r)
259
+ .step(() => pure(0));
260
+ };
@@ -15,8 +15,34 @@ export declare const githubReporterOutput: () => void;
15
15
  export declare const helpers: {
16
16
  isInteger: () => void;
17
17
  isIdentifier: () => void;
18
+ shouldLoad: () => void;
18
19
  fmtImport: () => void;
19
20
  fmtPath: () => void;
20
21
  fmtTerm: () => void;
21
22
  ghEscape: () => void;
22
23
  };
24
+ export declare const proof: {
25
+ flat: () => void;
26
+ nested: () => void;
27
+ throwKey: () => void;
28
+ throwKeyFail: () => void;
29
+ mixedPassFail: () => void;
30
+ returnValueSubTree: () => void;
31
+ arrayKeys: () => void;
32
+ nonTestFilesSkipped: () => void;
33
+ multipleFiles: () => void;
34
+ throwByFunctionName: () => void;
35
+ namedExports: () => void;
36
+ defaultReporterOutput: () => void;
37
+ defaultReporterFailOutput: () => void;
38
+ githubReporterOutput: () => void;
39
+ helpers: {
40
+ isInteger: () => void;
41
+ isIdentifier: () => void;
42
+ shouldLoad: () => void;
43
+ fmtImport: () => void;
44
+ fmtPath: () => void;
45
+ fmtTerm: () => void;
46
+ ghEscape: () => void;
47
+ };
48
+ };
@@ -3,6 +3,7 @@ import { emptyState } from "../../types/effects/node/virtual/module.f.js";
3
3
  import { virtual } from "../../types/effects/node/virtual/module.f.js";
4
4
  import { assert, assertEq, todo } from "../module.f.js";
5
5
  import { testAll, defaultReporter, fmtPath, fmtTerm, fmtImport, ghEscape, isInteger, isIdentifier, defaultTest, } from "./module.f.js";
6
+ import { shouldLoad } from "../module.f.js";
6
7
  const makeReporter = () => {
7
8
  const events = [];
8
9
  const reporter = {
@@ -42,7 +43,7 @@ const runMain = (dir, github = false) => {
42
43
  // flat object: two passing tests
43
44
  export const flat = () => {
44
45
  const [events, exit] = run({
45
- 'a.test.f.ts': () => ({ a: ok0, b: ok1 }),
46
+ 'a.proof.f.ts': () => ({ proof: { a: ok0, b: ok1 } }),
46
47
  });
47
48
  assertEq(exit, 0);
48
49
  const [e0, e1, e2] = events;
@@ -56,7 +57,7 @@ export const flat = () => {
56
57
  // nested object: leaf tests carry the full path including the sub-tree key
57
58
  export const nested = () => {
58
59
  const [events, exit] = run({
59
- 'n.test.f.ts': () => ({ math: { add: ok0, sub: ok0 } }),
60
+ 'n.proof.f.ts': () => ({ proof: { math: { add: ok0, sub: ok0 } } }),
60
61
  });
61
62
  assertEq(exit, 0);
62
63
  const [e0, e1, e2] = events;
@@ -70,7 +71,7 @@ export const nested = () => {
70
71
  // throw key: tests inside 'throw' pass on error result
71
72
  export const throwKey = () => {
72
73
  const [events, exit] = run({
73
- 't.test.f.ts': () => ({ throw: { a: fail0 } }),
74
+ 't.proof.f.ts': () => ({ proof: { throw: { a: fail0 } } }),
74
75
  });
75
76
  assertEq(exit, 0);
76
77
  const [e0, e1] = events;
@@ -83,7 +84,7 @@ export const throwKey = () => {
83
84
  // throw key fails when test does not throw (returns ok in throw context)
84
85
  export const throwKeyFail = () => {
85
86
  const [events, exit] = run({
86
- 't.test.f.ts': () => ({ throw: { a: ok0 } }),
87
+ 't.proof.f.ts': () => ({ proof: { throw: { a: ok0 } } }),
87
88
  });
88
89
  assertEq(exit, 1);
89
90
  const [e0, e1] = events;
@@ -95,7 +96,7 @@ export const throwKeyFail = () => {
95
96
  // mixed pass/fail updates summary counts
96
97
  export const mixedPassFail = () => {
97
98
  const [events, exit] = run({
98
- 'm.test.f.ts': () => ({ good: ok0, bad: fail0 }),
99
+ 'm.proof.f.ts': () => ({ proof: { good: ok0, bad: fail0 } }),
99
100
  });
100
101
  assertEq(exit, 1);
101
102
  const summary = events[events.length - 1];
@@ -108,11 +109,13 @@ export const mixedPassFail = () => {
108
109
  export const returnValueSubTree = () => {
109
110
  const inner = () => ({ result: ['ok', undefined], duration: 0 });
110
111
  const [events, exit] = run({
111
- 'r.test.f.ts': () => ({
112
- outer: () => ({
113
- result: ['ok', { inner }],
114
- duration: 0,
115
- }),
112
+ 'r.proof.f.ts': () => ({
113
+ proof: {
114
+ outer: () => ({
115
+ result: ['ok', { inner }],
116
+ duration: 0,
117
+ }),
118
+ }
116
119
  }),
117
120
  });
118
121
  // outer passes, then inner (from return value) also passes
@@ -126,7 +129,7 @@ export const returnValueSubTree = () => {
126
129
  // integer-indexed array keys appear as numeric path segments
127
130
  export const arrayKeys = () => {
128
131
  const [events, exit] = run({
129
- 'a.test.f.ts': () => ({ arr: [ok0, ok0] }),
132
+ 'a.proof.f.ts': () => ({ proof: { arr: [ok0, ok0] } }),
130
133
  });
131
134
  assertEq(exit, 0);
132
135
  const passEvents = events.filter(e => e[0] === 'result');
@@ -134,22 +137,24 @@ export const arrayKeys = () => {
134
137
  assertEq(passEvents[0][2][1], '0');
135
138
  assertEq(passEvents[1][2][1], '1');
136
139
  };
137
- // non-test files are skipped (only files ending in test.f.ts/js are loaded)
140
+ // non-proof files are skipped: plain `.ts` is not loaded; `.f.ts` without
141
+ // a `proof` export is loaded but produces no events
138
142
  export const nonTestFilesSkipped = () => {
139
143
  const [events, exit] = run({
140
- 'helper.ts': () => ({ a: ok0 }),
141
- 'b.test.f.ts': () => ({ x: ok0 }),
144
+ 'helper.ts': () => ({ a: ok0 }), // not loaded (plain .ts)
145
+ 'module.f.ts': () => ({ someExport: ok0 }), // loaded, no proof → skipped
146
+ 'b.proof.f.ts': () => ({ proof: { x: ok0 } }), // loaded, has proof → runs
142
147
  });
143
148
  assertEq(exit, 0);
144
149
  const results = events.filter(e => e[0] === 'result');
145
150
  assertEq(results.length, 1);
146
- assertEq(results[0][1], './b.test.f.ts');
151
+ assertEq(results[0][1], './b.proof.f.ts');
147
152
  };
148
153
  // multiple test files each produce result events
149
154
  export const multipleFiles = () => {
150
155
  const [events, exit] = run({
151
- 'a.test.f.ts': () => ({ x: ok0 }),
152
- 'b.test.f.ts': () => ({ y: ok0 }),
156
+ 'a.proof.f.ts': () => ({ proof: { x: ok0 } }),
157
+ 'b.proof.f.ts': () => ({ proof: { y: ok0 } }),
153
158
  });
154
159
  assertEq(exit, 0);
155
160
  const results = events.filter(e => e[0] === 'result');
@@ -164,51 +169,51 @@ export const throwByFunctionName = () => {
164
169
  // const named = ({ throw: () => fail0() }).throw
165
170
  const x = { throw: () => fail0() };
166
171
  const [events, exit] = run({
167
- 't.test.f.ts': () => ({ here: x.throw }),
172
+ 't.proof.f.ts': () => ({ proof: { here: x.throw } }),
168
173
  });
169
174
  assertEq(exit, 0);
170
175
  const passEvents = events.filter(e => e[0] === 'result');
171
176
  assertEq(passEvents.length, 1);
172
177
  assertEq(passEvents[0][2][0], 'here');
173
178
  };
174
- // every module export — `default` and named becomes a top-level path segment
179
+ // only the `proof` export is used; other module properties are ignored
175
180
  export const namedExports = () => {
176
181
  const [events, exit] = run({
177
- 'e.test.f.ts': () => ({ default: ok0, helper: ok0 }),
182
+ 'e.proof.f.ts': () => ({ proof: { a: ok0, b: ok0 }, other: ok0 }),
178
183
  });
179
184
  assertEq(exit, 0);
180
185
  const passEvents = events.filter(e => e[0] === 'result');
181
- assertEq(passEvents.length, 2);
182
- assertEq(passEvents[0][2][0], 'default');
183
- assertEq(passEvents[1][2][0], 'helper');
186
+ assertEq(passEvents.length, 2); // `other` is ignored
187
+ assertEq(passEvents[0][2][0], 'a');
188
+ assertEq(passEvents[1][2][0], 'b');
184
189
  };
185
190
  // the default (non-GitHub) reporter formats module/pass/summary lines on stdout
186
191
  export const defaultReporterOutput = () => {
187
192
  const [stdout, stderr, exit] = runMain({
188
- 'a.test.f.ts': () => ({ x: ok0 }),
193
+ 'a.proof.f.ts': () => ({ proof: { x: ok0 } }),
189
194
  });
190
195
  assertEq(exit, 0);
191
196
  assertEq(stderr, '');
192
- assertEq(stdout, 'import("./a.test.f.ts").x(): ok, 0.0000 ms\n'
197
+ assertEq(stdout, 'import("./a.proof.f.ts").proof.x(): ok, 0.0000 ms\n'
193
198
  + 'Number of tests: pass: 1, fail: 0, total: 1\n'
194
199
  + 'Time: 0.0000 ms\n');
195
200
  };
196
201
  // a failure on the non-GitHub reporter writes the error to stderr, not stdout
197
202
  export const defaultReporterFailOutput = () => {
198
203
  const [, stderr, exit] = runMain({
199
- 'a.test.f.ts': () => ({ bad: fail0 }),
204
+ 'a.proof.f.ts': () => ({ proof: { bad: fail0 } }),
200
205
  });
201
206
  assertEq(exit, 1);
202
- assertEq(stderr, 'import("./a.test.f.ts").bad(): error, 0.0000 ms\noops\n');
207
+ assertEq(stderr, 'import("./a.proof.f.ts").proof.bad(): error, 0.0000 ms\noops\n');
203
208
  };
204
209
  // the GitHub reporter emits an `::error` annotation with a percent-encoded
205
210
  // title (the JSON path) and message
206
211
  export const githubReporterOutput = () => {
207
212
  const [, stderr, exit] = runMain({
208
- 's.test.f.ts': () => ({ 'a:b,c%d': fail0 }),
213
+ 's.proof.f.ts': () => ({ proof: { 'a:b,c%d': fail0 } }),
209
214
  }, true);
210
215
  assertEq(exit, 1);
211
- assertEq(stderr, '::error file=./s.test.f.ts,line=1,title=import("./s.test.f.ts")["a%3Ab%2Cc%25d"]()::oops\n');
216
+ assertEq(stderr, '::error file=./s.proof.f.ts,line=1,title=import("./s.proof.f.ts").proof["a%3Ab%2Cc%25d"]()::oops\n');
212
217
  };
213
218
  // direct unit tests for the pure path-format helpers
214
219
  export const helpers = {
@@ -229,12 +234,32 @@ export const helpers = {
229
234
  assert(!isIdentifier('1a'));
230
235
  assert(!isIdentifier('a-b'));
231
236
  },
237
+ shouldLoad: () => {
238
+ // all .f.ts / .f.js — FS modules are safe to bulk-load
239
+ assert(shouldLoad('module.f.ts'));
240
+ assert(shouldLoad('module.f.js'));
241
+ assert(shouldLoad('a.proof.f.ts'));
242
+ assert(shouldLoad('dir/module.f.ts'));
243
+ // vanilla opt-in by filename
244
+ assert(shouldLoad('proof.ts'));
245
+ assert(shouldLoad('proof.js'));
246
+ assert(shouldLoad('proof.mts'));
247
+ assert(shouldLoad('proof.mjs'));
248
+ assert(shouldLoad('math.proof.ts'));
249
+ assert(shouldLoad('math.proof.js'));
250
+ assert(shouldLoad('math.proof.mts'));
251
+ assert(shouldLoad('dir/math.proof.ts'));
252
+ // non-FS, non-proof vanilla files are not loaded
253
+ assert(!shouldLoad('helper.ts'));
254
+ assert(!shouldLoad('module.ts'));
255
+ assert(!shouldLoad('proof.tsx'));
256
+ },
232
257
  fmtImport: () => {
233
- assertEq(fmtImport('./a.test.f.ts', []), 'import("./a.test.f.ts")()');
234
- assertEq(fmtImport('./a.test.f.ts', ['math', 'add']), 'import("./a.test.f.ts").math.add()');
235
- assertEq(fmtImport('./a.test.f.ts', ['users', '3']), 'import("./a.test.f.ts").users[3]()');
236
- assertEq(fmtImport('./a.test.f.ts', ['x', 'hello world']), 'import("./a.test.f.ts").x["hello world"]()');
237
- assertEq(fmtImport('./a.test.f.ts', ['outer', null, 'inner']), 'import("./a.test.f.ts").outer().inner()');
258
+ assertEq(fmtImport('./a.proof.f.ts', []), 'import("./a.proof.f.ts").proof()');
259
+ assertEq(fmtImport('./a.proof.f.ts', ['math', 'add']), 'import("./a.proof.f.ts").proof.math.add()');
260
+ assertEq(fmtImport('./a.proof.f.ts', ['users', '3']), 'import("./a.proof.f.ts").proof.users[3]()');
261
+ assertEq(fmtImport('./a.proof.f.ts', ['x', 'hello world']), 'import("./a.proof.f.ts").proof.x["hello world"]()');
262
+ assertEq(fmtImport('./a.proof.f.ts', ['outer', null, 'inner']), 'import("./a.proof.f.ts").proof.outer().inner()');
238
263
  },
239
264
  fmtPath: () => {
240
265
  assertEq(fmtPath([]), '');
@@ -257,3 +282,20 @@ export const helpers = {
257
282
  assertEq(ghEscape('a%b:c,d'), 'a%25b%3Ac%2Cd');
258
283
  },
259
284
  };
285
+ export const proof = {
286
+ flat,
287
+ nested,
288
+ throwKey,
289
+ throwKeyFail,
290
+ mixedPassFail,
291
+ returnValueSubTree,
292
+ arrayKeys,
293
+ nonTestFilesSkipped,
294
+ multipleFiles,
295
+ throwByFunctionName,
296
+ namedExports,
297
+ defaultReporterOutput,
298
+ defaultReporterFailOutput,
299
+ githubReporterOutput,
300
+ helpers
301
+ };
@@ -0,0 +1,4 @@
1
+ export declare const withSubtests: () => Promise<{
2
+ sub1: () => void;
3
+ sub2: () => never;
4
+ }>;
@@ -0,0 +1,7 @@
1
+ export const withSubtests = async () => {
2
+ await new Promise(resolve => setTimeout(resolve, 10));
3
+ return {
4
+ sub1: () => { },
5
+ sub2: () => { throw 'sub-test failure'; },
6
+ };
7
+ };
@@ -0,0 +1,4 @@
1
+ export declare const withSubtests: () => Promise<{
2
+ sub1: () => void;
3
+ sub2: () => void;
4
+ }>;
@@ -0,0 +1,7 @@
1
+ export const withSubtests = async () => {
2
+ await new Promise(resolve => setTimeout(resolve, 10));
3
+ return {
4
+ sub1: () => { },
5
+ sub2: () => { },
6
+ };
7
+ };
@@ -0,0 +1 @@
1
+ export declare const sleep_fail: () => Promise<never>;
@@ -0,0 +1,4 @@
1
+ export const sleep_fail = async () => {
2
+ await new Promise(resolve => setTimeout(resolve, 10));
3
+ throw 'async failure';
4
+ };
@@ -0,0 +1 @@
1
+ export declare const sleep: () => Promise<void>;
@@ -0,0 +1,3 @@
1
+ export const sleep = async () => {
2
+ await new Promise(resolve => setTimeout(resolve, 10));
3
+ };
@@ -0,0 +1,3 @@
1
+ export declare const thenableResolves: () => {
2
+ then(resolve: (v: undefined) => void): void;
3
+ };
@@ -0,0 +1,9 @@
1
+ // A test that returns a thenable (Promise-like object, not a real Promise).
2
+ // Per FunctionalScript convention, thenables are treated as plain values —
3
+ // not awaited. Both sandbox (fjs) and registerModule (node/bun/deno/playwright)
4
+ // must exit 0: the thenable object is walked as a sub-tree whose only key
5
+ // `then` is a function with parameters, so no leaf tests are found and the
6
+ // test trivially passes.
7
+ export const thenableResolves = () => ({
8
+ then(resolve) { resolve(undefined); }
9
+ });
@@ -0,0 +1,3 @@
1
+ export declare const shouldPass: () => {
2
+ then: () => string;
3
+ };
@@ -0,0 +1,3 @@
1
+ export const shouldPass = () => ({
2
+ then: () => 'ok'
3
+ });
@@ -0,0 +1,3 @@
1
+ export declare const proof: {
2
+ new: () => void;
3
+ };
@@ -77,7 +77,7 @@ const e = '{\n' +
77
77
  ' "typescript": "^4.7.4"\n' +
78
78
  ' }\n' +
79
79
  '}';
80
- export default {
80
+ export const proof = {
81
81
  new: () => {
82
82
  const w = (name) => {
83
83
  const fn = `${name}.json`;
@@ -1,8 +1,7 @@
1
- declare const _default: {
1
+ export declare const proof: {
2
2
  test: () => void;
3
3
  testCref: () => void;
4
4
  testAref: () => void;
5
5
  testArray: () => void;
6
6
  testObj: () => void;
7
7
  };
8
- export default _default;
@@ -1,7 +1,7 @@
1
1
  import { sort } from "../../types/object/module.f.js";
2
2
  import { run } from "./module.f.js";
3
3
  import { stringifyAsTree } from "../serializer/module.f.js";
4
- export default {
4
+ export const proof = {
5
5
  test: () => {
6
6
  const djs = run([1])([]);
7
7
  const result = stringifyAsTree(sort)(djs);
@@ -1,4 +1,4 @@
1
- declare const _default: {
1
+ export declare const proof: {
2
2
  valid: (() => void)[];
3
3
  invalid: (() => void)[];
4
4
  errorMetadata: (() => void)[];
@@ -11,4 +11,3 @@ declare const _default: {
11
11
  invalidWithArgs: (() => void)[];
12
12
  comments: (() => void)[];
13
13
  };
14
- export default _default;
@@ -7,7 +7,7 @@ import { stringifyAsTree } from "../serializer/module.f.js";
7
7
  import { stringify } from "../../json/module.f.js";
8
8
  const tokenizeString = s => toArray(tokenize(stringToList(s))(''));
9
9
  const stringifyDjsModule = stringifyAsTree(sort);
10
- export default {
10
+ export const proof = {
11
11
  valid: [
12
12
  () => {
13
13
  const tokenList = tokenizeString('export default null');
@@ -1,4 +1,4 @@
1
- declare const _default: {
1
+ export declare const proof: {
2
2
  tooFewArgs: {
3
3
  noArgs: () => void;
4
4
  oneArg: () => void;
@@ -8,4 +8,3 @@ declare const _default: {
8
8
  fileNotFound: () => void;
9
9
  parseError: () => void;
10
10
  };
11
- export default _default;
@@ -9,7 +9,7 @@ const readOutput = (root, path) => {
9
9
  }
10
10
  return utf8ToString(file);
11
11
  };
12
- export default {
12
+ export const proof = {
13
13
  tooFewArgs: {
14
14
  noArgs: () => {
15
15
  const [state, code] = virtual(emptyState)(compile([]));
@@ -7,11 +7,11 @@ import type { Unknown } from '../module.f.ts';
7
7
  import type { Entry as ObjectEntry } from '../../types/object/module.f.ts';
8
8
  import { type List } from '../../types/list/module.f.ts';
9
9
  export declare const undefinedSerialize: string[];
10
- type RefCounter = [number, number, boolean];
10
+ type RefCounter = readonly [number, number];
11
11
  type Entry = ObjectEntry<Unknown>;
12
12
  type Entries = List<Entry>;
13
13
  type MapEntries = (entries: Entries) => Entries;
14
- type Refs = Map<Unknown, RefCounter>;
14
+ type Refs = ReadonlyMap<Unknown, RefCounter>;
15
15
  export declare const serializeWithoutConst: (mapEntries: MapEntries) => (value: Unknown) => List<string>;
16
16
  export declare const stringify: (sort: MapEntries) => (djs: Unknown) => string;
17
17
  export declare const stringifyAsTree: (mapEntries: MapEntries) => (value: Unknown) => string;