functionalscript 0.46.0 → 0.47.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 (178) hide show
  1. package/fjs/basen/cbase32/module.f.mjs +18 -10
  2. package/fjs/basen/cbase32/proof.f.d.mts +3 -0
  3. package/fjs/basen/cbase32/proof.f.mjs +24 -2
  4. package/fjs/basen/proof.f.mjs +8 -16
  5. package/fjs/bnf/ll1/proof.f.d.mts +2 -0
  6. package/fjs/bnf/ll1/proof.f.mjs +33 -18
  7. package/fjs/bnf/module.f.d.mts +2 -2
  8. package/fjs/bnf/module.f.mjs +2 -2
  9. package/fjs/cas/proof.f.mjs +4 -26
  10. package/fjs/ci/common/module.f.mjs +6 -0
  11. package/fjs/ci/config/module.f.d.mts +7 -7
  12. package/fjs/ci/config/module.f.mjs +7 -7
  13. package/fjs/cli/module.f.mjs +14 -10
  14. package/fjs/crypto/vdf/module.f.d.mts +0 -1
  15. package/fjs/crypto/vdf/module.f.mjs +3 -14
  16. package/fjs/dev/module.f.mjs +10 -20
  17. package/fjs/djs/parser/module.f.d.mts +101 -53
  18. package/fjs/djs/parser/module.f.mjs +732 -525
  19. package/fjs/djs/parser/proof.f.d.mts +6 -0
  20. package/fjs/djs/parser/proof.f.mjs +315 -11
  21. package/fjs/djs/parser/types.d.ts +29 -2
  22. package/fjs/djs/tokenizer/module.f.mjs +122 -105
  23. package/fjs/djs/tokenizer/proof.f.mjs +17 -0
  24. package/fjs/djs/transpiler/module.f.mjs +4 -5
  25. package/fjs/djs/transpiler/proof.f.d.mts +2 -0
  26. package/fjs/djs/transpiler/proof.f.mjs +25 -0
  27. package/fjs/edag/amnesia/module.f.d.mts +20 -0
  28. package/fjs/edag/amnesia/module.f.mjs +303 -0
  29. package/fjs/edag/amnesia/proof.f.d.mts +79 -0
  30. package/fjs/edag/amnesia/proof.f.mjs +573 -0
  31. package/fjs/edag/amnesia/types.d.ts +15 -0
  32. package/fjs/edag/module.f.d.mts +410 -0
  33. package/fjs/edag/module.f.mjs +559 -0
  34. package/fjs/edag/proof.f.d.mts +116 -0
  35. package/fjs/edag/proof.f.mjs +686 -0
  36. package/fjs/edag/types.d.ts +55 -0
  37. package/fjs/effects/node/memory/module.d.mts +37 -3
  38. package/fjs/effects/node/memory/module.mjs +38 -4
  39. package/fjs/effects/node/memory/proof.d.mts +5 -2
  40. package/fjs/effects/node/memory/proof.mjs +36 -6
  41. package/fjs/effects/node/module.d.mts +9 -1
  42. package/fjs/effects/node/module.f.d.mts +39 -0
  43. package/fjs/effects/node/module.f.mjs +46 -0
  44. package/fjs/effects/node/module.mjs +234 -32
  45. package/fjs/effects/node/types.d.ts +28 -2
  46. package/fjs/effects/node/virtual/module.f.d.mts +19 -10
  47. package/fjs/effects/node/virtual/module.f.mjs +246 -32
  48. package/fjs/effects/node/virtual/proof.f.d.mts +17 -2
  49. package/fjs/effects/node/virtual/proof.f.mjs +232 -8
  50. package/fjs/effects/node/virtual/types.d.ts +57 -1
  51. package/fjs/emergent_testing/browser/proof.d.mts +55 -0
  52. package/fjs/emergent_testing/browser/proof.mjs +352 -0
  53. package/fjs/emergent_testing/browser/species.proof.d.mts +4 -0
  54. package/fjs/emergent_testing/browser/species.proof.mjs +45 -0
  55. package/fjs/emergent_testing/browser.d.mts +79 -0
  56. package/fjs/emergent_testing/browser.mjs +432 -0
  57. package/fjs/emergent_testing/proof.f.d.mts +1 -1
  58. package/fjs/emergent_testing/proof.f.mjs +2 -2
  59. package/fjs/js/string_escape/module.f.d.mts +47 -0
  60. package/fjs/js/string_escape/module.f.mjs +95 -0
  61. package/fjs/js/string_escape/proof.f.d.mts +8 -0
  62. package/fjs/js/string_escape/proof.f.mjs +68 -0
  63. package/fjs/js/tokenizer/module.f.mjs +14 -12
  64. package/fjs/mcp/cas/module.f.d.mts +1 -7
  65. package/fjs/mcp/cas/module.f.mjs +24 -15
  66. package/fjs/mcp/evo/module.f.mjs +9 -24
  67. package/fjs/mcp/module.f.d.mts +3 -1
  68. package/fjs/mcp/module.f.mjs +4 -2
  69. package/fjs/media/json/rtti/module.f.d.mts +2 -2
  70. package/fjs/media/json/rtti/module.f.mjs +1 -1
  71. package/fjs/media/json/schema/module.f.d.mts +15 -5
  72. package/fjs/media/json/schema/module.f.mjs +42 -15
  73. package/fjs/media/json/schema/proof.f.d.mts +10 -1
  74. package/fjs/media/json/schema/proof.f.mjs +88 -16
  75. package/fjs/media/json/serializer/module.f.mjs +14 -26
  76. package/fjs/media/json/types.d.ts +1 -1
  77. package/fjs/media/lock/module.f.d.mts +8 -3
  78. package/fjs/media/lock/module.f.mjs +9 -3
  79. package/fjs/media/lock/proof.f.d.mts +1 -1
  80. package/fjs/media/lock/proof.f.mjs +2 -2
  81. package/fjs/media/lock/types.d.ts +1 -3
  82. package/fjs/media/module.f.d.mts +20 -13
  83. package/fjs/media/module.f.mjs +29 -16
  84. package/fjs/media/note/module.f.d.mts +14 -7
  85. package/fjs/media/note/module.f.mjs +14 -7
  86. package/fjs/media/note/proof.f.mjs +1 -1
  87. package/fjs/media/proof.f.mjs +5 -5
  88. package/fjs/media/revision/module.f.d.mts +8 -2
  89. package/fjs/media/revision/module.f.mjs +9 -3
  90. package/fjs/media/revision/proof.f.mjs +1 -1
  91. package/fjs/media/revision/types.d.ts +1 -1
  92. package/fjs/media/type/module.f.d.mts +20 -0
  93. package/fjs/media/type/module.f.mjs +74 -0
  94. package/fjs/media/type/proof.f.d.mts +5 -0
  95. package/fjs/media/type/proof.f.mjs +24 -1
  96. package/fjs/module.f.mjs +6 -0
  97. package/fjs/path/module.f.d.mts +60 -3
  98. package/fjs/path/module.f.mjs +138 -14
  99. package/fjs/path/proof.f.d.mts +3 -0
  100. package/fjs/path/proof.f.mjs +298 -2
  101. package/fjs/protocol/json_rpc/module.f.d.mts +65 -28
  102. package/fjs/protocol/json_rpc/module.f.mjs +49 -13
  103. package/fjs/protocol/json_rpc/proof.f.d.mts +4 -0
  104. package/fjs/protocol/json_rpc/proof.f.mjs +29 -0
  105. package/fjs/protocol/json_rpc/types.d.ts +12 -2
  106. package/fjs/protocol/mcp/module.f.d.mts +89 -54
  107. package/fjs/protocol/mcp/module.f.mjs +110 -56
  108. package/fjs/protocol/mcp/proof.f.d.mts +13 -0
  109. package/fjs/protocol/mcp/proof.f.mjs +107 -7
  110. package/fjs/protocol/mcp/stdio/module.f.mjs +3 -4
  111. package/fjs/protocol/mcp/stdio/proof.f.mjs +2 -2
  112. package/fjs/protocol/mcp/types.d.ts +14 -2
  113. package/fjs/text/utf8/module.f.mjs +27 -1
  114. package/fjs/text/utf8/proof.f.mjs +37 -0
  115. package/fjs/types/array/module.f.mjs +18 -11
  116. package/fjs/types/array/proof.f.mjs +27 -1
  117. package/fjs/types/bigfloat/module.f.d.mts +48 -9
  118. package/fjs/types/bigfloat/module.f.mjs +147 -59
  119. package/fjs/types/bigfloat/proof.f.d.mts +2 -0
  120. package/fjs/types/bigfloat/proof.f.mjs +136 -4
  121. package/fjs/types/bigfloat/types.d.ts +17 -0
  122. package/fjs/types/function/module.f.d.mts +11 -2
  123. package/fjs/types/function/module.f.mjs +20 -2
  124. package/fjs/types/function/proof.f.mjs +5 -1
  125. package/fjs/types/phantom/types.d.ts +21 -0
  126. package/fjs/types/result/module.f.d.mts +4 -4
  127. package/fjs/types/result/module.f.mjs +2 -2
  128. package/fjs/types/result/proof.f.d.mts +6 -2
  129. package/fjs/types/result/proof.f.mjs +17 -1
  130. package/fjs/types/rtti/common/module.f.d.mts +78 -5
  131. package/fjs/types/rtti/common/module.f.mjs +168 -3
  132. package/fjs/types/rtti/common/proof.f.d.mts +14 -0
  133. package/fjs/types/rtti/common/proof.f.mjs +61 -2
  134. package/fjs/types/rtti/common/types.d.ts +23 -1
  135. package/fjs/types/rtti/data/module.f.d.mts +57 -1
  136. package/fjs/types/rtti/data/module.f.mjs +241 -47
  137. package/fjs/types/rtti/data/proof.f.d.mts +10 -0
  138. package/fjs/types/rtti/data/proof.f.mjs +272 -39
  139. package/fjs/types/rtti/data/types.d.ts +14 -7
  140. package/fjs/types/rtti/host.proof.d.mts +33 -0
  141. package/fjs/types/rtti/host.proof.mjs +170 -0
  142. package/fjs/types/rtti/module.f.d.mts +51 -6
  143. package/fjs/types/rtti/module.f.mjs +50 -3
  144. package/fjs/types/rtti/parse/module.f.d.mts +31 -24
  145. package/fjs/types/rtti/parse/module.f.mjs +197 -59
  146. package/fjs/types/rtti/parse/proof.f.d.mts +37 -2
  147. package/fjs/types/rtti/parse/proof.f.mjs +171 -47
  148. package/fjs/types/rtti/proof.f.d.mts +6 -0
  149. package/fjs/types/rtti/proof.f.mjs +42 -1
  150. package/fjs/types/rtti/ts/module.f.d.mts +9 -3
  151. package/fjs/types/rtti/ts/module.f.mjs +68 -15
  152. package/fjs/types/rtti/ts/proof.f.d.mts +42 -2
  153. package/fjs/types/rtti/ts/proof.f.mjs +139 -6
  154. package/fjs/types/rtti/ts/types.d.ts +164 -16
  155. package/fjs/types/rtti/types.d.ts +31 -3
  156. package/fjs/types/rtti/validate/module.f.d.mts +120 -0
  157. package/fjs/types/rtti/validate/module.f.mjs +351 -0
  158. package/fjs/types/rtti/validate/proof.f.d.mts +154 -0
  159. package/fjs/types/rtti/validate/proof.f.mjs +781 -0
  160. package/fjs/types/ts/types.d.ts +4 -0
  161. package/fjs/web/module.f.d.mts +115 -0
  162. package/fjs/web/module.f.mjs +581 -0
  163. package/fjs/web/proof.f.d.mts +39 -0
  164. package/fjs/web/proof.f.mjs +413 -0
  165. package/fjs/web/types.d.ts +39 -0
  166. package/fjs/website/browser-prepare.d.mts +5 -0
  167. package/fjs/website/browser-prepare.mjs +115 -0
  168. package/fjs/website/browser-source.d.mts +37 -0
  169. package/fjs/website/browser-source.mjs +202 -0
  170. package/fjs/website/browser-source.proof.d.mts +31 -0
  171. package/fjs/website/browser-source.proof.mjs +162 -0
  172. package/fjs/website/browser.d.mts +5 -0
  173. package/fjs/website/browser.mjs +8 -0
  174. package/fjs/website/module.f.d.mts +3 -3
  175. package/fjs/website/module.f.mjs +62 -9
  176. package/fjs/website/proof.f.d.mts +4 -0
  177. package/fjs/website/proof.f.mjs +18 -3
  178. package/package.json +2 -3
