functionalscript 0.46.1 → 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 +42 -25
  157. package/fjs/types/rtti/validate/module.f.mjs +187 -53
  158. package/fjs/types/rtti/validate/proof.f.d.mts +28 -2
  159. package/fjs/types/rtti/validate/proof.f.mjs +430 -75
  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
@@ -1,14 +1,84 @@
1
1
  /**
2
- * @import { Type } from '../types.ts'
2
+ * @import { Or, Type } from '../types.ts'
3
3
  * @import { Data } from '../data/types.ts'
4
+ * @import { Ts, TupleTs } from './types.ts'
5
+ * @import { Assert } from '../../../asserts/types.ts'
6
+ * @import { Equal } from '../../ts/types.ts'
4
7
  */
5
8
 
6
9
  import { assertEq } from '../../../asserts/module.f.mjs'
7
10
  import { toData, unitBit } from '../data/module.f.mjs'
8
- import { boolean, number, string, bigint, unknown, array, record, or, option, never } from '../module.f.mjs'
11
+ import { boolean, number, string, bigint, unknown, array, open, record, or, option, rest, never } from '../module.f.mjs'
9
12
  import { dataToTs, printer } from './module.f.mjs'
10
13
 
14
+ // ── `Ts<T>` over a tuple schema ─────────────────────────────────────────────
15
+ //
16
+ // Spelled as schema *types* rather than `typeof` a value: these are type-level
17
+ // facts, and a value existing only to be pointed at is an unused one.
18
+ //
19
+ // `TupleTs` splits off the trailing run of positions admitting `undefined` and
20
+ // renders it optional, which needs a known length. A schema array of non-fixed
21
+ // length — what `.map()` produces — has no trailing position to split off, so
22
+ // it keeps its element type instead, the homomorphic mapping's answer. Pinned
23
+ // because a split that falls back to the empty tuple silently renders such a
24
+ // schema `readonly []`, and nothing else here would have caught it.
25
+ /** @typedef {Assert<Equal<Ts<readonly (typeof number | typeof bigint)[]>, readonly (number | bigint)[]>>} _NonFixedLength */
26
+
27
+ // `option(t)` is `or(t, undefined)`; these are the schema types it produces.
28
+ /** @typedef {Or<readonly [typeof boolean, undefined]>} _OptionBoolean */
29
+ /** @typedef {Or<readonly [typeof string, undefined]>} _OptionString */
30
+
31
+ // A variadic tuple is the shape the `length` guard exists for, and the only
32
+ // one: its peel *succeeds*, binding the unknown-length prefix to `I`, so
33
+ // without the guard the reconstruction flattens it. The others below reach the
34
+ // fallback because their peel fails, and are held by that alone.
35
+ //
36
+ // Asserted as assignability rather than with `Equal<>`. `Equal<>` reports this
37
+ // shape as unchanged whether or not the guard is in place — it cannot see the
38
+ // difference — so an `Equal<>` pin here passes over the bug it is meant to
39
+ // catch. What the flattening actually costs is a string admitted in the number
40
+ // prefix, so that is what these state.
41
+ /** @typedef {readonly [...(typeof number)[], _OptionString]} _VariadicSchema */
42
+ /** @typedef {Assert<readonly [1, 'x', 2] extends Ts<_VariadicSchema> ? false : true>} _VariadicPrefixRejectsMixedPrefix */
43
+ /** @typedef {Assert<readonly [1, 2, 'x'] extends Ts<_VariadicSchema> ? true : false>} _VariadicPrefixAdmitsItsOwnShape */
44
+
45
+ // A rest element after a fixed prefix is the same shape from the other side,
46
+ // and is held for the same reason: `length` is `number`, so the mapping stands.
47
+ //
48
+ // This row and `_NonFixedLength` document intent rather than discriminate a
49
+ // mechanism. The guard and the fallback both answer `M` for these two shapes,
50
+ // so neither single mutation moves them — only removing both at once does.
51
+ // The rows that pin one mechanism each are `_VariadicPrefixRejectsMixedPrefix`
52
+ // (the guard), `_OptionalMember` (the fallback) and
53
+ // `_UnionKeepsBranchCorrelation` (the distribution).
54
+ /** @typedef {Assert<Equal<Ts<readonly [typeof number, ...(typeof string)[]]>, readonly [number, ...string[]]>>} _RestTuple */
55
+
56
+ // A schema whose own tuple type already marks a member optional is held by the
57
+ // *fallback* rather than the length guard: its length is `1 | 2`, not `number`,
58
+ // so it reaches the split, where the peel needs a required last element and
59
+ // finds none. An optional position is what this transform produces, so one the
60
+ // caller wrote is already in the target form and the mapping stands.
61
+ /** @typedef {Assert<Equal<Ts<readonly [typeof number, (typeof string)?]>, readonly [number, string?]>>} _OptionalMember */
62
+
63
+ // A union of tuple schemas is split per member, not once across the union.
64
+ // Splitting the union lets the two halves distribute independently and the
65
+ // spread then pairs every prefix with every suffix, so `[number, boolean]` —
66
+ // A's prefix with B's suffix — would pass. Assignability again: this is a
67
+ // statement about which values the union admits.
68
+ /** @typedef {readonly [typeof number, _OptionString]} _BranchA */
69
+ /** @typedef {readonly [typeof string, _OptionBoolean, _OptionNumber]} _BranchB */
70
+ /** @typedef {Or<readonly [typeof number, undefined]>} _OptionNumber */
71
+ /** @typedef {Assert<readonly [1, true] extends TupleTs<_BranchA | _BranchB> ? false : true>} _UnionKeepsBranchCorrelation */
72
+ /** @typedef {Assert<readonly [1, 'x'] extends TupleTs<_BranchA | _BranchB> ? true : false>} _UnionAdmitsItsOwnBranches */
73
+
74
+ /** @typedef {Assert<Equal<Ts<readonly [typeof number, typeof bigint, _OptionBoolean, _OptionString]>, readonly [number, bigint, (boolean | undefined)?, (string | undefined)?]>>} _OptionalTail */
75
+
76
+ // Only the *trailing* run: TypeScript forbids a required element after an
77
+ // optional one, so an interior position that admits `undefined` stays required.
78
+ /** @typedef {Assert<Equal<Ts<readonly [_OptionString, typeof number]>, readonly [string | undefined, number]>>} _InteriorStaysRequired */
79
+
11
80
  const toTs = printer()
