functionalscript 0.45.0 → 0.46.1

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 (296) hide show
  1. package/README.md +5 -3
  2. package/fjs/asn.1/module.f.mjs +8 -1
  3. package/fjs/asn.1/proof.f.d.mts +10 -0
  4. package/fjs/asn.1/proof.f.mjs +16 -0
  5. package/fjs/basen/base128/module.f.mjs +22 -5
  6. package/fjs/bnf/data/module.f.d.mts +32 -6
  7. package/fjs/bnf/data/module.f.mjs +112 -7
  8. package/fjs/bnf/data/proof.f.d.mts +2 -0
  9. package/fjs/bnf/data/proof.f.mjs +74 -3
  10. package/fjs/bnf/data/types.d.ts +20 -2
  11. package/fjs/bnf/descent/module.f.d.mts +28 -14
  12. package/fjs/bnf/descent/module.f.mjs +122 -77
  13. package/fjs/bnf/descent/proof.f.d.mts +2 -0
  14. package/fjs/bnf/descent/proof.f.mjs +117 -31
  15. package/fjs/bnf/descent/types.d.ts +12 -14
  16. package/fjs/bnf/ll1/module.f.d.mts +64 -22
  17. package/fjs/bnf/ll1/module.f.mjs +214 -154
  18. package/fjs/bnf/ll1/proof.f.d.mts +15 -2
  19. package/fjs/bnf/ll1/proof.f.mjs +323 -149
  20. package/fjs/bnf/ll1/types.d.ts +16 -24
  21. package/fjs/bnf/matcher/module.f.d.mts +66 -0
  22. package/fjs/bnf/matcher/module.f.mjs +81 -0
  23. package/fjs/bnf/matcher/proof.f.d.mts +10 -0
  24. package/fjs/bnf/matcher/proof.f.mjs +79 -0
  25. package/fjs/bnf/matcher/types.d.ts +54 -0
  26. package/fjs/bnf/testlib.f.d.mts +31 -0
  27. package/fjs/bnf/testlib.f.mjs +80 -0
  28. package/fjs/cas/cli/module.f.d.mts +1 -1
  29. package/fjs/cas/cli/module.f.mjs +14 -20
  30. package/fjs/cas/cli/proof.f.d.mts +1 -3
  31. package/fjs/cas/cli/proof.f.mjs +44 -33
  32. package/fjs/cas/evo/module.f.d.mts +64 -18
  33. package/fjs/cas/evo/module.f.mjs +148 -70
  34. package/fjs/cas/evo/proof.f.d.mts +10 -1
  35. package/fjs/cas/evo/proof.f.mjs +305 -223
  36. package/fjs/cas/evo/types.d.ts +45 -9
  37. package/fjs/cas/module.f.d.mts +18 -32
  38. package/fjs/cas/module.f.mjs +129 -128
  39. package/fjs/cas/proof.f.d.mts +11 -8
  40. package/fjs/cas/proof.f.mjs +259 -147
  41. package/fjs/cas/types.d.ts +24 -10
  42. package/fjs/ci/config/module.f.d.mts +2 -2
  43. package/fjs/ci/config/module.f.mjs +2 -2
  44. package/fjs/ci/module.f.d.mts +5 -5
  45. package/fjs/ci/module.f.mjs +8 -7
  46. package/fjs/ci/nix/module.f.d.mts +7 -5
  47. package/fjs/ci/nix/module.f.mjs +13 -12
  48. package/fjs/ci/nix/proof.f.mjs +2 -2
  49. package/fjs/ci/proof.f.mjs +9 -5
  50. package/fjs/cli/module.f.d.mts +4 -6
  51. package/fjs/cli/module.f.mjs +4 -8
  52. package/fjs/cli/proof.f.mjs +17 -16
  53. package/fjs/cli/types.d.ts +2 -3
  54. package/fjs/common/monoid/types.d.ts +1 -1
  55. package/fjs/crypto/hmac/module.f.mjs +2 -2
  56. package/fjs/crypto/sha2/module.f.mjs +3 -1
  57. package/fjs/crypto/sha2/proof.f.d.mts +1 -0
  58. package/fjs/crypto/sha2/proof.f.mjs +24 -0
  59. package/fjs/crypto/sha2/types.d.ts +11 -0
  60. package/fjs/crypto/sign/module.f.mjs +2 -2
  61. package/fjs/dev/module.f.d.mts +13 -4
  62. package/fjs/dev/module.f.mjs +56 -27
  63. package/fjs/dev/update/module.f.d.mts +9 -4
  64. package/fjs/dev/update/module.f.mjs +14 -10
  65. package/fjs/dev/update/proof.f.d.mts +1 -3
  66. package/fjs/dev/update/proof.f.mjs +10 -5
  67. package/fjs/djs/module.f.d.mts +13 -5
  68. package/fjs/djs/module.f.mjs +31 -16
  69. package/fjs/djs/parser/module.f.d.mts +13 -3
  70. package/fjs/djs/parser/module.f.mjs +117 -16
  71. package/fjs/djs/parser/proof.f.d.mts +5 -1
  72. package/fjs/djs/parser/proof.f.mjs +274 -12
  73. package/fjs/djs/parser/types.d.ts +7 -1
  74. package/fjs/djs/proof.f.d.mts +18 -2
  75. package/fjs/djs/proof.f.mjs +187 -12
  76. package/fjs/djs/serializer/module.f.d.mts +25 -8
  77. package/fjs/djs/serializer/module.f.mjs +61 -15
  78. package/fjs/djs/serializer/proof.f.d.mts +5 -0
  79. package/fjs/djs/serializer/proof.f.mjs +23 -1
  80. package/fjs/djs/tokenizer/module.f.d.mts +17 -5
  81. package/fjs/djs/tokenizer/module.f.mjs +99 -60
  82. package/fjs/djs/tokenizer/proof.f.d.mts +1 -1
  83. package/fjs/djs/tokenizer/proof.f.mjs +74 -67
  84. package/fjs/djs/transpiler/module.f.d.mts +12 -7
  85. package/fjs/djs/transpiler/module.f.mjs +82 -56
  86. package/fjs/djs/transpiler/types.d.ts +7 -3
  87. package/fjs/djs/types.d.ts +7 -1
  88. package/fjs/effects/list/module.f.d.mts +14 -11
  89. package/fjs/effects/list/module.f.mjs +13 -11
  90. package/fjs/effects/list/types.d.ts +27 -7
  91. package/fjs/effects/memory/module.f.d.mts +2 -1
  92. package/fjs/effects/memory/module.f.mjs +3 -4
  93. package/fjs/effects/memory/proof.f.mjs +13 -7
  94. package/fjs/effects/memory/types.d.ts +4 -3
  95. package/fjs/effects/mock/module.f.d.mts +20 -4
  96. package/fjs/effects/mock/module.f.mjs +37 -5
  97. package/fjs/effects/mock/types.d.ts +10 -1
  98. package/fjs/effects/module.d.mts +4 -2
  99. package/fjs/effects/module.f.d.mts +466 -280
  100. package/fjs/effects/module.f.mjs +537 -299
  101. package/fjs/effects/module.mjs +2 -1
  102. package/fjs/effects/node/memory/module.d.mts +4 -2
  103. package/fjs/effects/node/memory/module.mjs +6 -3
  104. package/fjs/effects/node/memory/proof.mjs +10 -4
  105. package/fjs/effects/node/module.d.mts +4 -3
  106. package/fjs/effects/node/module.f.d.mts +168 -33
  107. package/fjs/effects/node/module.f.mjs +256 -52
  108. package/fjs/effects/node/module.mjs +57 -34
  109. package/fjs/effects/node/proof.f.d.mts +28 -2
  110. package/fjs/effects/node/proof.f.mjs +161 -42
  111. package/fjs/effects/node/types.d.ts +106 -18
  112. package/fjs/effects/node/virtual/module.f.d.mts +18 -4
  113. package/fjs/effects/node/virtual/module.f.mjs +110 -68
  114. package/fjs/effects/node/virtual/proof.f.d.mts +28 -2
  115. package/fjs/effects/node/virtual/proof.f.mjs +190 -9
  116. package/fjs/effects/proof.f.d.mts +69 -37
  117. package/fjs/effects/proof.f.mjs +410 -130
  118. package/fjs/effects/types.d.ts +161 -33
  119. package/fjs/emergent_testing/module.f.d.mts +19 -12
  120. package/fjs/emergent_testing/module.f.mjs +93 -33
  121. package/fjs/emergent_testing/proof.f.d.mts +21 -7
  122. package/fjs/emergent_testing/proof.f.mjs +166 -32
  123. package/fjs/emergent_testing/types.d.ts +22 -4
  124. package/fjs/fsm/module.f.d.mts +14 -4
  125. package/fjs/fsm/module.f.mjs +54 -37
  126. package/fjs/fsm/proof.f.d.mts +2 -0
  127. package/fjs/fsm/proof.f.mjs +83 -114
  128. package/fjs/js/keywords/module.f.d.mts +52 -0
  129. package/fjs/js/keywords/module.f.mjs +72 -0
  130. package/fjs/js/keywords/proof.f.d.mts +3 -0
  131. package/fjs/js/keywords/proof.f.mjs +13 -0
  132. package/fjs/js/tokenizer/module.f.d.mts +26 -6
  133. package/fjs/js/tokenizer/module.f.mjs +145 -159
  134. package/fjs/js/tokenizer/proof.f.d.mts +1 -0
  135. package/fjs/js/tokenizer/proof.f.mjs +54 -24
  136. package/fjs/js/tokenizer/types.d.ts +33 -24
  137. package/fjs/mcp/cas/module.f.d.mts +1 -6
  138. package/fjs/mcp/cas/module.f.mjs +55 -51
  139. package/fjs/mcp/cas/proof.f.d.mts +15 -0
  140. package/fjs/mcp/cas/proof.f.mjs +174 -0
  141. package/fjs/mcp/evo/module.f.d.mts +19 -10
  142. package/fjs/mcp/evo/module.f.mjs +48 -27
  143. package/fjs/mcp/evo/proof.f.d.mts +6 -1
  144. package/fjs/mcp/evo/proof.f.mjs +115 -31
  145. package/fjs/mcp/module.f.d.mts +4 -4
  146. package/fjs/mcp/module.f.mjs +6 -6
  147. package/fjs/mcp/proof.f.d.mts +5 -3
  148. package/fjs/mcp/proof.f.mjs +112 -46
  149. package/fjs/media/html/module.f.mjs +1 -1
  150. package/fjs/media/json/extended/module.f.d.mts +82 -0
  151. package/fjs/media/json/extended/module.f.mjs +153 -0
  152. package/fjs/media/json/extended/proof.f.d.mts +42 -0
  153. package/fjs/media/json/extended/proof.f.mjs +127 -0
  154. package/fjs/media/json/extended/types.d.ts +23 -0
  155. package/fjs/media/json/module.f.d.mts +8 -2
  156. package/fjs/media/json/module.f.mjs +43 -41
  157. package/fjs/media/json/number/module.f.d.mts +59 -0
  158. package/fjs/media/json/number/module.f.mjs +136 -0
  159. package/fjs/media/json/number/proof.f.d.mts +24 -0
  160. package/fjs/media/json/number/proof.f.mjs +86 -0
  161. package/fjs/media/json/number/types.d.ts +28 -0
  162. package/fjs/media/json/parser/module.f.d.mts +25 -13
  163. package/fjs/media/json/parser/module.f.mjs +114 -70
  164. package/fjs/media/json/parser/proof.f.d.mts +5 -0
  165. package/fjs/media/json/parser/proof.f.mjs +31 -1
  166. package/fjs/media/json/parser/types.d.ts +33 -14
  167. package/fjs/media/json/rtti/module.f.d.mts +1 -1
  168. package/fjs/media/json/rtti/module.f.mjs +1 -1
  169. package/fjs/media/json/rtti/proof.f.mjs +9 -9
  170. package/fjs/media/json/schema/module.f.mjs +3 -13
  171. package/fjs/media/json/schema/proof.f.d.mts +0 -1
  172. package/fjs/media/json/schema/proof.f.mjs +1 -2
  173. package/fjs/media/json/serializer/module.f.d.mts +32 -1
  174. package/fjs/media/json/serializer/module.f.mjs +64 -2
  175. package/fjs/media/json/tokenizer/module.f.mjs +7 -3
  176. package/fjs/media/json/tokenizer/proof.f.d.mts +6 -0
  177. package/fjs/media/json/tokenizer/proof.f.mjs +62 -21
  178. package/fjs/media/json/types.d.ts +36 -10
  179. package/fjs/media/lock/module.f.d.mts +100 -0
  180. package/fjs/media/lock/module.f.mjs +125 -0
  181. package/fjs/media/lock/proof.f.d.mts +33 -0
  182. package/fjs/media/lock/proof.f.mjs +196 -0
  183. package/fjs/media/lock/types.d.ts +15 -0
  184. package/fjs/media/module.f.d.mts +6 -5
  185. package/fjs/media/module.f.mjs +8 -7
  186. package/fjs/media/note/module.f.d.mts +121 -0
  187. package/fjs/media/note/module.f.mjs +131 -0
  188. package/fjs/media/note/proof.f.d.mts +29 -0
  189. package/fjs/media/note/proof.f.mjs +150 -0
  190. package/fjs/media/note/types.d.ts +10 -0
  191. package/fjs/media/proof.f.d.mts +4 -1
  192. package/fjs/media/proof.f.mjs +40 -21
  193. package/fjs/media/revision/module.f.d.mts +78 -7
  194. package/fjs/media/revision/module.f.mjs +119 -12
  195. package/fjs/media/revision/proof.f.d.mts +10 -0
  196. package/fjs/media/revision/proof.f.mjs +88 -0
  197. package/fjs/media/revision/types.d.ts +34 -5
  198. package/fjs/media/type/module.f.d.mts +33 -15
  199. package/fjs/media/type/module.f.mjs +35 -29
  200. package/fjs/media/type/proof.f.d.mts +2 -1
  201. package/fjs/media/type/proof.f.mjs +30 -10
  202. package/fjs/module.f.mjs +29 -8
  203. package/fjs/nanvm/proof.f.mjs +3 -3
  204. package/fjs/nanvm/rust/module.f.mjs +1 -1
  205. package/fjs/nanvm/update/module.f.d.mts +4 -4
  206. package/fjs/nanvm/update/module.f.mjs +8 -9
  207. package/fjs/nanvm/update/proof.f.mjs +4 -3
  208. package/fjs/proof.f.d.mts +3 -3
  209. package/fjs/proof.f.mjs +36 -10
  210. package/fjs/protocol/json_rpc/module.f.d.mts +2 -2
  211. package/fjs/protocol/json_rpc/module.f.mjs +3 -3
  212. package/fjs/protocol/json_rpc/proof.f.mjs +4 -4
  213. package/fjs/protocol/mcp/module.f.d.mts +10 -13
  214. package/fjs/protocol/mcp/module.f.mjs +74 -58
  215. package/fjs/protocol/mcp/proof.f.d.mts +13 -4
  216. package/fjs/protocol/mcp/proof.f.mjs +193 -61
  217. package/fjs/protocol/mcp/stdio/module.f.d.mts +15 -7
  218. package/fjs/protocol/mcp/stdio/module.f.mjs +38 -25
  219. package/fjs/protocol/mcp/stdio/proof.f.d.mts +4 -2
  220. package/fjs/protocol/mcp/stdio/proof.f.mjs +45 -12
  221. package/fjs/protocol/mcp/stdio/types.d.ts +1 -1
  222. package/fjs/protocol/mcp/types.d.ts +17 -6
  223. package/fjs/sul/id/module.f.d.mts +0 -1
  224. package/fjs/sul/id/module.f.mjs +2 -3
  225. package/fjs/sul/level/hash/module.f.mjs +2 -1
  226. package/fjs/sul/level/hash/proof.f.mjs +2 -2
  227. package/fjs/sul/module.f.mjs +18 -13
  228. package/fjs/text/code_point/module.f.d.mts +8 -0
  229. package/fjs/text/code_point/module.f.mjs +8 -1
  230. package/fjs/text/code_point/proof.f.d.mts +1 -0
  231. package/fjs/text/code_point/proof.f.mjs +11 -0
  232. package/fjs/text/sgr/module.f.d.mts +4 -6
  233. package/fjs/text/sgr/module.f.mjs +6 -7
  234. package/fjs/text/utf16/module.f.mjs +7 -2
  235. package/fjs/text/utf16/proof.f.mjs +3 -3
  236. package/fjs/text/utf8/module.f.mjs +4 -2
  237. package/fjs/types/array/module.f.mjs +14 -2
  238. package/fjs/types/bit_vec/module.f.d.mts +0 -2
  239. package/fjs/types/bit_vec/module.f.mjs +46 -32
  240. package/fjs/types/bit_vec/proof.f.mjs +2 -2
  241. package/fjs/types/btree/remove/module.f.mjs +1 -1
  242. package/fjs/types/btree/set/module.f.mjs +9 -11
  243. package/fjs/types/btree/set/proof.f.mjs +12 -0
  244. package/fjs/types/byte_set/module.f.d.mts +11 -4
  245. package/fjs/types/byte_set/module.f.mjs +14 -6
  246. package/fjs/types/byte_set/proof.f.mjs +7 -7
  247. package/fjs/types/function/compare/module.f.mjs +10 -3
  248. package/fjs/types/list/module.f.d.mts +1 -1
  249. package/fjs/types/list/module.f.mjs +1 -1
  250. package/fjs/types/nullable/module.f.d.mts +18 -4
  251. package/fjs/types/nullable/module.f.mjs +21 -4
  252. package/fjs/types/nullable/proof.f.d.mts +4 -0
  253. package/fjs/types/nullable/proof.f.mjs +15 -0
  254. package/fjs/types/object/module.f.d.mts +12 -2
  255. package/fjs/types/object/module.f.mjs +11 -1
  256. package/fjs/types/patricia_trie/module.f.mjs +26 -11
  257. package/fjs/types/result/module.f.d.mts +3 -3
  258. package/fjs/types/result/module.f.mjs +3 -3
  259. package/fjs/types/rtti/common/module.f.d.mts +25 -28
  260. package/fjs/types/rtti/common/module.f.mjs +36 -32
  261. package/fjs/types/rtti/common/proof.f.mjs +5 -5
  262. package/fjs/types/rtti/data/module.f.d.mts +14 -0
  263. package/fjs/types/rtti/data/module.f.mjs +25 -2
  264. package/fjs/types/rtti/data/proof.f.d.mts +1 -0
  265. package/fjs/types/rtti/data/proof.f.mjs +25 -2
  266. package/fjs/types/rtti/parse/module.f.d.mts +24 -14
  267. package/fjs/types/rtti/parse/module.f.mjs +37 -28
  268. package/fjs/types/rtti/parse/proof.f.d.mts +3 -2
  269. package/fjs/types/rtti/parse/proof.f.mjs +33 -14
  270. package/fjs/types/rtti/proof.f.mjs +3 -1
  271. package/fjs/types/rtti/ts/module.f.mjs +13 -17
  272. package/fjs/types/rtti/ts/types.d.ts +14 -1
  273. package/fjs/types/rtti/validate/module.f.d.mts +81 -19
  274. package/fjs/types/rtti/validate/module.f.mjs +107 -61
  275. package/fjs/types/rtti/validate/proof.f.d.mts +13 -10
  276. package/fjs/types/rtti/validate/proof.f.mjs +186 -197
  277. package/fjs/types/sorted_set/module.f.d.mts +18 -0
  278. package/fjs/types/sorted_set/module.f.mjs +22 -0
  279. package/fjs/types/sorted_set/proof.f.d.mts +1 -0
  280. package/fjs/types/sorted_set/proof.f.mjs +16 -1
  281. package/fjs/types/uint8array/module.f.d.mts +1 -1
  282. package/fjs/types/uint8array/module.f.mjs +1 -1
  283. package/fjs/website/module.f.d.mts +3 -3
  284. package/fjs/website/module.f.mjs +4 -7
  285. package/fjs/website/proof.f.mjs +2 -1
  286. package/package.json +2 -2
  287. package/fjs/dev/package_json/module.f.d.mts +0 -39
  288. package/fjs/dev/package_json/module.f.mjs +0 -40
  289. package/fjs/dev/package_json/proof.f.d.mts +0 -6
  290. package/fjs/dev/package_json/proof.f.mjs +0 -32
  291. package/fjs/effects/eff/module.f.d.mts +0 -20
  292. package/fjs/effects/eff/module.f.mjs +0 -70
  293. package/fjs/effects/eff/proof.f.d.mts +0 -15
  294. package/fjs/effects/eff/proof.f.mjs +0 -69
  295. package/fjs/effects/eff/types.d.ts +0 -71
  296. package/fjs/types/rtti/validate/types.d.ts +0 -6