@@ -5,20 +5,22 @@
5
5
  *
6
6
  * @import { Vec } from '../../../types/bit_vec/types.ts'
7
7
  * @import { PartialMemOperationMap, RunInstance } from '../../mock/types.ts'
8
- * @import { Dirent, FileStat, IoError, IoResult, Module, NodeOp, NodeProgramOptions, SandboxResult } from '../types.ts'
8
+ * @import { Dirent, FileStat, IoError, IoResult, Module, NodeOp, NodeProgramOptions, OpResult, RequestListener, SandboxResult, Server } from '../types.ts'
9
+ * @import { Operation } from '../../types.ts'
9
10
  * @import { Result } from '../../../types/result/types.ts'
10
11
  * @import { Error } from '../../../types/result/types.ts'
11
- * @import { Dir, State, _Entity } from './types.ts'
12
+ * @import { Dir, JsModule, State, _Entity, _VirtualListener, _VirtualServer } from './types.ts'
12
13
  */
13
14
 
14
15
  import { assert, todo } from '../../../asserts/module.f.mjs'
15
16
  import { isProperPrefix, join, parse } from '../../../path/module.f.mjs'
16
17
  import { utf8ToString } from '../../../text/module.f.mjs'
17
18
  import { empty, length, maxLengthBytes, msb, vec } from '../../../types/bit_vec/module.f.mjs'