81
+
12
82
  const toTsMut = printer(true)
13
83
 
14
84
  /** @type {(rtti: Type, expected: string) => void} */
@@ -96,10 +166,24 @@ export const proof = {
96
166
  negInf: () => eq(-Infinity, 'number'),
97
167
  string: () => eq('hello', '"hello"'),
98
168
  bigint: () => eq(7n, '7n'),
169
+ // a bare tuple is closed, so it prints exactly — the same rendering
170
+ // `Ts<>` gives it, which is what makes that cast sound
99
171
  emptyTuple: () => eq([], 'readonly[]'),
100
172
  tuple: () => eq([12, true], 'readonly[12,true]'),
101
- // an unconstrained struct is the whole object kind
102
- emptyStruct: () => eq({}, '{readonly[k in string]?:unknown}'),
173
+ // a position the array may end before prints optional, as the key it
174
+ // is the array counterpart of does
175
+ optionalTuplePosition: () => eq(
176
+ [number, option(string)],
177
+ 'readonly[number,(undefined|string)?]',
178
+ ),
179
+ allOptionalTuple: () => eq(
180
+ [option(number)],
181
+ 'readonly[(undefined|number)?]',
182
+ ),
183
+ // a declared `unknown` key is a key the container has, so it is not
184
+ // dropped the way an `open` struct's is
185
+ emptyStruct: () => eq({}, '{}'),
186
+ unknownProp: () => eq({ a: unknown }, '{readonly"a"?:unknown}'),
103
187
  struct: () => eq(
104
188
  { a: number, b: string },
105
189
  '{readonly"a":number,readonly"b":string}',
@@ -128,7 +212,45 @@ export const proof = {
128
212
  thunks: () => eq(or(number, string), 'number|string'),
129
213
  mixed: () => eq(or(42, string), '42|string'),
130
214
  },
215
+ // The open and stated-rest forms. An object type is structurally open in
216
+ // TypeScript, so an `open` struct prints as wide as it can be printed;
217
+ // a tuple has a rest element, so this printer says exactly what the schema
218
+ // says — `Ts<>` renders the same tail, for the same reason.
219
+ open: {
220
+ // an unconstrained tuple, or struct, is the whole kind
221
+ emptyTuple: () => eq(open([]), 'readonly(unknown)[]'),
222
+ unconstrainedTuple: () => eq(open([unknown]), 'readonly(unknown)[]'),
223
+ tuple: () => eq(open([12, true]), 'readonly[12,true,...readonly(unknown)[]]'),
224
+ emptyStruct: () => eq(open({}), '{readonly[k in string]?:unknown}'),
225
+ struct: () => eq(open({ a: number }), '{readonly"a":number}'),
226
+ // an unconstrained key *is* dropped once the container is open
227
+ unknownProp: () => eq(open({ a: unknown }), '{readonly[k in string]?:unknown}'),
228
+ // a stated rest prints as the rest element / index signature it is.
229
+ // The tail admits `undefined` because a hole past the prefix is no
230
+ // member, so a reader skips it and the index reads `undefined`.
231
+ tupleRest: () => eq(rest([number], string), 'readonly[number,...readonly(undefined|string)[]]'),
232
+ structRest: () => eq(
233
+ rest({ a: number }, string),
234
+ '{readonly"a":number}&{readonly[k in string]?:number|string}',
235
+ ),
236
+ // `unknown` already admits `undefined`, so the open tail is unchanged
237
+ openIsAnUnconstrainedRest: () => {
238
+ assertEq(toTs(rest([number], unknown)), toTs(open([number])))
239
+ assertEq(toTs(rest({ a: number }, unknown)), toTs(open({ a: number })))
240
+ },
241
+ // an empty rest is the bare form, recognized here through the data
242
+ // form — including the `[or()]` spelling, which `RestTs` cannot see
243
+ // and so renders with a (wider, still sound) tail
244
+ emptyRestIsTheBareForm: () => {
245
+ assertEq(toTs(rest([number], never)), toTs([number]))
246
+ assertEq(toTs(rest([number], [never])), toTs([number]))
247
+ },
248
+ mut: () => eqMut(rest([number], string), '[number,...(undefined|string)[]]'),
249
+ },
131
250
  never: () => eq(never, 'never'),
251
+ // an array with no admissible element is the empty array, and nothing
252
+ // past a prefix is what prints as an exact-length tuple
253
+ arrayOfNever: () => eq(array(never), 'readonly[]'),
132
254
  // union members follow the canonical kind order, `undefined` first
133
255
  option: () => eq(option(number), 'undefined|number'),
134
256
  normalization: {
@@ -198,9 +320,20 @@ export const proof = {
198
320
  },
199
321
  },
200
322
  data: {
323
+ // The tail admits `undefined` on top of the rest — a hole past the
324
+ // prefix is no member, so a reader skips it and the index reads
325
+ // `undefined`. A rest that already admits it is printed as it is.
201
326
  tupleWithRest: () => {
202
327
  eqData([{}, { array: [{ prefix: [{ number: true }], rest: { string: true } }] }],
203
- [[], 'readonly[number,...readonly(string)[]]'])
328
+ [[], 'readonly[number,...readonly(undefined|string)[]]'])
329
+ eqData([{}, { array: [{ prefix: [{ number: true }], rest: { unit: unitBit(undefined), string: true } }] }],
330
+ [[], 'readonly[number,...readonly(undefined|string)[]]'])
331
+ },
332
+ // an exact-length pattern is the one that prints without a rest
333
+ // element — what a bare, closed tuple converts to
334
+ exactLengthTuple: () => {
335
+ eqData([{}, { array: [{ prefix: [{ number: true }, { string: true }] }] }],
336
+ [[], 'readonly[number,string]'])
204
337
  },
205
338
  structWithRest: () => {
206
339
  // the index signature must cover the declared keys too, so the
@@ -226,7 +359,7 @@ export const proof = {
226
359
  array: () => eqMut(array(number), '(number)[]'),
227
360
  nestedArray: () => eqMut(array(array(boolean)), '((boolean)[])[]'),
228
361
  record: () => eqMut(record(string), '{[k in string]?:string}'),
229
- tuple: () => eqMut([12, true], '[12,true]'),
362
+ tuple: () => eqMut(open([12, true]), '[12,true,...(unknown)[]]'),
230
363
  struct: () => eqMut({ a: number, b: string }, '{"a":number,"b":string}'),
231
364
  },
232
365
  throw: {
@@ -9,7 +9,8 @@
9
9
  *
10
10
  * @module
11
11
  */
12
- import type { Tag0, Tag1, Struct, Tuple, Type } from '../types.ts';
12
+ import type { And, Equal } from '../../ts/types.ts';
13
+ import type { Tag0, Tag1, Or, Struct, Tuple, Type, ConstObject } from '../types.ts';
13
14
  import type { phantomKey } from '../../phantom/types.ts';
14
15
  import type { StringMap } from '../../object/types.ts';
15
16
  /**
@@ -52,28 +53,132 @@ export type RecordTs<T extends Type> = {
52
53
  readonly [K in string]?: Ts<T>;
53
54
  };
54
55
  /**
55
- * Maps a tuple schema to a readonly tuple of resolved types.
56
- *
57
- * **The commented-out line is the accurate mapping.** A tuple schema is *open*
58
- * a longer array is a member of the set it describes (see "Structs and
59
- * tuples are open" in `../README.md`) — and the open form below says so. It is
60
- * commented out because TypeScript could not handle it, so this renders the
61
- * closed approximation instead.
62
- *
63
- * That is a limitation of this renderer, **not** a statement about the value
64
- * model. Do not cite the exact mapping as evidence that tuples are closed and
65
- * add a length check to `../parse/module.f.mjs`; that inference is what
66
- * produced #1622.
56
+ * Maps a tuple schema to a readonly tuple of resolved types, with the
57
+ * **trailing** positions whose sets admit `undefined` rendered optional:
58
+ * `[number, bigint, option(boolean), option(string)]` becomes
59
+ * `readonly[number, bigint, (boolean|undefined)?, (string|undefined)?]`.
60
+ *
61
+ * That is the same rule {@link StructTs} applies per key a member is
62
+ * required exactly when its set excludes `undefined` — so an array may stop
63
+ * at the last required position, which is what `../parse/module.f.mjs` and
64
+ * `../validate/module.f.mjs` accept. Only the trailing run: TypeScript
65
+ * forbids a required element after an optional one, so a position that admits
66
+ * `undefined` with a required one after it stays required with `undefined` in
67
+ * its type (see {@link _tupleInteriorOption}).
68
+ *
69
+ * **Deriving this generically took three specific moves**, each defeating an
70
+ * error that sank the obvious spellings — do not simplify it back:
71
+ *
72
+ * - `MappedTs` resolves `Ts<>` **once per position**, and the split then walks
73
+ * the mapped tuple rather than the schema. Testing `undefined extends
74
+ * Ts<Last>` during the walk evaluates `Ts<>` twice per position and raises
75
+ * TS2589 (excessively deep).
76
+ * - `Extract<…, readonly unknown[]>` is what makes a mapped type spreadable.
77
+ * Spreading it directly raises TS2574 ("a rest element type must be an array
78
+ * type") — TypeScript cannot prove a mapped type over a generic `keyof T` is
79
+ * array-shaped. An `& readonly unknown[]` intersection silences that too,
80
+ * but leaves the intersection in the rendered type, so `Equal<>` against a
81
+ * hand-written tuple fails; `Extract` resolves clean.
82
+ * - `extends infer M extends …` forces each intermediate to resolve before it
83
+ * is used in rest position. Without it the concrete instantiation works and
84
+ * the generic one does not.
85
+ *
86
+ * A recursive form that prepends onto an all-optional tail (rather than
87
+ * splitting) crashes the compiler outright — a Go stack overflow in tsgo
88
+ * 7.0.2, not a diagnostic.
89
+ *
90
+ * **The exact length is the model, not an approximation of it.** A bare tuple
91
+ * schema is *closed* — the positions it declares and no others (see "Structs
92
+ * and tuples are closed" in `../README.md`) — which is what a TypeScript tuple
93
+ * already means, so this rendering and the schema denote the same set and
94
+ * `validate`'s success cast is sound. A schema admitting more says so, and
95
+ * {@link RestTs} renders the tail it says it with.
67
96
  */
68
- export type TupleTs<T extends Tuple> = {
97
+ type MappedTs<T extends Tuple> = Extract<{
69
98
  readonly [K in keyof T]: Ts<T[K]>;
70
- };
99
+ }, readonly unknown[]>;
100
+ type RequiredPart<M extends readonly unknown[]> = M extends readonly [...infer I extends readonly unknown[], infer L] ? undefined extends L ? RequiredPart<I> : M : M;
101
+ type OmittablePart<M extends readonly unknown[], Acc extends readonly unknown[] = readonly []> = M extends readonly [...infer I extends readonly unknown[], infer L] ? undefined extends L ? OmittablePart<I, readonly [L, ...Acc]> : Acc : Acc;
102
+ type AsOptional<O extends readonly unknown[]> = Extract<{
103
+ readonly [K in keyof O]+?: O[K];
104
+ }, readonly unknown[]>;
105
+ export type TupleTs<T extends Tuple> = MappedTs<T> extends infer M extends readonly unknown[] ? SplitTs<M> : never;
106
+ /**
107
+ * Splits one mapped tuple. `M` is naked in the first conditional on purpose:
108
+ * that distributes over a union of tuples, so each member is split and rebuilt
109
+ * whole. Splitting the union instead lets `RequiredPart` and `OmittablePart`
110
+ * distribute separately, and the spread then recombines every prefix with
111
+ * every suffix — a union of `[number, option(string)]` and
112
+ * `[string, option(boolean), option(number)]` would admit `[number, boolean]`.
113
+ *
114
+ * Splitting a trailing run off also needs a *fixed* length. A schema array of
115
+ * non-fixed length (what `.map()` produces) and a variadic tuple
116
+ * (`[...(typeof number)[], option(string)]`) both have `length: number` and no
117
+ * last position to peel, so they keep the mapping as it is — splitting them
118
+ * would drop the element type and the prefix's shape respectively, and widen
119
+ * what `Ts<T>` admits.
120
+ */
121
+ type SplitTs<M extends readonly unknown[]> = M extends readonly unknown[] ? number extends M['length'] ? M : RequiredPart<M> extends infer R extends readonly unknown[] ? OmittablePart<M> extends infer O extends readonly unknown[] ? readonly [...R, ...AsOptional<O>] : never : never : never;
71
122
  type OptionalFields<T extends Struct> = {
72
123
  readonly [K in keyof T as undefined extends Ts<T[K]> ? K : never]?: Ts<T[K]>;
73
124
  };
74
125
  type RequiredFields<T extends Struct> = {
75
126
  readonly [K in keyof T as undefined extends Ts<T[K]> ? never : K]: Ts<T[K]>;
76
127
  };
128
+ /**
129
+ * Maps a container with a stated rest to the resolved type of its declared
130
+ * members, **plus the tail the rest states**.
131
+ *
132
+ * The struct kind needs no tail: an object type is width-open in TypeScript,
133
+ * so `ConstTs<C>` already admits the undeclared keys — as wide as TypeScript
134
+ * can render either way.
135
+ *
136
+ * The tuple kind does, and a rename of the old exact-only rendering would have
137
+ * relocated the unsound cast rather than removing it: `Ts<typeof open([42])>`
138
+ * would be the exact `readonly[42]` while `validate(open([42]))([42, 'x'])`
139
+ * accepts and hands back two elements.
140
+ *
141
+ * **The tail admits `undefined`.** Both readers check an undeclared member as
142
+ * a member, and a hole past the prefix is no member, so
143
+ * `validate(rest([42], string))([42, , ])` is `ok` and index 1 reads
144
+ * `undefined`. `...(Ts<R> | undefined)[]` is what "a rest never sees an absent
145
+ * member" says on the type side. The common case pays nothing: `open(c)`'s
146
+ * rest is `unknown`, which already admits `undefined`.
147
+ *
148
+ * **An empty rest renders no tail**, since `rest(c, or())` is the bare `c` —
149
+ * one set, so one rendering, and `readonly[42, ...undefined[]]` would admit
150
+ * the `[42, undefined]` both readers reject. What counts as empty is
151
+ * `emptyRest`'s question in `../data/module.f.mjs`, and that is a `toData`
152
+ * conversion plus `subset` both ways, which `types.ts` cannot invoke. Nor is
153
+ * `Ts<R> extends never` a substitute — `Ts<readonly[Or<readonly[]>]>` is
154
+ * `readonly[never]`, whose `length` is `1`. So this recognizes the one
155
+ * directly spellable empty rest, `or()`, and **keeps the tail whenever it
156
+ * cannot tell**. The conservatism has a direction: a kept tail is wider than
157
+ * the schema but sound — every accepted value still has the rendered type,
158
+ * which is the only direction a success cast needs — while a wrongly dropped
159
+ * one is the unsound cast. Where that leaves this and the runtime printer
160
+ * disagreeing (`rest([42], readonly[Or<readonly[]>])`, which the data form
161
+ * recognizes as empty and this does not) the printer is the narrower of the
162
+ * two; see {@link _restEmptyIndirect}.
163
+ *
164
+ * The other place the two differ is a rest with **no prefix**, which is the
165
+ * uniform array: `rest([], string)` and `array(string)` are one set, so the
166
+ * printer — which goes through the data form and sees one node — prints
167
+ * `ArrayTs`'s `readonly(string)[]` for both, while this renders the tail and
168
+ * so answers `readonly(string|undefined)[]` for the first. The tail is the
169
+ * sound one of the two, a hole being no member on either spelling; `ArrayTs`
170
+ * carries that gap already and closing it is its own change, not this one's.
171
+ * See {@link _restNoPrefix}.
172
+ */
173
+ export type RestTs<C extends ConstObject, R extends Type> = C extends Tuple ? TupleRestTs<C, R> : ConstTs<C>;
174
+ /**
175
+ * `[R] extends [...]` rather than a naked `R`: a naked one distributes, and a
176
+ * union rest would then render a union of tuples rather than a tuple whose
177
+ * tail is a union.
178
+ */
179
+ type TupleRestTs<C extends Tuple, R extends Type> = [
180
+ R
181
+ ] extends [Or<readonly []>] ? TupleTs<C> : TupleTs<C> extends infer M extends readonly unknown[] ? readonly [...M, ...ReadonlyArray<Ts<R> | undefined>] : never;
77
182
  /** Maps a struct schema to a readonly object of resolved types, with optional fields for schemas that include `undefined`. */
78
183
  export type StructTs<T extends Struct> = (keyof OptionalFields<T> extends never ? unknown : OptionalFields<T>) & (keyof RequiredFields<T> extends never ? unknown : RequiredFields<T>);
79
184
  /**
@@ -96,6 +201,33 @@ export type StructTs<T extends Struct> = (keyof OptionalFields<T> extends never
96
201
  * // Ts<typeof my> → MyType
97
202
  * ```
98
203
  *
204
+ * `MyType` here is an unchecked annotation — nothing derives it, so a typo
205
+ * (the wrong type, or one that has drifted from `myConst`) is trusted
206
+ * silently. Pin it down with two asserts, one against the un-annotated
207
+ * `myThunk` (forces the real structural walk) and one against the
208
+ * phantom-wrapped `my` (catches the two drifting apart):
209
+ *
210
+ * ```ts
211
+ * type _Check0 = Assert<Check<MyType, typeof myThunk>>
212
+ * type _Check1 = Assert<Check<MyType, typeof my>>
213
+ * ```
214
+ *
215
+ * {@link Check3} is the same pair written once:
216
+ *
217
+ * ```ts
218
+ * type _Check = Assert<Check3<MyType, typeof myThunk, typeof my>>
219
+ * ```
220
+ *
221
+ * See `fjs/edag/module.f.mjs` (`_exp`/`exp`) for this in practice. Note also
222
+ * that the phantom branch below does `Exclude<O, undefined>`, so a `MyType`
223
+ * that includes bare `undefined` at its top level will never satisfy
224
+ * `_Check1`. That is a constraint on the wrapped type, not on how wide it
225
+ * is: a union is fine when no member contributes a top-level `undefined` —
226
+ * `fjs/edag` wraps `Exp`, the whole node union, because its `undefined` is
227
+ * the tagged `['undefined']` rather than the bare value. Wrap one type per
228
+ * recursive cycle, and if the natural one does carry a top-level
229
+ * `undefined`, wrap a narrower node type inside the cycle instead.
230
+ *
99
231
  * @example
100
232
  * ```ts
101
233
  * type A = Ts<typeof string> // string
@@ -108,5 +240,21 @@ export type Ts<T extends Type> = unknown extends T ? Unknown : T extends {
108
240
  readonly [phantomKey]?: infer O;
109
241
  } ? Exclude<O, undefined> : T extends () => infer I ? (I extends readonly ['const', infer C] ? ConstTs<C> : I extends readonly ['boolean'] ? boolean : I extends readonly ['number'] ? number : I extends readonly ['string'] ? string : I extends readonly ['bigint'] ? bigint : I extends readonly ['unknown'] ? Unknown : I extends readonly ['array', infer E extends Type] ? readonly Ts<E>[] : I extends readonly ['record', infer E extends Type] ? {
110
242
  readonly [k in string]?: Ts<E>;
111
- } : I extends readonly ['or', ...infer A extends readonly Type[]] ? Ts<A[number]> : never) : ConstTs<T>;
243
+ } : I extends readonly ['or', ...infer A extends readonly Type[]] ? Ts<A[number]> : I extends readonly ['rest', infer C extends ConstObject, infer R extends Type] ? RestTs<C, R> : never) : ConstTs<T>;
244
+ /**
245
+ * Pins a hand-written TypeScript type `A` against the type an rtti schema `B`
246
+ * actually derives to — `Assert<Check<A, B>>` reads as "`A` is `Ts<B>`".
247
+ * This is the assertion the {@link Ts} doc above uses for the two-Phantom-check
248
+ * pattern, and the one every hand-written recursive type in this codebase (a
249
+ * `Tree`, a `LockMap`, …) pins against its rtti schema with.
250
+ */
251
+ export type Check<A, B extends Type> = Equal<A, Ts<B>>;
252
+ /**
253
+ * The two-assert `Phantom` pattern from the {@link Ts} doc, in one
254
+ * assert: `T` is both `Ts<R0>` (the raw thunk, forcing the structural walk)
255
+ * and `Ts<R1>` (the phantom-wrapped export). Checking only the wrapped
256
+ * export is a tautology — `Ts<>` short-circuits to the annotation — so the
257
+ * `R0` half is what gives this teeth.
258
+ */
259
+ export type Check3<T, R0 extends Type, R1 extends Type> = And<Equal<T, Ts<R0>>, Equal<T, Ts<R1>>>;
112
260
  export {};
@@ -11,7 +11,7 @@
11
11
  * ```
12
12
  * Type = Const | Thunk
13
13
  * Thunk = () => Info
14
- * Info = ['const', Const] | Info0<Tag0> | Info1<Tag1, Type>
14
+ * Info = ['const', Const] | Info0<Tag0> | Info1<Tag1, Type> | InfoRest<ConstObject, Type>
15
15
  * ```
16
16
  *
17
17
  * ## Nullary schemas (no type parameter)
@@ -32,6 +32,13 @@
32
32
  * Inside a recursive `Thunk`-based definition, wrap consts with `() => ['const', c]`
33
33
  * to keep the schema uniform.
34
34
  *
35
+ * ## Undeclared members
36
+ *
37
+ * A `Struct` or a `Tuple` on its own is **closed** — it admits the members it
38
+ * declares and no others. `rest(c, r)` states the set every undeclared member
39
+ * belongs to, and `open(c)` is `rest(c, unknown)`: any undeclared member at
40
+ * all. See "Structs and tuples are closed" in `./README.md`.
41
+ *
35
42
  * ## Converting to TypeScript types
36
43
  *
37
44
  * See `./ts/module.f.ts` for `Ts<T>` and the `*Ts` transformer types.
@@ -55,7 +62,7 @@ export type Tag0 = typeof tag0List[number];
55
62
  /** Info tuple for a nullary tag: `readonly[tag]`. */
56
63
  export type Info0<T extends Tag0> = T extends Tag0 ? readonly [T] : never;
57
64
  /** Any schema: a `Const` used directly, or a `Thunk` for tag-based/recursive schemas. */
58
- export type Type = (() => (readonly ['const', Const] | readonly ['bigint'] | readonly ['boolean'] | readonly ['number'] | readonly ['string'] | readonly ['unknown'] | readonly ['array', Type] | readonly ['record', Type] | readonly ['or', ...readonly Type[]])) | Const;
65
+ export type Type = (() => (readonly ['const', Const] | readonly ['bigint'] | readonly ['boolean'] | readonly ['number'] | readonly ['string'] | readonly ['unknown'] | readonly ['array', Type] | readonly ['record', Type] | readonly ['or', ...readonly Type[]] | readonly ['rest', ConstObject, Type])) | Const;
59
66
  /** The type of a nullary thunk for `Tag0`. */
60
67
  export type _Type0<T extends Tag0> = () => Info0<T>;
61
68
  /** Schema type for `boolean`. */
@@ -74,10 +81,31 @@ export type Tag1 = 'array' | 'record';
74
81
  export type Info1<K extends Tag1, T extends Type> = K extends Tag1 ? readonly [K, T] : never;
75
82
  /** The type of a unary thunk for `Tag1` with inner type `T`. */
76
83
  export type Type1<K extends Tag1, T extends Type> = () => Info1<K, T>;
77
- export type _MakeType1<K extends Tag1> = <T extends Type>(t: T) => Type1<K, T>;
84
+ export type _MakeType1<K extends Tag1> = <const T extends Type>(t: T) => Type1<K, T>;
78
85
  /** Schema type for a readonly array with element type `T`. */
79
86
  export type Array<T extends Type> = Type1<'array', T>;
80
87
  /** Schema type for a record (index signature) with value type `T`. */
81
88
  export type Record<T extends Type> = Type1<'record', T>;
82
89
  /** Schema type for a union of types `T`. */
83
90
  export type Or<T extends readonly Type[]> = () => readonly ['or', ...T];
91
+ /**
92
+ * Info tuple for a container with a stated rest: `readonly[tag, container,
93
+ * rest]`, where `rest` is the set every member the container does not declare
94
+ * belongs to. A bare `C` is the same set with a rest of `never`, and needs no
95
+ * spelling of its own.
96
+ */
97
+ export type InfoRest<C extends ConstObject, R extends Type> = readonly ['rest', C, R];
98
+ /**
99
+ * Schema type for a container `C` whose undeclared members are `R`. Both
100
+ * parameters are required: `never` — the bare `C` — is the identity a schema
101
+ * writes by leaving the wrapper off, so there is no default to state.
102
+ */
103
+ export type Rest<C extends ConstObject, R extends Type> = () => InfoRest<C, R>;
104
+ export type _MakeRest = <const C extends ConstObject, const R extends Type>(c: C, r: R) => Rest<C, R>;
105
+ /**
106
+ * `open`'s signature. It is not `_MakeRest` partially applied: the `const`
107
+ * modifier is what keeps `open([42])` a literal tuple rather than widening it
108
+ * to `Type[]`, so the modifier has to be restated here. `../proof.f.mjs` pins
109
+ * that, as it does for every other `const`-taking constructor.
110
+ */
111
+ export type _MakeOpen = <const C extends ConstObject>(c: C) => Rest<C, Unknown>;
@@ -26,21 +26,33 @@
26
26
  * it to convention. Which reader a caller wants, and why both exist, is in
27
27
  * "The two schema-form readers" in `../README.md`.
28
28
  *
29
- * ## Structs and tuples are open
30
- *
31
- * Openness is the shared rule, not a `parse` detail — see "Structs and tuples
32
- * are open" in `../README.md`. `validate` iterates the *schema's* entries, so
33
- * an undeclared key or a longer array is never visited: it is accepted, and it
34
- * is still there afterwards because the value is returned as-is. An absent
35
- * member reads as `undefined`, so a member is required exactly when its set
36
- * excludes `undefined`.
37
- *
38
- * **Do not add a length check for tuples here.** `Ts<readonly [42]>` is the
39
- * exact tuple only because TypeScript cannot express the open one (see
40
- * `../ts/types.ts` `TupleTs`); reading that rendering as the value model is
41
- * what produced #1622, whose check lived in this module's ancestor and was
42
- * deleted with it. A schema that wants exact members says so — see
43
- * `../todo/close-type.md`.
29
+ * ## Structs and tuples are closed
30
+ *
31
+ * Closedness is the shared rule, not a `parse` detail — see "Structs and
32
+ * tuples are closed" in `../README.md`. A bare `Struct` or `Tuple` admits the
33
+ * members it declares and no others, so an undeclared key or an index past the
34
+ * prefix rejects the value. A tuple answers by **length** as well as by
35
+ * member, because a hole past the prefix is no member and would slip through
36
+ * the member check alone.
37
+ *
38
+ * Closedness is about *undeclared* members and leaves the required/optional
39
+ * rule alone: an absent member reads as `undefined`, so a member is required
40
+ * exactly when its set excludes `undefined`, and a schema whose trailing
41
+ * position admits it still accepts a shorter array. A tuple schema declares by
42
+ * length, so a hole in the *schema* is a position whose schema is `undefined`
43
+ * — see "A hole is a declared position" in `../README.md`.
44
+ *
45
+ * The length check is the model rather than an inference from `Ts<>`. #1622
46
+ * added one by reading `Ts<readonly[42]>`'s exact tuple as the value model
47
+ * while the model said open, and it was reverted for that reason; what has
48
+ * changed since is the model, not the reading.
49
+ *
50
+ * ## Stated rests
51
+ *
52
+ * `rest(c, r)` admits the declared members plus any number of members
53
+ * belonging to `r`, and `open(c)` — `rest(c, unknown)` — admits anything else
54
+ * besides. This widens what is accepted and changes nothing else: a success
55
+ * still carries the very value it was given, undeclared members included.
44
56
  *
45
57
  * ## Dispatch strategy
46
58
  *
@@ -54,9 +66,9 @@
54
66
  *
55
67
  * ## Recursion safety
56
68
  *
57
- * For `array` and `record` schemas, the inner item validator is instantiated
58
- * lazily — only after confirming the container is non-empty. This prevents
59
- * infinite recursion when validating recursive schemas like
69
+ * The inner validator of an `array`, a `record` or a `rest` is instantiated
70
+ * lazily — only after confirming there is a member for it to read. This
71
+ * prevents infinite recursion when validating recursive schemas like
60
72
  * `const list = () => ['array', list]`.
61
73
  *
62
74
  * See `./types.ts` for the `Path`/`Result`/`Validate`/`ValidationError`
@@ -65,8 +77,9 @@
65
77
  * @module
66
78
  *
67
79
  * @import { Unknown } from '../ts/types.ts'
68
- * @import { Info1, Struct, Tag1, Tuple, Type } from '../types.ts'
69
- * @import { Container, IsContainer, Validate, Visitor } from '../common/types.ts'
80
+ * @import { ConstObject, Info1, Tag1, Type } from '../types.ts'
81
+ * @import { Container, Fits, IsContainer, SchemaEntries, Validate, ValidateE, Visitor } from '../common/types.ts'
82
+ * @import { StringMap } from '../../object/types.ts'
70
83
  */
71
84
  import type { Type } from '../types.ts';
72
85
  import type { Validate } from '../common/types.ts';
@@ -90,14 +103,18 @@ import type { Validate } from '../common/types.ts';
90
103
  * v(input) // ['ok', input] — the same array, not a copy
91
104
  * v([1, 'two']) // ['error', { path: ['1'], message: 'unexpected value' }]
92
105
  *
93
- * // open, and the extras are still there afterwards
94
- * validate([number, number])([1, 2, 3]) // ['ok', [1, 2, 3]]
95
- * validate({ a: number })({ a: 1, b: 2 }) // ['ok', { a: 1, b: 2 }]
106
+ * // closed, so a member the schema does not name rejects the value
107
+ * validate([number, number])([1, 2, 3]) // ['error', ]
108
+ * validate({ a: number })({ a: 1, b: 2 }) // ['error', ]
96
109
  *
97
110
  * // an absent optional member stays absent
98
111
  * validate({ a: number, b: option(string) })({ a: 1 }) // ['ok', { a: 1 }]
112
+ *
113
+ * // a stated rest says what the undeclared members may be; `open` says anything
114
+ * validate(rest({ a: number }, number))({ a: 1, b: 2 }) // ['ok', { a: 1, b: 2 }]
115
+ * validate(open({ a: number }))({ a: 1, b: 'x' }) // ['ok', { a: 1, b: 'x' }]
99
116
  * ```
100
117
  *
101
- * @type {<T extends Type>(rtti: T) => Validate<T>}
118
+ * @type {<const T extends Type>(rtti: T) => Validate<T>}
102
119
  */
103
- export declare const validate: <T extends Type>(rtti: T) => Validate<T>;
120
+ export declare const validate: <const T extends Type>(rtti: T) => Validate<T>;