@@ -1,11 +1,37 @@
1
1
  /**
2
2
  * @import { Vec } from "../../types/bit_vec/types.ts"
3
- * @import { IoResult, ReadFile } from "./types.ts"
4
- * @import { Dir } from "./virtual/types.ts"
3
+ * @import { IoChannel, IoError, IoResult, ReadFile } from "./types.ts"
4
+ * @import { Result } from "../../types/result/types.ts"
5
5
  * @import { List } from "../list/types.ts"
6
6
  * @import { OperationMap } from "../types.ts"
7
7
  */
8
8
  export declare const proof: {
9
+ toIoError: {
10
+ error: () => void;
11
+ withCode: () => void;
12
+ string: () => void;
13
+ null: () => void;
14
+ nonStringCode: () => void;
15
+ noCode: () => void;
16
+ };
17
+ isNotFound: {
18
+ enoent: () => void;
19
+ otherCode: () => void;
20
+ notImplemented: () => void;
21
+ };
22
+ errorMessage: {
23
+ io: () => void;
24
+ notImplemented: () => void;
25
+ };
26
+ errorSummary: {
27
+ io: () => void;
28
+ ioWithoutCode: () => void;
29
+ notImplemented: () => void;
30
+ };
31
+ exitStep: {
32
+ ok: () => void;
33
+ error: () => void;
34
+ };
9
35
  externalTestContext: () => void;
10
36
  map: () => void;
11
37
  fetch: () => void;
@@ -1,15 +1,16 @@
1
1
  /**
2
2
  * @import { Vec } from "../../types/bit_vec/types.ts"
3
- * @import { IoResult, ReadFile } from "./types.ts"
4
- * @import { Dir } from "./virtual/types.ts"
3
+ * @import { IoChannel, IoError, IoResult, ReadFile } from "./types.ts"
4
+ * @import { Result } from "../../types/result/types.ts"
5
5
  * @import { List } from "../list/types.ts"
6
6
  * @import { OperationMap } from "../types.ts"
7
7
  */
8
8
 
9
9
  import { empty, isVec, uint, vec, vec8 } from "../../types/bit_vec/module.f.mjs"
10
10
  import { utf8, utf8ToString } from "../../text/module.f.mjs"
11
- import { match, pure, step } from "../module.f.mjs"
12
- import { both, fetch, mkdir, now, readdir, readFile, readUtf8File, rm, sandbox, writeFile, writeUtf8File, rename, readBytes, randomInt, writeFromStream, usesInlineTestContext, versionLessThan } from "./module.f.mjs"
11
+ import { match } from "../module.f.mjs"
12
+ import { mapStep, step as ioStep } from "../module.f.mjs"
13
+ import { both, errorMessage, errorSummary, exitStep, fetch, ioError, isNotFound, mkdir, now, readdir, readFile, readUtf8File, rm, sandbox, toIoError, writeFile, writeUtf8File, rename, readBytes, randomInt, writeFromStream, usesInlineTestContext, versionLessThan } from "./module.f.mjs"
13
14
  import { create as memCreate, read as memRead, write as memWrite } from "../memory/module.f.mjs"
14
15
  import { empty as listEmpty, nonEmpty as listNonEmpty } from "../list/module.f.mjs"
15
16
  import { emptyState, virtual } from "./virtual/module.f.mjs"
@@ -29,7 +30,115 @@ const readHelloMap = {
29
30
 
30
31
  const readHello = match(readHelloMap)
31
32
 
33
+ /**
34
+ * Asserts that a channel error is a host failure carrying `message`. Every
35
+ * runner reports through the same normalized {@link IoError}, so a proof
36
+ * against the virtual filesystem names the message rather than the shape.
37
+ * @type {(e: IoChannel, message: string) => void}
38
+ */
39
+ const assertIoMessage = (e, message) => {
40
+ assert(e[0] === 'ioError', e)
41
+ assertEq(e[1].message, message)
42
+ }
43
+
44
+ /** Asserts that an operation succeeded with `expected`.
45
+ * @type {<T, E>(r: Result<T, E>, expected: T) => void}
46
+ */
47
+ const assertOk = (r, expected) => {
48
+ assert(r[0] === 'ok', r)
49
+ assertEq(r[1], expected)
50
+ }
51
+
32
52
  export const proof = {
53
+ // The one boundary where a runner's `catch` becomes effect data: whatever
54
+ // was thrown is reduced to a code (when the host attached a string one)
55
+ // and a message.
56
+ toIoError: {
57
+ error: () => {
58
+ assertIoMessage(toIoError(new Error('boom')), 'boom')
59
+ },
60
+ withCode: () => {
61
+ const e = toIoError(Object.assign(new Error('missing'), { code: 'ENOENT' }))
62
+ assert(e[0] === 'ioError', e)
63
+ assertEq(e[1].code, 'ENOENT', e)
64
+ assertEq(e[1].message, 'missing', e)
65
+ },
66
+ // A thrown non-`Error` still normalizes: the value's string form is the
67
+ // message, and there is no code to carry.
68
+ string: () => {
69
+ const e = toIoError('plain')
70
+ assert(e[0] === 'ioError', e)
71
+ assertEq(e[1].code, undefined, e)
72
+ assertEq(e[1].message, 'plain', e)
73
+ },
74
+ null: () => {
75
+ assertIoMessage(toIoError(null), 'null')
76
+ },
77
+ // An object whose `code` is not a string is not an OS error code, so it
78
+ // is dropped rather than carried as one.
79
+ nonStringCode: () => {
80
+ const e = toIoError({ code: 42 })
81
+ assert(e[0] === 'ioError', e)
82
+ assertEq(e[1].code, undefined, e)
83
+ },
84
+ noCode: () => {
85
+ const e = toIoError({})
86
+ assert(e[0] === 'ioError', e)
87
+ assertEq(e[1].code, undefined, e)
88
+ },
89
+ },
90
+ isNotFound: {
91
+ enoent: () => {
92
+ assert(isNotFound(ioError({ code: 'ENOENT', message: 'no such file or directory' })))
93
+ },
94
+ otherCode: () => {
95
+ assert(!isNotFound(ioError({ code: 'EACCES', message: 'permission denied' })))
96
+ },
97
+ // A runner that cannot perform the operation has not looked for the
98
+ // path at all, so a missing handler is never "not found".
99
+ notImplemented: () => {
100
+ assert(!isNotFound(['notImplemented', 'readFile']))
101
+ },
102
+ },
103
+ errorMessage: {
104
+ io: () => {
105
+ assertEq(errorMessage(ioError({ message: 'disk full' })), 'disk full')
106
+ },
107
+ notImplemented: () => {
108
+ assertEq(errorMessage(['notImplemented', 'readFile']), 'operation not implemented: readFile')
109
+ },
110
+ },
111
+ errorSummary: {
112
+ // The distinction that matters: `errorMessage` hands back the host's
113
+ // words, which is where the path lives; `errorSummary` never does.
114
+ io: () => {
115
+ assertEq(errorSummary(ioError({ code: 'ENOENT', message: "no such file or directory, scandir '/home/u/.cas'" })), 'io error: ENOENT')
116
+ },
117
+ ioWithoutCode: () => {
118
+ assertEq(errorSummary(ioError({ message: "cannot read '/home/u/.cas'" })), 'io error')
119
+ },
120
+ notImplemented: () => {
121
+ assertEq(errorSummary(['notImplemented', 'readdir']), 'operation not implemented: readdir')
122
+ },
123
+ },
124
+ exitStep: {
125
+ // The exit-code policy a `NodeProgram` ends with: success is `0`...
126
+ ok: () => {
127
+ const [state, code] = virtual(emptyState)(exitStep(writeFile('hello', vec8(0x2An))))
128
+ assertEq(code[0], 'ok')
129
+ assertEq(code[1], 0)
130
+ assertEq(state.stderr, '')
131
+ },
132
+ // ...and a failure is reported on `stderr` and exits `1`.
133
+ // ...and the code is `[1]` either way, which is what lets a runner
134
+ // read it without asking which branch it came from.
135
+ error: () => {
136
+ const [state, code] = virtual(emptyState)(exitStep(readFile('missing')))
137
+ assertEq(code[0], 'error')
138
+ assertEq(code[1], 1)
139
+ assertEq(state.stderr, 'no such file or directory\n')
140
+ },
141
+ },
33
142
  externalTestContext: () => {
34
143
  assert(usesInlineTestContext('node', 'v22.20.0'))
35
144
  assert(usesInlineTestContext('node', '25.99.99'))
@@ -44,18 +153,15 @@ export const proof = {
44
153
  assert(!versionLessThan('26.1.1', '26.1.1'))
45
154
  },
46
155
  map: () => {
47
- const e = step(
48
- readFile('hello'),
49
- ([k, v]) => {
50
- assert(k !== 'error', v)
51
- return pure(uint(v) * 2n)
52
- })
156
+ const e = mapStep(readFile('hello'), v => uint(v) * 2n)
53
157
  //
54
158
  let r = readHello(e)
55
159
  while (r[0] === 'cont') {
56
160
  r = readHello(r[2](r[1]))
57
161
  }
58
- assertEq(r[1], 0x2An)
162
+ // `done` carries the whole `Result`: an interpreter never separates the
163
+ // channels, so the projection's value is inside the `ok`.
164
+ assertEq(r[1][1], 0x2An)
59
165
  },
60
166
  fetch: () => {
61
167
  const [_, [t, result]] = virtual({
@@ -81,8 +187,11 @@ export const proof = {
81
187
  )
82
188
  assert(t === 'ok', result)
83
189
  const tmp = state.root.tmp
84
- assert(!(typeof tmp !== 'object' || Array.isArray(tmp)), state.root)
85
- const cache = (/** @type {Dir} */ (tmp)).cache
190
+ // `instanceof Array`, not `Array.isArray`: only the former's negative
191
+ // branch removes a `readonly` array from a union, so only it narrows
192
+ // `_Entity` to `Dir`.
193
+ assert(!(typeof tmp !== 'object' || tmp instanceof Array), state.root)
194
+ const cache = tmp.cache
86
195
  assert(!(typeof cache !== 'object' || Array.isArray(cache)), tmp)
87
196
  },
88
197
  nonRec: () => {
@@ -122,7 +231,8 @@ export const proof = {
122
231
  nestedPath: () => {
123
232
  const [_, [t, result]] = virtual(emptyState)(readFile('tmp/cache'))
124
233
  assert(t === 'error', result)
125
- if ((/** @type {{ code?: unknown }} */ (result)).code !== 'ENOENT') { throw result }
234
+ assert(result[0] === 'ioError', result)
235
+ assertEq(result[1].code, 'ENOENT', result)
126
236
  },
127
237
  withinLimit: () => {
128
238
  // Test with a small file well within the 131,072 byte limit
@@ -197,7 +307,7 @@ export const proof = {
197
307
  noSuchDir: () => {
198
308
  const [_, [t, result]] = virtual(emptyState)(readdir('tmp', { recursive: true }))
199
309
  assert(t === 'error', result)
200
- assertEq(result, 'invalid path')
310
+ assertIoMessage(result, 'invalid path')
201
311
  },
202
312
  },
203
313
  writeFile: {
@@ -229,7 +339,7 @@ export const proof = {
229
339
  writeFile('tmp/cache', vec8(0x2An))
230
340
  )
231
341
  assert(t === 'error', result)
232
- assertEq(result, 'invalid file')
342
+ assertIoMessage(result, 'invalid file')
233
343
  assertEq(state.root.tmp, undefined, state.root)
234
344
  },
235
345
  directory: () => {
@@ -242,7 +352,7 @@ export const proof = {
242
352
  writeFile('tmp', vec8(0x2An))
243
353
  )
244
354
  assert(t === 'error', result)
245
- assertEq(result, 'invalid file')
355
+ assertIoMessage(result, 'invalid file')
246
356
  const tmp = state.root.tmp
247
357
  assert(!(tmp === undefined || Array.isArray(tmp)), tmp)
248
358
  },
@@ -272,13 +382,13 @@ export const proof = {
272
382
  })(rm('tmp/cache'))
273
383
  assert(t === 'ok', result)
274
384
  const tmp = state.root.tmp
275
- assert(!(typeof tmp !== 'object' || Array.isArray(tmp)), state.root)
276
- assertEq((/** @type {Dir} */ (tmp)).cache, undefined, tmp)
385
+ assert(!(typeof tmp !== 'object' || tmp instanceof Array), state.root)
386
+ assertEq(tmp.cache, undefined, tmp)
277
387
  },
278
388
  noSuchFile: () => {
279
389
  const [_, [t, result]] = virtual(emptyState)(rm('hello'))
280
390
  assert(t === 'error', result)
281
- assertEq(result, 'no such file')
391
+ assertIoMessage(result, 'no such file')
282
392
  },
283
393
  isDirectory: () => {
284
394
  const [state, [t, result]] = virtual({
@@ -286,18 +396,20 @@ export const proof = {
286
396
  root: { tmp: {} },
287
397
  })(rm('tmp'))
288
398
  assert(t === 'error', result)
289
- assertEq(result, 'invalid path')
399
+ assertIoMessage(result, 'invalid path')
290
400
  assert(state.root.tmp !== undefined, state.root)
291
401
  },
292
402
  },
293
403
  both: () => {
294
- const [_, results] = virtual({
404
+ const [_, both2] = virtual({
295
405
  ...emptyState,
296
406
  root: {
297
407
  a: [vec8(0x2An)],
298
408
  b: [vec8(0x15n)],
299
409
  },
300
410
  })(both(readFile('a'))(readFile('b')))
411
+ assert(both2[0] === 'ok', both2)
412
+ const results = both2[1]
301
413
  assert(results[0][0] === 'ok', results[0])
302
414
  assert(results[1][0] === 'ok', results[1])
303
415
  assertEq(uint(results[0][1]), 0x2An, results[0][1])
@@ -305,41 +417,48 @@ export const proof = {
305
417
  },
306
418
  now: () => {
307
419
  const [_, result] = virtual({ ...emptyState, epochNs: 1_000_000 })(now())
308
- assertEq(result, 1_000_000)
420
+ assertOk(result, 1_000_000)
309
421
  },
310
422
  sandbox: {
311
423
  // Virtual `sandbox` is now a pass-through: the function is expected
312
424
  // to return a `SandboxResult` directly. Fixtures dictate the result
313
425
  // (and `duration`) instead of the runner measuring.
314
426
  ok: () => {
315
- const [_, { result, duration }] = virtual(emptyState)(
316
- sandbox(() => /** @type {never} */ ({ result: ['ok', 42], duration: 0 })))
427
+ const [_, sandboxed] = virtual(emptyState)(
428
+ sandbox(() => ({ result: ['ok', 42], duration: 0 })))
429
+ // Two `Result`s, one inside the other on purpose: the outer one is
430
+ // the operation's own status, the inner one is the sandboxed
431
+ // function's outcome — returned data, not effect status.
432
+ assert(sandboxed[0] === 'ok', sandboxed)
433
+ const { result, duration } = sandboxed[1]
317
434
  assert(result[0] === 'ok', result)
318
435
  assertEq(result[1], 42)
319
436
  assertEq(duration, 0)
320
437
  },
321
438
  error: () => {
322
439
  const err = new Error('fail')
323
- const [_, { result }] = virtual(emptyState)(
324
- sandbox(() => /** @type {never} */ ({ result: ['error', err], duration: 0 })))
440
+ const [_, sandboxed] = virtual(emptyState)(
441
+ sandbox(() => ({ result: ['error', err], duration: 0 })))
442
+ assert(sandboxed[0] === 'ok', sandboxed)
443
+ const { result } = sandboxed[1]
325
444
  assert(result[0] === 'error', result)
326
445
  assertEq(result[1], err)
327
446
  },
328
447
  },
329
448
  memory: {
330
449
  createAndRead: () => {
331
- const effect = step(memCreate(42), key => memRead(key))
450
+ const effect = ioStep(memCreate(42), key => memRead(key))
332
451
  const [_, value] = virtual(emptyState)(effect)
333
- assertEq(value, 42)
452
+ assertOk(value, 42)
334
453
  },
335
454
  createAndWrite: () => {
336
- const effect = step(
455
+ const effect = ioStep(
337
456
  memCreate(1),
338
- key => step(
457
+ key => ioStep(
339
458
  memWrite(key, 99),
340
459
  () => memRead(key)))
341
460
  const [_, value] = virtual(emptyState)(effect)
342
- assertEq(value, 99)
461
+ assertOk(value, 99)
343
462
  },
344
463
  },
345
464
  rename: {
@@ -351,7 +470,7 @@ export const proof = {
351
470
  assert(t === 'ok', result)
352
471
  assertEq(state.root.src, undefined, state.root)
353
472
  assert(Array.isArray(state.root.dst), state.root)
354
- assertEq(uint((/** @type {readonly Vec[]} */ (state.root.dst))[0]), 0x2An, state.root)
473
+ assertEq(uint(state.root.dst[0]), 0x2An, state.root)
355
474
  },
356
475
  nestedRename: () => {
357
476
  const [state, [t, result]] = virtual({
@@ -360,8 +479,8 @@ export const proof = {
360
479
  })(rename('tmp/src', 'tmp/dst'))
361
480
  assert(t === 'ok', result)
362
481
  const tmp = state.root.tmp
363
- assert(!(typeof tmp !== 'object' || Array.isArray(tmp)), state.root)
364
- assertEq((/** @type {Dir} */ (tmp)).src, undefined, tmp)
482
+ assert(!(typeof tmp !== 'object' || tmp instanceof Array), state.root)
483
+ assertEq(tmp.src, undefined, tmp)
365
484
  },
366
485
  dirOverFile: () => {
367
486
  const [state, [t, result]] = virtual({
@@ -408,18 +527,18 @@ export const proof = {
408
527
  randomInt: {
409
528
  increments: () => {
410
529
  const [state1, r1] = virtual(emptyState)(randomInt())
411
- assertEq(r1, 0)
530
+ assertOk(r1, 0)
412
531
  const [state2, r2] = virtual(state1)(randomInt())
413
- assertEq(r2, 1)
532
+ assertOk(r2, 1)
414
533
  const [_, r3] = virtual(state2)(randomInt())
415
- assertEq(r3, 2)
534
+ assertOk(r3, 2)
416
535
  },
417
536
  },
418
537
  writeFromStream: {
419
538
  createExclusiveFails: () => {
420
539
  // The destination already exists, so `createExclusive` fails (EEXIST) and
421
540
  // the error propagates without ever touching `writeBytes`.
422
- /** @type {List<never, IoResult<Vec>>} */
541
+ /** @type {List<never, Vec, IoChannel>} */
423
542
  const chunks = listEmpty()
424
543
  const [state, [t, result]] = virtual({
425
544
  ...emptyState,
@@ -432,13 +551,13 @@ export const proof = {
432
551
  invalidBufferSize: () => {
433
552
  // A chunk whose bit length isn't a multiple of 8 trips the
434
553
  // byte-alignment guard before `writeBytes` is ever called.
435
- /** @type {List<never, IoResult<Vec>>} */
436
- const chunks = listNonEmpty(['ok', vec(4n)(0b1010n)], listEmpty())
554
+ /** @type {List<never, Vec, IoChannel>} */
555
+ const chunks = listNonEmpty(vec(4n)(0b1010n), listEmpty())
437
556
  const [_, [t, result]] = virtual(emptyState)(
438
557
  writeFromStream('hello', chunks)
439
558
  )
440
559
  assert(t === 'error', result)
441
- assertEq(result, 'invalid buffer size')
560
+ assertIoMessage(result, 'invalid buffer size')
442
561
  },
443
562
  },
444
563
  }
@@ -9,10 +9,70 @@ import type { MemOp } from '../memory/types.ts';
9
9
  import type { Nominal } from '../../types/nominal/types.ts';
10
10
  import type { Result } from '../../types/result/types.ts';
11
11
  import type { StringMap } from '../../types/object/types.ts';
12
- import type { Effect, Operation, ToAsyncOperationMap } from '../types.ts';
12
+ import type { Effect, NotImplemented, Operation, ToAsyncOperationMap } from '../types.ts';
13
13
  import type { List } from '../list/types.ts';
14
- export type IoResult<T> = Result<T, unknown>;
15
- export type All = ['all', <T>(...effects: Effect<never, T>[]) => readonly T[]];
14
+ /**
15
+ * A host failure, normalized: whatever the runtime threw reduced to a
16
+ * serializable record. `code` is the OS error code when the host supplied one
17
+ * (`'ENOENT'`, `'EEXIST'`), absent otherwise.
18
+ *
19
+ * It is a tagged tuple for the same reason {@link NotImplemented} is — the two
20
+ * share an error channel, and the tag is what tells them apart. That
21
+ * distinction is the whole reason this type exists: with a bare `unknown`
22
+ * error, `NotImplemented | unknown` collapses to `unknown` and a program can no
23
+ * longer tell "this runner cannot do it" from "the host tried and failed".
24
+ *
25
+ * Normalizing also keeps the channel serializable. A thrown `Error` carries a
26
+ * stack, a `cause`, and arbitrary own properties; none of it survives a wire
27
+ * hop, and a runner in another process could not reproduce it.
28
+ */
29
+ export type IoError = readonly ['ioError', IoErrorInfo];
30
+ export type IoErrorInfo = {
31
+ readonly code?: string;
32
+ readonly message: string;
33
+ };
34
+ /**
35
+ * The result of an operation with no failures of its own: it either produces
36
+ * its value or reports that the runner does not implement it.
37
+ *
38
+ * Every operation's return type is a `Result`, including the ones that cannot
39
+ * fail on their own terms — an operation left on a raw contract would be a hole
40
+ * in the error channel, and a runner may omit a handler for any of them.
41
+ */
42
+ export type OpResult<T> = Result<T, NotImplemented>;
43
+ /**
44
+ * The error channel of anything that performs host IO: a normalized host
45
+ * failure, or the report that the runner does not implement the operation.
46
+ *
47
+ * It is one name rather than a union spelled at each site, and that is a
48
+ * migration property rather than brevity. An effect that does no IO *yet* is
49
+ * one added `readFile` away from doing some, and if each signature names its
50
+ * own errors, that one change walks up every enclosing signature — the failure
51
+ * mode that sank `throws` clauses elsewhere, where engineers eventually
52
+ * declared everything throwing rather than maintain the cascade. Declaring the
53
+ * standard channel once is that concession made deliberately: an IO-touching
54
+ * effect says it fails *the way node IO fails*, and gaining a new way to do so
55
+ * changes nothing above it.
56
+ *
57
+ * It is not a licence to widen. An operation with failures of its own extends
58
+ * the channel (`IoChannel | ParseError`), and a computation whose errors are
59
+ * genuinely narrower should say so — this is the default for IO, not a ceiling.
60
+ */
61
+ export type IoChannel = NotImplemented | IoError;
62
+ /**
63
+ * The result of an operation that performs host IO: its value, a normalized
64
+ * host failure, or the missing-handler report.
65
+ */
66
+ export type IoResult<T> = Result<T, IoChannel>;
67
+ /**
68
+ * Runs its effects concurrently and answers each one's whole `Result`.
69
+ *
70
+ * The nesting is deliberate and belongs to the runner: this envelope says
71
+ * whether `all` itself could be dispatched, and each inner `Result` is what
72
+ * that effect answered. `allOk` (`./module.f.mjs`) is the collapse a fallible
73
+ * chain wants.
74
+ */
75
+ export type All = ['all', <T, E>(...effects: Effect<never, T, E>[]) => OpResult<readonly Result<T, E>[]>];
16
76
  export type Fetch = ['fetch', (url: string) => IoResult<Vec>];
17
77
  export type MakeDirectoryOptions = {
18
78
  readonly recursive: true;
@@ -42,7 +102,7 @@ export type WriteFile = readonly ['writeFile', (path: string, data: Vec) => IoRe
42
102
  export type Rm = readonly ['rm', (path: string) => IoResult<void>];
43
103
  export type Rename = readonly ['rename', (src: string, dst: string) => IoResult<void>];
44
104
  export type ReadBytes = readonly ['readBytes', (path: string, offset: number, size: number) => IoResult<Vec>];
45
- export type RandomInt = readonly ['randomInt', () => number];
105
+ export type RandomInt = readonly ['randomInt', () => OpResult<number>];
46
106
  export type ExecResult = {
47
107
  readonly stdout: string;
48
108
  readonly stderr: string;
@@ -66,7 +126,7 @@ export type CreateExclusive = readonly ['createExclusive', (path: string) => IoR
66
126
  */
67
127
  export type WriteBytes = readonly ['writeBytes', (path: string, offset: number, data: Vec) => IoResult<void>];
68
128
  /** @internal */
69
- export type _WriteLoop = <O extends Operation>(offset: number, e: List<O, IoResult<Vec>>) => Effect<O | WriteBytes, IoResult<void>>;
129
+ export type _WriteLoop = <O extends Operation>(offset: number, e: List<O, Vec, IoChannel>) => Effect<O | WriteBytes, void, IoChannel>;
70
130
  /** File metadata returned by `stat`. Only `size` (in bytes) for now. */
71
131
  export type FileStat = {
72
132
  readonly size: number;
@@ -86,11 +146,16 @@ export type ServerResponse = {
86
146
  readonly headers: Headers;
87
147
  readonly body: Vec;
88
148
  };
89
- export type RequestListener<O extends Operation> = (_: IncomingMessage) => Effect<O, ServerResponse>;
90
- export type CreateServer = ['createServer', (listener: RequestListener<Operation>) => Server];
91
- export type Listen = ['listen', (server: Server, port: number) => void];
149
+ /**
150
+ * An HTTP request handler. The channel is `never` because the response frame
151
+ * *is* where a failure goes a listener that cannot answer still has a status
152
+ * code to answer with, so absorbing is the contract rather than an omission.
153
+ */
154
+ export type RequestListener<O extends Operation> = (_: IncomingMessage) => Effect<O, ServerResponse, never>;
155
+ export type CreateServer = ['createServer', (listener: RequestListener<Operation>) => OpResult<Server>];
156
+ export type Listen = ['listen', (server: Server, port: number) => OpResult<void>];
92
157
  export type Http = CreateServer | Listen;
93
- export type Forever = ['forever', () => never];
158
+ export type Forever = ['forever', () => OpResult<never>];
94
159
  export type Module = StringMap<unknown>;
95
160
  export type Import = ['import', (path: string) => IoResult<Module>];
96
161
  /** Named output streams accepted by the `Write` effect. */
@@ -100,7 +165,7 @@ export type WriteConsoles = 'stdout' | 'stderr';
100
165
  * a `Vec`. The Node runner maps each stream name to the appropriate fd and
101
166
  * delegates to the OS via `stream.write()` with backpressure handling.
102
167
  */
103
- export type Write = readonly ['write', (stream: WriteConsoles, data: Vec) => void];
168
+ export type Write = readonly ['write', (stream: WriteConsoles, data: Vec) => OpResult<void>];
104
169
  export type Console = (s: string) => Effect<Write, void>;
105
170
  /** Named input streams accepted by the `Read` effect. */
106
171
  export type ReadConsoles = 'stdin';
@@ -112,10 +177,10 @@ export type ReadConsoles = 'stdin';
112
177
  * rather than the interpreter. Back-pressure is naturally sequential — the next
113
178
  * `read` is only issued once the previous byte is consumed.
114
179
  */
115
- export type Read = readonly ['read', (stream: ReadConsoles) => number | null];
180
+ export type Read = readonly ['read', (stream: ReadConsoles) => OpResult<number | null>];
116
181
  /** @internal */
117
182
  export type _UtfList = EffectList<number>;
118
- export type Now = readonly ['now', () => number];
183
+ export type Now = readonly ['now', () => OpResult<number>];
119
184
  /**
120
185
  * The outcome of a `Sandbox` operation.
121
186
  *
@@ -132,7 +197,7 @@ export type SandboxResult<T> = {
132
197
  */
133
198
  readonly duration: number;
134
199
  };
135
- export type Sandbox = readonly ['sandbox', <T>(f: () => T) => SandboxResult<T>];
200
+ export type Sandbox = readonly ['sandbox', <T>(f: () => T) => OpResult<SandboxResult<T>>];
136
201
  /**
137
202
  * Resolves the return value of a test function inside the effect runner.
138
203
  * If `p` is a real `Promise`, it is awaited and rejections propagate as
@@ -140,7 +205,7 @@ export type Sandbox = readonly ['sandbox', <T>(f: () => T) => SandboxResult<T>];
140
205
  * (objects with a `.then` method that are not `instanceof Promise`) are
141
206
  * treated as ordinary values — not awaited. See `fjs/dev/tf/README.md`.
142
207
  */
143
- export type Await = readonly ['await', (p: unknown) => readonly [unknown]];
208
+ export type Await = readonly ['await', (p: unknown) => OpResult<readonly [unknown]>];
144
209
  /**
145
210
  * Signature of a framework test-registration function (e.g. `nodeTest.test`,
146
211
  * `bunTest.test`). Returns `Promise<void>` so async sub-tests can be awaited.
@@ -157,10 +222,18 @@ export type TestFn = (name: string, options: {
157
222
  export type TestContext = {
158
223
  readonly test: TestFn;
159
224
  };
160
- /** Effect operation that registers a named test with the active `TestContext`. */
161
- export type Test = readonly ['test', (ctx: TestContext, name: string, expectFailure: boolean, test: (t: TestContext) => Effect<Test | All | Await, void>) => void];
225
+ /**
226
+ * Operation that registers a named test with the active `TestContext`.
227
+ *
228
+ * The callback's `never` is the honest reading of what an external framework
229
+ * accepts. Node `--test`, Bun and Deno take a body that either returns or
230
+ * throws; there is no channel to answer a failure through, so the body absorbs
231
+ * its own — which `emergent_testing` does, by panicking, since a throw is the
232
+ * one failure signal those frameworks understand.
233
+ */
234
+ export type Test = readonly ['test', (ctx: TestContext, name: string, expectFailure: boolean, test: (t: TestContext) => Effect<Test | All | Await, void, never>) => OpResult<void>];
162
235
  export type NodeOp = Access | All | Await | Fetch | Fs | Http | Forever | Import | MemOp | Now | RandomInt | Read | Sandbox | Write | Test;
163
- export type NodeEffect<T> = Effect<NodeOp, T>;
236
+ export type NodeEffect<T, E = IoChannel> = Effect<NodeOp, T, E>;
164
237
  export type NodeOperationMap = ToAsyncOperationMap<NodeOp>;
165
238
  /**
166
239
  * The environment variables.
@@ -199,5 +272,20 @@ export type NodeProgramOptions = {
199
272
  readonly nodeVersion?: string;
200
273
  readonly inlineTestContext: boolean;
201
274
  };
202
- export type Program<O extends Operation> = (options: NodeProgramOptions) => Effect<O, number>;
275
+ /**
276
+ * A program: run it, and it answers an exit code.
277
+ *
278
+ * The code lives in a `Result` rather than in a bare `number`, and the two
279
+ * branches say which kind of code it is — `ok(0)` for success, `error(n)` for
280
+ * failure. A bare `number` could not: nothing could short-circuit on it, so a
281
+ * chain that ran one program and then another had to re-test the code by hand,
282
+ * and `step(…, () => pure(0))` was a way to report a failed program as a clean
283
+ * exit that the type system had no opinion about.
284
+ *
285
+ * **`T` is the literal `0`**, so a success carries no information beyond
286
+ * having succeeded, and `r[1]` is the exit code in *either* branch. A runner
287
+ * reads the code without asking which branch it came from; a caller that cares
288
+ * whether the program failed asks `r[0]`.
289
+ */
290
+ export type Program<O extends Operation> = (options: NodeProgramOptions) => Effect<O, 0, number>;
203
291
  export type NodeProgram = Program<NodeOp>;
@@ -4,9 +4,10 @@
4
4
  * @module
5
5
  *
6
6
  * @import { Vec } from '../../../types/bit_vec/types.ts'
7
- * @import { MemOperationMap, RunInstance } from '../../mock/types.ts'
8
- * @import { Key } from '../../memory/types.ts'
9
- * @import { Dirent, FileStat, IoResult, Module, NodeOp, NodeProgramOptions, SandboxResult } from '../types.ts'
7
+ * @import { PartialMemOperationMap, RunInstance } from '../../mock/types.ts'
8
+ * @import { Dirent, FileStat, IoError, IoResult, Module, NodeOp, NodeProgramOptions, SandboxResult } from '../types.ts'
9
+ * @import { Result } from '../../../types/result/types.ts'
10
+ * @import { Error } from '../../../types/result/types.ts'
10
11
  * @import { Dir, State, _Entity } from './types.ts'
11
12
  */
12
13
  import type { RunInstance } from '../../mock/types.ts';
@@ -14,7 +15,20 @@ import type { NodeOp, NodeProgramOptions } from '../types.ts';
14
15
  import type { State } from './types.ts';
15
16
  /** @type {State} */
16
17
  export declare const emptyState: State;
17
- /** @type {RunInstance<NodeOp, State>} */
18
+ /**
19
+ * The virtual runner.
20
+ *
21
+ * **It implements part of `NodeOp`, and says so.** `exec`, `createServer`,
22
+ * `listen`, `forever` and `test` have no meaning against an in-memory
23
+ * filesystem, and they used to be present as `todo` handlers — entries that
24
+ * existed only to satisfy a total operation map and threw when reached. They
25
+ * are simply absent now, so a program that asks for one gets
26
+ * `error(notImplemented)` back through its own continuation and decides what an
27
+ * incompatible runner means for it, which is what `NotImplemented` was
28
+ * introduced for. A command that is not a `NodeOp` at all still panics.
29
+ *
30
+ * @type {RunInstance<NodeOp, State>}
31
+ */
18
32
  export declare const virtual: RunInstance<NodeOp, State>;
19
33
  /**
20
34
  * Safe, inert defaults for every {@link NodeProgramOptions} field, intended for