18
- import { error, ok } from '../../../types/result/module.f.mjs'
19
- import { ioError, nodeCommands } from '../module.f.mjs'
19
+ import { error, ok, unwrap } from '../../../types/result/module.f.mjs'
20
+ import { emptyHost, emptyHostError, ioError, nodeCommands } from '../module.f.mjs'
20
21
  import { partialRun } from '../../mock/module.f.mjs'
21
22
  import { asBase, asNominal } from '../../memory/module.f.mjs'
23
+ import { asBase as asBaseServer, asNominal as asNominalServer } from '../../../types/nominal/module.f.mjs'
22
24
 
23
25
  /** @type {State} */
24
26
  export const emptyState = {
@@ -31,8 +33,29 @@ export const emptyState = {
31
33
  memoryNext: 0,
32
34
  memoryValues: {},
33
35
  randomNext: 0,
36
+ listening: [],
37
+ requests: [],
38
+ responses: [],
34
39
  }
35
40
 
41
+ /**
42
+ * @param {_Entity} entity
43
+ * @returns {entity is readonly Vec[]}
44
+ */
45
+ const isBinFile = entity => Array.isArray(entity)
46
+
47
+ /**
48
+ * @param {_Entity} entity
49
+ * @returns {entity is JsModule}
50
+ */
51
+ const isJsModule = entity => typeof entity === 'function'
52
+
53
+ /**
54
+ * @param {_Entity} entity
55
+ * @returns {entity is Dir}
56
+ */
57
+ const isDir = entity => !isBinFile(entity) && !isJsModule(entity)
58
+
36
59
  /**
37
60
  * @template T
38
61
  * @param {(dir: Dir, path: readonly string[]) => readonly [Dir, T]} op
@@ -46,10 +69,7 @@ const operation = op => {
46
69
  }
47
70
  const [first, ...rest] = path
48
71
  const subDir = dir[first]
49
- // `instanceof Array`, not `Array.isArray`: the latter narrows to `any[]`,
50
- // which `readonly Vec[]` is not assignable to, so its negative branch never
51
- // removes a `readonly` array from `_Entity`. Only `instanceof` narrows here.
52
- if (typeof subDir !== 'object' || subDir instanceof Array) {
72
+ if (subDir === undefined || !isDir(subDir)) {
53
73
  return op(dir, path)
54
74
  }
55
75
  const [newSubDir, r] = f(subDir, rest)
@@ -105,12 +125,12 @@ const enoent = error(ioError({ code: 'ENOENT', message: 'no such file or directo
105
125
  const readFile = readOperation((dir, path) => {
106
126
  if (path.length !== 1) { return enoent }
107
127
  const file = dir[path[0]]
108
- if (typeof file === 'function') { throw new Error(`'${path[0]}' is a JsModule; readFile not supported`) }
109
128
  if (file === undefined) { return enoent }
129
+ if (isJsModule(file)) { throw new Error(`'${path[0]}' is a JsModule; readFile not supported`) }
110
130
  // `operation`'s wrapper descends into every plain-object (`Dir`) entry
111
131
  // before this op ever runs, and the `JsModule` case already threw above,
112
132
  // so `file` here is always a `Vec[]` — never a bare `Dir`.
113
- assert(Array.isArray(file), `'${path[0]}' is not a file`)
133
+ assert(isBinFile(file), `'${path[0]}' is not a file`)
114
134
  const chunks = file
115
135
  const capBits = maxLengthBytes * 8n
116
136
  let result = empty
@@ -129,7 +149,7 @@ const readFile = readOperation((dir, path) => {
129
149
  const import_ = readOperation((dir, path) => {
130
150
  if (path.length !== 1) { return fail('no such file') }
131
151
  const entry = dir[path[0]]
132
- if (typeof entry !== 'function') { return fail(`'${path[0]}' is not a JsModule`) }
152
+ if (entry === undefined || !isJsModule(entry)) { return fail(`'${path[0]}' is not a JsModule`) }
133
153
  return ok(entry())
134
154
  })
135
155
 
@@ -140,7 +160,7 @@ const writeFileOp = payload => (dir, path) => {
140
160
  if (path.length !== 1) { return [dir, writeFileError] }
141
161
  const [name] = path
142
162
  const file = dir[name]
143
- if (file !== undefined && !Array.isArray(file)) { return [dir, writeFileError] }
163
+ if (file !== undefined && !isBinFile(file)) { return [dir, writeFileError] }
144
164
  dir = { ...dir, [name]: [payload] }
145
165
  return [dir, okVoid]
146
166
  }
@@ -161,7 +181,7 @@ const readdir = (base, recursive) => readOperation((dir, path) => {
161
181
  let result = []
162
182
  for (const [name, content] of entries(d)) {
163
183
  if (content === undefined) { continue }
164
- const isFile = content instanceof Array || typeof content !== 'object'
184
+ const isFile = !isDir(content)
165
185
  result = [...result, { name, parentPath, isFile }]
166
186
  if (!isFile && recursive) {
167
187
  result = [...result, ...f(join(parentPath, name), content)]
@@ -209,7 +229,7 @@ const extractEntity = (dir, path) => {
209
229
  }
210
230
  const [first, ...rest] = path
211
231
  const sub = dir[first]
212
- if (sub === undefined || sub instanceof Array || typeof sub === 'function') { return [dir, enoent] }
232
+ if (sub === undefined || !isDir(sub)) { return [dir, enoent] }
213
233
  const [newSub, result] = extractEntity(sub, rest)
214
234
  if (result[0] === 'error') { return [dir, result] }
215
235
  return [{ ...dir, [first]: newSub }, result]
@@ -228,8 +248,8 @@ const insertEntityAt = (dir, path, entity) => {
228
248
  const [name] = path
229
249
  const existing = dir[name]
230
250
  if (existing !== undefined) {
231
- const entityIsDir = !Array.isArray(entity) && typeof entity === 'object'
232
- const existingIsDir = !Array.isArray(existing) && typeof existing === 'object'
251
+ const entityIsDir = isDir(entity)
252
+ const existingIsDir = isDir(existing)
233
253
  if (entityIsDir && !existingIsDir) {
234
254
  return [dir, fail(`cannot overwrite file '${name}' with a directory`)]
235
255
  }
@@ -249,7 +269,7 @@ const insertEntityAt = (dir, path, entity) => {
249
269
  const [first, ...rest] = path
250
270
  const sub = dir[first]
251
271
  if (sub === undefined) { return [dir, enoent] }
252
- if (sub instanceof Array || typeof sub === 'function') { return [dir, fail('not a directory')] }
272
+ if (!isDir(sub)) { return [dir, fail('not a directory')] }
253
273
  const [newSub, result] = insertEntityAt(sub, rest, entity)
254
274
  if (result[0] === 'error') { return [dir, result] }
255
275
  return [{ ...dir, [first]: newSub }, result]
@@ -276,12 +296,12 @@ const rename = (src, dst) => state => {
276
296
  const readBytesOp = (path, offset, size) => readOperation((dir, p) => {
277
297
  if (p.length !== 1) { return enoent }
278
298
  const file = dir[p[0]]
279
- if (typeof file === 'function') { throw new Error(`'${p[0]}' is a JsModule; readBytes not supported`) }
280
299
  if (file === undefined) { return enoent }
300
+ if (isJsModule(file)) { throw new Error(`'${p[0]}' is a JsModule; readBytes not supported`) }
281
301
  // `operation`'s wrapper descends into every plain-object (`Dir`) entry
282
302
  // before this op ever runs, and the `JsModule` case already threw above,
283
303
  // so `file` here is always a `Vec[]` — never a bare `Dir`.
284
- assert(Array.isArray(file), `'${p[0]}' is not a file`)
304
+ assert(isBinFile(file), `'${p[0]}' is not a file`)
285
305
  if (!Number.isInteger(offset)) { return fail(`Offset ${offset} is not an integer`) }
286
306
  if (!Number.isInteger(size)) { return fail(`Chunk size ${size} is not an integer`) }
287
307
  if (offset < 0) { return fail(`Offset ${offset} is negative`) }
@@ -306,6 +326,12 @@ const readBytesOp = (path, offset, size) => readOperation((dir, p) => {
306
326
  return ok(result)
307
327
  })(path)
308
328
 
329
+ /** What `stat` answers for a name that exists and is not a regular file.
330
+ *
331
+ * @type {IoResult<FileStat>}
332
+ */
333
+ const notRegular = ok({ size: 0, isFile: false })
334
+
309
335
  /** Total byte size of a chunk-list file (each chunk is byte-aligned).
310
336
  *
311
337
  * @type {(chunks: readonly Vec[]) => number}
@@ -339,7 +365,7 @@ const writeBytesRawOp = (offset, data) => (dir, p) => {
339
365
  const [name] = p
340
366
  const file = dir[name]
341
367
  if (file === undefined) { return [dir, enoent] } // writeBytes never creates
342
- if (!Array.isArray(file)) { return [dir, fail(`'${name}' is not a file`)] }
368
+ if (!isBinFile(file)) { return [dir, fail(`'${name}' is not a file`)] }
343
369
  if (!Number.isInteger(offset) || offset < 0) { return [dir, fail(`Offset ${offset} is invalid`)] }
344
370
  const chunks = file
345
371
  if (offset !== fileSizeBytes(chunks)) {
@@ -351,15 +377,193 @@ const writeBytesRawOp = (offset, data) => (dir, p) => {
351
377
  /** @type {(path: string, offset: number, data: Vec) => (state: State) => readonly [State, IoResult<void>]} */
352
378
  const writeBytesOp = (path, offset, data) => operation(writeBytesRawOp(offset, data))(path)
353
379
 
354
- /** @type {(path: string) => (state: State) => readonly [State, IoResult<FileStat>]} */
355
- const statOp = readOperation((dir, path) => {
380
+ /**
381
+ * `stat` reports what is there, including when what is there is not a regular
382
+ * file — a host stats a directory or a FIFO successfully and says it is not a
383
+ * file, and a caller's guard against reading one can only be exercised here if
384
+ * this runner does the same.
385
+ *
386
+ * Two entries answer `isFile: false`. A `JsModule` is this file system's stand-in
387
+ * for a name that exists and is not a file at all. A **directory** arrives as an
388
+ * empty remaining path, because `operation` has already descended into it — the
389
+ * one way to reach `statOp` with nothing left to look up — and that is what it
390
+ * means, root included.
391
+ *
392
+ * @type {(path: string) => (state: State) => readonly [State, IoResult<FileStat>]}
393
+ */
394
+ const statPath = readOperation((dir, path) => {
395
+ if (path.length === 0) { return notRegular }
356
396
  if (path.length !== 1) { return enoent }
357
397
  const file = dir[path[0]]
358
398
  if (file === undefined) { return enoent }
359
- if (!Array.isArray(file)) { return fail(`'${path[0]}' is not a file`) }
360
- return ok({ size: fileSizeBytes(file) })
399
+ // `isBinFile` rather than a local `Array.isArray`: which entity kind a name
400
+ // holds is asked in one place now (#1697), and `stat` is one of its askers.
401
+ if (!isBinFile(file)) { return notRegular }
402
+ return ok({ size: fileSizeBytes(file), isFile: true })
361
403
  })
362
404
 
405
+ /**
406
+ * An empty path names nothing, and `parse` cannot say so: it collapses to the
407
+ * same empty segment list `.` does, and `.` is the root. A host answers `ENOENT`
408
+ * for `stat('')`, so this asks the question `parse` has already thrown away —
409
+ * before the answer can depend on it.
410
+ *
411
+ * @type {(path: string) => (state: State) => readonly [State, IoResult<FileStat>]}
412
+ */
413
+ const statOp = path => path === '' ? state => [state, enoent] : statPath(path)
414
+
415
+ // ── HTTP ──────────────────────────────────────────────────────────────────────
416
+ //
417
+ // There are no sockets here, but the two operations that *set up* a server still
418
+ // have a faithful meaning without one: `createServer` hands back a handle, and
419
+ // `listen` accepts exactly the requests the fixture queued. That is what makes a
420
+ // request-in / response-out proof possible against the in-memory filesystem —
421
+ // the same listener the Node runner would drive, driven by fixture data instead
422
+ // of a socket. `forever` is the one HTTP-adjacent operation with no meaning
423
+ // here; see the note on {@link virtual} below.
424
+
425
+ /**
426
+ * Whether `port` is one a host would accept: an integer in `0`–`65535`, where
427
+ * `0` asks for an ephemeral one. Node throws `ERR_SOCKET_BAD_PORT` for anything
428
+ * else, and a runner that accepted `-1` or `NaN` would let a program be proven
429
+ * that cannot run.
430
+ *
431
+ * @type {(port: number) => boolean}
432
+ */
433
+ const isPort = port => Number.isInteger(port) && port >= 0 && port <= maxPort
434
+
435
+ /** @type {number} */
436
+ const maxPort = 0xffff
437
+
438
+ /** The port that asks for any free port rather than naming one.
439
+ *
440
+ * @type {number}
441
+ */
442
+ const ephemeral = 0
443
+
444
+ /**
445
+ * The handle **is** the listener.
446
+ *
447
+ * `Server` is a nominal over `unknown`, so what a runner keeps inside one is its
448
+ * own business: this one keeps the listener there, and `listen` takes it back
449
+ * out. In the state instead, it would model a runner with room for one server —
450
+ * two `createServer` calls and the second silently answers for the first, where
451
+ * the Node runner gives each its own socket — so it rides in the handle, and two
452
+ * servers in one program are two servers here too.
453
+ *
454
+ * A fresh record wraps it for that last reason: two calls with the *same*
455
+ * listener must still be two servers, and a handle that was the listener itself
456
+ * made them one — which `listen` then read as a single server listening twice.
457
+ *
458
+ * @type {(listener: RequestListener<Operation>) => (state: State) => readonly[State, OpResult<Server>]}
459
+ */
460
+ const createServer = listener => state => {
461
+ // The same narrowing the Node runner performs before handing a request to
462
+ // it: `CreateServer` declares its listener over `Operation`, since a
463
+ // `Server` must not carry a type parameter, and a runner can only run one
464
+ // at its own operation set.
465
+ /** @type {_VirtualServer} */
466
+ const server = { listener: /** @type {_VirtualListener} */ (listener) }
467
+ /** @type {Server} */
468
+ const handle = asNominalServer(server)
469
+ return [state, ok(handle)]
470
+ }
471
+
472
+ /**
473
+ * Takes the address, then hands `server`'s listener every queued request in
474
+ * turn, threading the state through each and recording what came back. The
475
+ * queue is emptied, so a second `listen` does not re-deliver.
476
+ *
477
+ * **Binding can fail here, because it can fail on a host.** A port outside
478
+ * `0`–`65535` or not an integer is `ERR_SOCKET_BAD_PORT`, an address already
479
+ * taken is `EADDRINUSE`, and a server asked to listen twice is
480
+ * `ERR_SERVER_ALREADY_LISTEN` — the three failures Node reports, in the shape it
481
+ * reports them. Port `0` is the exception to the second: it names no port, so
482
+ * two servers asking for one do not collide. `Listen` became fallible precisely to carry them, and a runner
483
+ * that always succeeded would make a program that mishandles either look
484
+ * correct.
485
+ *
486
+ * **`unwrap` is total, and the type system is what makes it so.** A
487
+ * `RequestListener`'s channel is `never`, and a listener reaches that by
488
+ * absorbing every `Result` its effects produce — through `resultStep` /
489
+ * `resultMapStep`, which receive the whole `Result`, including this runner's
490
+ * `notImplemented` for an operation it lacks. So a listener that calls `exec`
491
+ * here does not fail: it is handed the refusal and answers with a response
492
+ * frame, which is the contract `RequestListener` states.
493
+ *
494
+ * A listener that propagated instead would make this throw — but such a
495
+ * listener does not type-check in that position (`Type 'IoChannel' is not
496
+ * assignable to type 'never'`), so the throw is the checked assertion of an
497
+ * invariant, not a case to handle. Handling it defensively would add a branch
498
+ * nothing can reach, which the coverage gate rejects and
499
+ * [`fjs/AGENTS.md`](../../../AGENTS.md) §1.2 tells you to restructure away.
500
+ *
501
+ * The listener comes out of the handle rather than out of the state, so which
502
+ * server was asked to listen is the one that answers — see {@link createServer}.
503
+ *
504
+ * @type {(server: Server, port: number, host: string) => (state: State) => readonly[State, IoResult<void>]}
505
+ */
506
+ const listen = (server, port, host) => state => {
507
+ // Asked **first**, as the Node runner asks it: an empty host is refused
508
+ // before anything else is looked at, including whether this server is
509
+ // already listening. Node forms no opinion about `''` at all — it binds —
510
+ // so there is no host order to copy here and the two runners simply have to
511
+ // pick the same one. They did not at first: this check sat below the
512
+ // listening one, and an already-listening server retried with `''` reported
513
+ // `ERR_SERVER_ALREADY_LISTEN` here against `ERR_INVALID_ARG_VALUE` there.
514
+ if (host === emptyHost) { return [state, error(emptyHostError)] }
515
+ const bound = /** @type {_VirtualServer} */ (asBaseServer(server))
516
+ const { listener } = bound
517
+ // Asked **before** the port, because that is the order Node asks in: a
518
+ // server already listening reports `ERR_SERVER_ALREADY_LISTEN` for `-1`,
519
+ // `1.5`, `65536` and `NaN` alike — checked on Linux with Node 22.22.2 and on
520
+ // Darwin with Node 23.11.0, where the same values on a fresh server all
521
+ // report `ERR_SOCKET_BAD_PORT`. Which of two true things a failure names is
522
+ // part of what a runner promises, so a program branching on the code
523
+ // branches the same way here as there.
524
+ if (state.listening.some(b => b.server === bound)) {
525
+ return [state, error(ioError({
526
+ code: 'ERR_SERVER_ALREADY_LISTEN',
527
+ message: 'Listen method has been called more than once without closing.',
528
+ }))]
529
+ }
530
+ if (!isPort(port)) {
531
+ return [state, error(ioError({
532
+ code: 'ERR_SOCKET_BAD_PORT',
533
+ // Byte-for-byte what Node says, type included: this runner claims
534
+ // to report failures in the shape the host reports them, and a
535
+ // message that is nearly right is a claim that is not.
536
+ message: `options.port should be >= 0 and < 65536. Received type number (${port}).`,
537
+ }))]
538
+ }
539
+ // Lower-cased because a DNS name is case-insensitive and so is the
540
+ // hexadecimal of an IPv6 literal: `LOCALHOST` and `localhost` are one
541
+ // address, and a host refuses the second bind — checked on Linux with Node
542
+ // 22.22.2 and on Darwin with Node 23.11.0. Unlike the wildcard rules in
543
+ // [address-model](./todo/address-model.md), that equivalence is the same
544
+ // everywhere, so it is safe to model.
545
+ const address = `${host.toLowerCase()}:${port}`
546
+ // Port `0` asks the host for whichever port is free, so two servers that
547
+ // ask both get one — checked on Linux with Node 22.22.2 and on Darwin with
548
+ // Node 23.11.0: they come back bound to different ports.
549
+ // Comparing `host:0` to `host:0` would refuse the second, which is the worse
550
+ // direction to be wrong in: a runner that rejects a program a host accepts
551
+ // stops work that would have run.
552
+ if (port !== ephemeral && state.listening.some(b => b.address === address)) {
553
+ return [state, error(ioError({
554
+ code: 'EADDRINUSE',
555
+ message: `listen EADDRINUSE: address already in use ${address}`,
556
+ }))]
557
+ }
558
+ /** @type {State} */
559
+ let s = { ...state, listening: [...state.listening, { address, server: bound }], requests: [] }
560
+ for (const request of state.requests) {
561
+ const [next, response] = virtual(s)(listener(request))
562
+ s = { ...next, responses: [...next.responses, unwrap(response)] }
563
+ }
564
+ return [s, okVoid]
565
+ }
566
+
363
567
  /** @type {PartialMemOperationMap<NodeOp, State>} */
364
568
  const map = {
365
569
  all: (...a) => state => {
@@ -409,6 +613,8 @@ const map = {
409
613
  createExclusive,
410
614
  writeBytes: writeBytesOp,
411
615
  stat: statOp,
616
+ createServer,
617
+ listen,
412
618
  randomInt: () => state => [{ ...state, randomNext: state.randomNext + 1 }, ok(state.randomNext)],
413
619
  now: () => state => [state, ok(state.epochNs)],
414
620
  // Virtual sandbox is a pass-through: the fixture's test function is
@@ -435,14 +641,22 @@ const map = {
435
641
  /**
436
642
  * The virtual runner.
437
643
  *
438
- * **It implements part of `NodeOp`, and says so.** `exec`, `createServer`,
439
- * `listen`, `forever` and `test` have no meaning against an in-memory
440
- * filesystem, and they used to be present as `todo` handlers — entries that
441
- * existed only to satisfy a total operation map and threw when reached. They
442
- * are simply absent now, so a program that asks for one gets
443
- * `error(notImplemented)` back through its own continuation and decides what an
444
- * incompatible runner means for it, which is what `NotImplemented` was
445
- * introduced for. A command that is not a `NodeOp` at all still panics.
644
+ * **It implements part of `NodeOp`, and says so.** `exec`, `forever` and `test`
645
+ * have no meaning against an in-memory filesystem, and they used to be present
646
+ * as `todo` handlers — entries that existed only to satisfy a total operation
647
+ * map and threw when reached. They are simply absent now, so a program that
648
+ * asks for one gets `error(notImplemented)` back through its own continuation
649
+ * and decides what an incompatible runner means for it, which is what
650
+ * `NotImplemented` was introduced for. A command that is not a `NodeOp` at all
651
+ * still panics.
652
+ *
653
+ * `forever` is absent for a reason no implementation could remove: its result
654
+ * type is `Result<never, NotImplemented>`, so `error(notImplemented)` is the
655
+ * *only* value it can produce — a runner that cannot block forever has nothing
656
+ * else to answer, and a server program run here therefore ends where it would
657
+ * otherwise have blocked. `createServer` and `listen` do have meanings without
658
+ * a socket and are implemented above, which is what makes the rest of such a
659
+ * program provable.
446
660
  *
447
661
  * @type {RunInstance<NodeOp, State>}
448
662
  */
@@ -1,6 +1,6 @@
1
1
  /**
2
- * @import { Dir } from './types.ts'
3
- * @import { NodeOp } from '../types.ts'
2
+ * @import { Dir, State } from './types.ts'
3
+ * @import { IncomingMessage, NodeOp, RequestListener } from '../types.ts'
4
4
  * @import { Effect } from '../../types.ts'
5
5
  * @import { IoChannel } from '../types.ts'
6
6
  */
@@ -51,6 +51,7 @@ export declare const proof: {
51
51
  renameNonEmptyDirOverEmptyDir: () => void;
52
52
  renameEmptyDirOverNonEmptyDir: () => void;
53
53
  renameFileOntoDirectory: () => void;
54
+ renameForeignArrayFileOntoDirectory: () => void;
54
55
  readFileTooLarge: () => void;
55
56
  readBytesNegativeSize: () => void;
56
57
  readBytesZeroSize: () => void;
@@ -59,6 +60,20 @@ export declare const proof: {
59
60
  readBytesFractionalOffset: () => void;
60
61
  readBytesAcrossChunkBoundary: () => void;
61
62
  writeBytesWrongOffset: () => void;
63
+ statOnDirectory: () => void;
64
+ statOnEmptyPath: () => void;
62
65
  statOnJsModule: () => void;
63
66
  largeFileReadBytes: () => void;
67
+ http: {
68
+ answersQueuedRequests: () => void;
69
+ dispatchesThroughTheHandle: () => void;
70
+ badPort: () => void;
71
+ ephemeralPorts: () => void;
72
+ addressInUse: () => void;
73
+ addressInUseIgnoresCase: () => void;
74
+ emptyHostRefused: () => void;
75
+ emptyHostBeatsAlreadyListening: () => void;
76
+ alreadyListening: () => void;
77
+ foreverIsNotImplemented: () => void;
78
+ };
64
79
  };