functionalscript 0.41.0 → 0.42.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 (46) hide show
  1. package/fjs/ci/config/module.f.d.ts +3 -3
  2. package/fjs/ci/config/module.f.js +3 -3
  3. package/fjs/ci/deno/module.f.d.ts +8 -0
  4. package/fjs/ci/deno/module.f.js +9 -1
  5. package/fjs/ci/deno/proof.f.d.ts +5 -0
  6. package/fjs/ci/deno/proof.f.js +26 -0
  7. package/fjs/ci/proof.f.js +3 -2
  8. package/fjs/dev/module.f.d.ts +15 -4
  9. package/fjs/dev/module.f.js +38 -7
  10. package/fjs/dev/package_json/module.f.js +7 -4
  11. package/fjs/dev/proof.f.js +10 -1
  12. package/fjs/djs/parser/module.f.js +13 -3
  13. package/fjs/djs/parser/proof.f.d.ts +1 -0
  14. package/fjs/djs/parser/proof.f.js +26 -0
  15. package/fjs/djs/tokenizer/proof.f.js +11 -4
  16. package/fjs/emergent_testing/proof.f.js +20 -3
  17. package/fjs/js/tokenizer/proof.f.js +7 -0
  18. package/fjs/mcp/cas/module.f.js +4 -1
  19. package/fjs/mcp/evo/proof.f.js +6 -1
  20. package/fjs/mcp/proof.f.js +37 -18
  21. package/fjs/media/json/module.f.d.ts +15 -1
  22. package/fjs/media/json/module.f.js +17 -1
  23. package/fjs/media/json/parser/module.f.js +13 -3
  24. package/fjs/media/json/parser/proof.f.d.ts +1 -0
  25. package/fjs/media/json/parser/proof.f.js +37 -1
  26. package/fjs/media/json/proof.f.d.ts +4 -0
  27. package/fjs/media/json/proof.f.js +14 -2
  28. package/fjs/media/json/serializer/module.f.d.ts +4 -0
  29. package/fjs/media/json/serializer/module.f.js +39 -2
  30. package/fjs/media/json/serializer/proof.f.js +32 -8
  31. package/fjs/media/module.f.d.ts +92 -10
  32. package/fjs/media/module.f.js +73 -8
  33. package/fjs/media/proof.f.d.ts +10 -0
  34. package/fjs/media/proof.f.js +79 -10
  35. package/fjs/media/revision/module.f.d.ts +9 -1
  36. package/fjs/media/revision/module.f.js +14 -5
  37. package/fjs/protocol/mcp/stdio/module.f.js +2 -5
  38. package/fjs/text/utf16/module.f.d.ts +24 -0
  39. package/fjs/text/utf16/module.f.js +24 -0
  40. package/fjs/text/utf16/proof.f.d.ts +1 -0
  41. package/fjs/text/utf16/proof.f.js +10 -1
  42. package/fjs/types/rtti/validate/proof.f.d.ts +2 -0
  43. package/fjs/types/rtti/validate/proof.f.js +58 -0
  44. package/nanvm-lib/tests/proof.f.d.ts +9 -0
  45. package/nanvm-lib/tests/proof.f.js +58 -7
  46. package/package.json +2 -2
@@ -1,13 +1,72 @@
1
1
  import { fromVec } from '../text/utf8/module.f.js';
2
2
  import { detectVec } from './type/module.f.js';
3
- import { decodeText as decodeRevisionText, mediaType as revisionMediaType } from './revision/module.f.js';
4
- import { assertNotNullish } from '../asserts/module.f.js';
3
+ import { parse } from './json/module.f.js';
4
+ import { assert, assertNotNullish } from '../asserts/module.f.js';
5
+ import { validate } from '../types/rtti/validate/module.f.js';
6
+ /** The default refinement: structural validation alone decides the match. */
7
+ const always = () => true;
8
+ /** Structural validation followed by the dialect's own refinement. */
9
+ const matchWith = (v) => (extraValidate) => (u) => {
10
+ const [tag, value] = v(u);
11
+ return tag === 'ok' && extraValidate(value);
12
+ };
13
+ /**
14
+ * Registers a dialect for detection: its rtti schema, plus whatever rtti can't
15
+ * say about it.
16
+ *
17
+ * The dialect name is read out of the schema's own `dialect` member — that
18
+ * member is a string const in a dialect schema, which is what makes the schema
19
+ * self-discriminating, so a separate name alongside it would be a second copy
20
+ * that can disagree with the first. It follows that no registration can claim
21
+ * one dialect while validating another's blobs. The media type is likewise
22
+ * derived rather than supplied: `application/${dialect}+json`, the mechanical
23
+ * derivation `fjs/media/revision/README.md` documents.
24
+ *
25
+ * `extraValidate` runs *after* structural validation, on the dialect's own
26
+ * decoded type, and closes the gap rtti leaves — "this string is
27
+ * cbase32-decodable", "this number is a non-negative safe integer". It returns
28
+ * `boolean`, so it has no error channel and nothing to report but yes or no,
29
+ * and it never sees the raw bytes: parsing and the schema walk stay with the
30
+ * detector. Omitting it gets classification (what a blob claims to be and
31
+ * structurally looks like); supplying it gets detection as strict as the
32
+ * dialect's own decoder.
33
+ *
34
+ * **The name is neither grammar-checked nor allowlisted.** A schema saying
35
+ * `dialect: 'foo'` yields `application/foo+json`, and that is intended:
36
+ * `vnd.fjs.*` is this repo's convention for its *own* formats, not a constraint
37
+ * on what a caller may detect — another vendor's `vnd.*` blob, or a widely used
38
+ * name not under `vnd.` at all, is a legitimate thing to register. The name is
39
+ * not attacker-controlled either: it comes from a schema a programmer wrote and
40
+ * passed here, never from the blob being classified, so no untrusted string
41
+ * reaches `mime_type` along this path. (A detector that read the name *out of*
42
+ * a blob would need the RFC 6838 grammar check and an allowlist; this one does
43
+ * not.) Registering a name that is not a valid RFC 6838 restricted-name yields
44
+ * a malformed media type in the registrant's own results, and nowhere else.
45
+ *
46
+ * The constraint is `Struct` — every member must be a real rtti `Type`, which
47
+ * is what rejects a member like `() => 42` at compile time (rtti would read it
48
+ * as a thunk and `match` would *throw* on the first blob rather than return
49
+ * `false`). TypeScript cannot also require a direct string `dialect` member
50
+ * under that constraint, so that half is asserted here instead: loudly, once,
51
+ * when the entry is constructed. A thunk-form `dialect`
52
+ * (`() => ['const', 'x']`) is a perfectly valid rtti schema, it just is not
53
+ * registerable — write the string directly, as `revisionSchema` does.
54
+ */
55
+ export const dialectEntry = (type, extraValidate = always) => {
56
+ const { dialect } = type;
57
+ assert(typeof dialect === 'string', 'dialectEntry: schema has no direct string `dialect` member');
58
+ return { dialect, match: matchWith(validate(type))(extraValidate) };
59
+ };
5
60
  /**
6
- * Classifies a whole buffered `Vec`, the same three-way `{ length, mime_type,
7
- * type }` shape as `fjs/media/type` `detectVec`, but with dialect-tagged JSON
8
- * recognized ahead of the plain `text/plain` fallback.
61
+ * Classifies a whole buffered `Vec` against `dialects`, returning the same
62
+ * three-way `{ length, mime_type, type }` shape as `fjs/media/type`
63
+ * `detectVec`, but with dialect-tagged JSON recognized ahead of the plain
64
+ * `text/plain` fallback. The first entry whose `match` accepts the parsed value
65
+ * wins; entries that overlap are the registrant's own business, since matching
66
+ * `dialect` as an exact literal already makes structural validation reject
67
+ * every other dialect's blob.
9
68
  */
10
- export const detect = (bytes) => {
69
+ export const detect = (dialects) => (bytes) => {
11
70
  const base = detectVec(bytes);
12
71
  // Only whole-blob-valid UTF-8 text can possibly be JSON; a magic-byte hit
13
72
  // or binary fallback is never a dialect match.
@@ -18,6 +77,12 @@ export const detect = (bytes) => {
18
77
  // whole-blob-valid UTF-8 — the same two conditions `fromVec` checks, via the
19
78
  // same decoder — so `fromVec` cannot return `null` here.
20
79
  const text = assertNotNullish(fromVec(bytes), 'detect: type text implies fromVec succeeds');
21
- const [tag] = decodeRevisionText(text);
22
- return tag === 'ok' ? { ...base, mime_type: revisionMediaType } : base;
80
+ const [tag, value] = parse(text);
81
+ if (tag === 'error') {
82
+ return base;
83
+ }
84
+ const matched = dialects.find(({ match }) => match(value));
85
+ return matched === undefined
86
+ ? base
87
+ : { ...base, mime_type: `application/${matched.dialect}+json` };
23
88
  };
@@ -1,9 +1,19 @@
1
+ import { type DialectEntry } from './module.f.ts';
1
2
  export declare const proof: {
2
3
  validRevision: () => void;
3
4
  keyOrderIndependent: () => void;
4
5
  invalidRevisionFallsThrough: () => void;
6
+ nonHashSnapshotFallsThrough: () => void;
7
+ secondDialect: () => void;
8
+ nonVndDialectName: () => void;
9
+ firstMatchWins: () => void;
10
+ noDialects: () => void;
5
11
  ordinaryJsonFallsThrough: () => void;
6
12
  plainTextFallsThrough: () => void;
7
13
  binaryFallsThrough: () => void;
8
14
  largeNonJsonWithinBound: () => void;
15
+ throw: {
16
+ thunkDialectMember: () => DialectEntry;
17
+ noDialectMember: () => DialectEntry;
18
+ };
9
19
  };
@@ -1,14 +1,37 @@
1
1
  import { assertEq } from '../asserts/module.f.js';
2
2
  import { msb, u8ListToVec, repeat, vec8 } from '../types/bit_vec/module.f.js';
3
- import { detect } from './module.f.js';
4
- import { dialect } from './revision/module.f.js';
3
+ import { detect, dialectEntry } from './module.f.js';
4
+ import { dialect, revisionDialect } from './revision/module.f.js';
5
+ import { number, string } from '../types/rtti/module.f.js';
5
6
  // All test strings here are ASCII, so char code === UTF-8 byte value.
6
7
  const utf8Bytes = (s) => u8ListToVec(msb)([...s].map(c => c.charCodeAt(0)));
7
8
  const revisionJson = `{"dialect":"${dialect}","subject":"8","parents":[],"snapshot":"8","generation":0}`;
9
+ const dialects = [revisionDialect];
10
+ const detectRevision = detect(dialects);
11
+ /**
12
+ * A second dialect following the same `vnd.fjs.<name>` convention, registered
13
+ * with no refinement: structure alone decides the match.
14
+ */
15
+ const noteSchema = {
16
+ dialect: 'vnd.fjs.note',
17
+ text: string,
18
+ };
19
+ const noteDialect = dialectEntry(noteSchema);
20
+ /** A dialect name outside `vnd.fjs.*` — registerable, and detected as itself. */
21
+ const gadgetSchema = {
22
+ dialect: 'application.gadget',
23
+ size: number,
24
+ };
25
+ const gadgetDialect = dialectEntry(gadgetSchema);
26
+ /**
27
+ * `DialectEntry` is deliberately not opaque, so overlapping entries — which a
28
+ * self-discriminating schema can't produce on its own — can be stated directly.
29
+ */
30
+ const anyEntry = (name) => ({ dialect: name, match: () => true });
8
31
  export const proof = {
9
32
  // A valid revision blob is recognized and reported under its derived media type.
10
33
  validRevision: () => {
11
- const m = detect(utf8Bytes(revisionJson));
34
+ const m = detectRevision(utf8Bytes(revisionJson));
12
35
  assertEq(m.type, 'text');
13
36
  assertEq(m.mime_type, 'application/vnd.fjs.revision+json');
14
37
  },
@@ -16,7 +39,7 @@ export const proof = {
16
39
  // not first must still be detected.
17
40
  keyOrderIndependent: () => {
18
41
  const text = `{"parents":[],"subject":"8","dialect":"${dialect}","snapshot":"8","generation":0}`;
19
- const m = detect(utf8Bytes(text));
42
+ const m = detectRevision(utf8Bytes(text));
20
43
  assertEq(m.mime_type, 'application/vnd.fjs.revision+json');
21
44
  },
22
45
  // A revision missing a required field (here `snapshot` and `generation`)
@@ -24,25 +47,63 @@ export const proof = {
24
47
  // classification.
25
48
  invalidRevisionFallsThrough: () => {
26
49
  const text = `{"dialect":"${dialect}","subject":"8","parents":["8","r"]}`;
27
- const m = detect(utf8Bytes(text));
50
+ const m = detectRevision(utf8Bytes(text));
51
+ assertEq(m.type, 'text');
52
+ assertEq(m.mime_type, 'text/plain');
53
+ },
54
+ // The `extraValidate` path: structurally a revision, but `snapshot` is not
55
+ // a cbase32 hash, so `revisionDialect`'s refinement rejects it and the
56
+ // verdict stays `text/plain` — exactly what `decodeText` would say.
57
+ nonHashSnapshotFallsThrough: () => {
58
+ const text = `{"dialect":"${dialect}","subject":"8","parents":[],"snapshot":"not a hash","generation":0}`;
59
+ const m = detectRevision(utf8Bytes(text));
60
+ assertEq(m.type, 'text');
61
+ assertEq(m.mime_type, 'text/plain');
62
+ },
63
+ // A second dialect, registered by the caller, is recognized alongside the
64
+ // first — and an entry with no refinement matches on structure alone.
65
+ secondDialect: () => {
66
+ const d = detect([revisionDialect, noteDialect]);
67
+ assertEq(d(utf8Bytes('{"dialect":"vnd.fjs.note","text":"hi"}')).mime_type, 'application/vnd.fjs.note+json');
68
+ assertEq(d(utf8Bytes(revisionJson)).mime_type, 'application/vnd.fjs.revision+json');
69
+ // Same tag, wrong shape: no entry matches.
70
+ assertEq(d(utf8Bytes('{"dialect":"vnd.fjs.note","text":42}')).mime_type, 'text/plain');
71
+ },
72
+ // The name is neither grammar-checked nor allowlisted: a dialect outside
73
+ // `vnd.fjs.*` yields its own derived media type.
74
+ nonVndDialectName: () => {
75
+ const m = detect([gadgetDialect])(utf8Bytes('{"dialect":"application.gadget","size":3}'));
76
+ assertEq(m.mime_type, 'application/application.gadget+json');
77
+ },
78
+ // Entries are tried in order and the first match wins.
79
+ firstMatchWins: () => {
80
+ const bytes = utf8Bytes('{}');
81
+ assertEq(detect([anyEntry('vnd.fjs.a'), anyEntry('vnd.fjs.b')])(bytes).mime_type, 'application/vnd.fjs.a+json');
82
+ assertEq(detect([anyEntry('vnd.fjs.b'), anyEntry('vnd.fjs.a')])(bytes).mime_type, 'application/vnd.fjs.b+json');
83
+ },
84
+ // An empty registry recognizes nothing; every blob keeps the
85
+ // `fjs/media/type` verdict.
86
+ noDialects: () => {
87
+ const m = detect([])(utf8Bytes(revisionJson));
28
88
  assertEq(m.type, 'text');
29
89
  assertEq(m.mime_type, 'text/plain');
30
90
  },
31
91
  // Ordinary JSON with no `dialect` field at all falls through unchanged.
32
92
  ordinaryJsonFallsThrough: () => {
33
- const m = detect(utf8Bytes('{"hello":"world"}'));
93
+ const m = detectRevision(utf8Bytes('{"hello":"world"}'));
34
94
  assertEq(m.type, 'text');
35
95
  assertEq(m.mime_type, 'text/plain');
36
96
  },
37
- // Plain (non-JSON) text is unaffected.
97
+ // Plain (non-JSON) text is unaffected: the parse fails and the
98
+ // `fjs/media/type` verdict stands.
38
99
  plainTextFallsThrough: () => {
39
- const m = detect(utf8Bytes('hello'));
100
+ const m = detectRevision(utf8Bytes('hello'));
40
101
  assertEq(m.type, 'text');
41
102
  assertEq(m.mime_type, 'text/plain');
42
103
  },
43
104
  // Binary content (magic-byte hit) is unaffected by dialect detection.
44
105
  binaryFallsThrough: () => {
45
- const m = detect(u8ListToVec(msb)([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
106
+ const m = detectRevision(u8ListToVec(msb)([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
46
107
  assertEq(m.type, 'base64');
47
108
  assertEq(m.mime_type, 'image/png');
48
109
  },
@@ -51,8 +112,16 @@ export const proof = {
51
112
  // non-JSON content within that bound.
52
113
  largeNonJsonWithinBound: () => {
53
114
  const a = repeat(1000n)(vec8(0x61n)); // 1,000 bytes of 'a'
54
- const m = detect(a);
115
+ const m = detectRevision(a);
55
116
  assertEq(m.type, 'text');
56
117
  assertEq(m.mime_type, 'text/plain');
57
118
  },
119
+ throw: {
120
+ // The `dialect` member must be a direct string const. A thunk-form one
121
+ // is a valid rtti schema but is not registerable, and `dialectEntry`
122
+ // says so at registration rather than per blob.
123
+ thunkDialectMember: () => dialectEntry({ dialect: string }),
124
+ // A schema with no `dialect` member at all is likewise refused.
125
+ noDialectMember: () => dialectEntry({ text: string }),
126
+ },
58
127
  };
@@ -1,7 +1,8 @@
1
1
  import { type ValidationError } from '../../types/rtti/validate/module.f.ts';
2
2
  import type { Ts } from '../../types/rtti/ts/module.f.ts';
3
- import type { Unknown } from '../json/module.f.ts';
3
+ import { type Unknown } from '../json/module.f.ts';
4
4
  import { type Result } from '../../types/result/module.f.ts';
5
+ import { type DialectEntry } from '../module.f.ts';
5
6
  /**
6
7
  * Format tag: names the dialect of this BLOB. The media type it is served
7
8
  * with is derived mechanically: `application/` + `dialect` + `+json`.
@@ -76,3 +77,10 @@ export declare const validate: (value: Unknown) => Result<Revision, RevisionErro
76
77
  * satisfies the schema is a revision, regardless of key order or whitespace.
77
78
  */
78
79
  export declare const decodeText: (text: string) => Result<Revision, RevisionError>;
80
+ /**
81
+ * This dialect as a registry entry for `fjs/media`'s `detect`. It carries the
82
+ * semantic checks too, so a blob is detected as `vnd.fjs.revision` exactly when
83
+ * {@link decodeText} would accept it — a structurally valid revision whose
84
+ * `snapshot` is not a cbase32 hash is not one.
85
+ */
86
+ export declare const revisionDialect: DialectEntry;
@@ -16,11 +16,10 @@
16
16
  */
17
17
  import { array, number, option, string } from '../../types/rtti/module.f.js';
18
18
  import { validate as rttiValidate } from '../../types/rtti/validate/module.f.js';
19
- import { parse as jsonParse } from '../json/parser/module.f.js';
20
- import { tokenize as jsonTokenize } from '../json/tokenizer/module.f.js';
21
- import { stringToList } from '../../text/utf16/module.f.js';
19
+ import { parse as parseJson } from '../json/module.f.js';
22
20
  import { cBase32ToVec } from '../../basen/cbase32/module.f.js';
23
21
  import { error, ok } from '../../types/result/module.f.js';
22
+ import { dialectEntry } from '../module.f.js';
24
23
  /**
25
24
  * Format tag: names the dialect of this BLOB. The media type it is served
26
25
  * with is derived mechanically: `application/` + `dialect` + `+json`.
@@ -106,8 +105,6 @@ export const validate = (value) => {
106
105
  }
107
106
  return checkReferences(v);
108
107
  };
109
- /** Parses `text` as JSON without relying on `JSON.parse` (the shared `fjs/media/json` parser/tokenizer). */
110
- const parseJson = (text) => jsonParse(jsonTokenize(stringToList(text)));
111
108
  /**
112
109
  * Decodes `text` as a `revision` BLOB: JSON-parses it, then validates it per
113
110
  * {@link validate}. Detection is semantic, not syntactic — any JSON that
@@ -120,3 +117,15 @@ export const decodeText = (text) => {
120
117
  }
121
118
  return validate(v);
122
119
  };
120
+ /** {@link checkReferences} as the `boolean` refinement a {@link DialectEntry} takes. */
121
+ const isValidRevision = (r) => {
122
+ const [tag] = checkReferences(r);
123
+ return tag === 'ok';
124
+ };
125
+ /**
126
+ * This dialect as a registry entry for `fjs/media`'s `detect`. It carries the
127
+ * semantic checks too, so a blob is detected as `vnd.fjs.revision` exactly when
128
+ * {@link decodeText} would accept it — a structurally valid revision whose
129
+ * `snapshot` is not a cbase32 hash is not one.
130
+ */
131
+ export const revisionDialect = dialectEntry(revisionSchema, isValidRevision);
@@ -32,10 +32,7 @@
32
32
  import { pure, step } from '../../../effects/module.f.js';
33
33
  import { readLine, write } from '../../../effects/node/module.f.js';
34
34
  import { tryUtf8 } from '../../../text/module.f.js';
35
- import { stringToList } from '../../../text/utf16/module.f.js';
36
- import { stringify } from '../../../media/json/module.f.js';
37
- import { tokenize } from '../../../media/json/tokenizer/module.f.js';
38
- import { parse } from '../../../media/json/parser/module.f.js';
35
+ import { parse, stringify } from '../../../media/json/module.f.js';
39
36
  import { sort } from '../../../types/object/module.f.js';
40
37
  import { internalError, jsonrpc, parseError } from '../../json_rpc/module.f.js';
41
38
  import { error, ok } from '../../../types/result/module.f.js';
@@ -61,7 +58,7 @@ export const stdioTransport = (handler) => step(readLine('stdin'), line => line
61
58
  ? pure(undefined)
62
59
  : handleLine(handler)(line));
63
60
  const handleLine = (handler) => (line) => {
64
- const [t, value] = parse(tokenize(stringToList(line)));
61
+ const [t, value] = parse(line);
65
62
  return step(t === 'error'
66
63
  ? writeResponse(parseErrorResponse)
67
64
  : step(handler(value), resp => resp === null
@@ -135,3 +135,27 @@ export declare const listToString: (input: List<U16>) => string;
135
135
  * ```
136
136
  */
137
137
  export declare const codePointListToString: (input: List<CodePoint>) => string;
138
+ /**
139
+ * Converts a single Unicode code point to a string — the scalar counterpart of
140
+ * {@link codePointListToString}, for callers that hold one code point rather
141
+ * than a list of them and would otherwise wrap it in a single-element list.
142
+ *
143
+ * Input outside the assignable range — a surrogate, a value above
144
+ * `0x10FFFF`, or a negative one — is truncated to its low 16 bits and encoded
145
+ * as that code unit, rather than rejected. This matches
146
+ * {@link codePointListToString}, which reports malformed input through the
147
+ * decoder's `errorMask` on the way *in* rather than through a failure on the
148
+ * way out.
149
+ *
150
+ * @param codePoint - A Unicode code point (`CodePoint`).
151
+ * @returns The one- or two-code-unit string encoding it.
152
+ *
153
+ * @example
154
+ *
155
+ * ```ts
156
+ * codePointToString(0x48) // 'H'
157
+ * codePointToString(0x1F600) // the 😀 surrogate pair
158
+ * codePointToString(0x110000) // '\u0000' — out of range, low 16 bits kept
159
+ * ```
160
+ */
161
+ export declare const codePointToString: (codePoint: CodePoint) => string;
@@ -285,3 +285,27 @@ export const listToString = fn(map(String.fromCharCode))
285
285
  * ```
286
286
  */
287
287
  export const codePointListToString = (input) => listToString(fromCodePointList(input));
288
+ /**
289
+ * Converts a single Unicode code point to a string — the scalar counterpart of
290
+ * {@link codePointListToString}, for callers that hold one code point rather
291
+ * than a list of them and would otherwise wrap it in a single-element list.
292
+ *
293
+ * Input outside the assignable range — a surrogate, a value above
294
+ * `0x10FFFF`, or a negative one — is truncated to its low 16 bits and encoded
295
+ * as that code unit, rather than rejected. This matches
296
+ * {@link codePointListToString}, which reports malformed input through the
297
+ * decoder's `errorMask` on the way *in* rather than through a failure on the
298
+ * way out.
299
+ *
300
+ * @param codePoint - A Unicode code point (`CodePoint`).
301
+ * @returns The one- or two-code-unit string encoding it.
302
+ *
303
+ * @example
304
+ *
305
+ * ```ts
306
+ * codePointToString(0x48) // 'H'
307
+ * codePointToString(0x1F600) // the 😀 surrogate pair
308
+ * codePointToString(0x110000) // '\u0000' — out of range, low 16 bits kept
309
+ * ```
310
+ */
311
+ export const codePointToString = (codePoint) => String.fromCharCode(...codePointToUtf16(codePoint));
@@ -6,4 +6,5 @@ export declare const proof: {
6
6
  listToString: (() => void)[];
7
7
  stringToCodePointList: (() => void)[];
8
8
  codePointListToString: (() => void)[];
9
+ codePointToString: (() => void)[];
9
10
  };
@@ -1,4 +1,4 @@
1
- import { toCodePointList, fromCodePointList, stringToList, listToString, stringToCodePointList, codePointListToString } from './module.f.js';
1
+ import { toCodePointList, fromCodePointList, stringToList, listToString, stringToCodePointList, codePointListToString, codePointToString } from './module.f.js';
2
2
  import { stringify as jsonStringify } from '../../media/json/module.f.js';
3
3
  import { sort } from '../../types/object/module.f.js';
4
4
  import { toArray } from '../../types/list/module.f.js';
@@ -131,5 +131,14 @@ export const proof = {
131
131
  const codePoints = [0x48, 0x65, 0x6C, 0x6C, 0x6F];
132
132
  const outputString = codePointListToString(codePoints);
133
133
  }
134
+ ],
135
+ codePointToString: [
136
+ () => { assertEq(codePointToString(0x48), 'H'); },
137
+ // supplementary plane: one code point, two code units
138
+ () => { assertEq(codePointToString(0x1f600), '😀'); },
139
+ // a surrogate is not a code point; it round-trips as its own code unit
140
+ () => { assertEq(codePointToString(0xd800), '\ud800'); },
141
+ // above the maximum code point, the low 16 bits are kept
142
+ () => { assertEq(codePointToString(0x110000), '\u0000'); },
134
143
  ]
135
144
  };
@@ -113,4 +113,6 @@ export declare const proof: {
113
113
  arrayOfArrays: () => void;
114
114
  recordOfRecords: () => void;
115
115
  };
116
+ funcParam: () => () => void;
117
+ funcObj: () => () => void;
116
118
  };
@@ -288,4 +288,62 @@ export const proof = {
288
288
  assertError(v({ a: 42 }));
289
289
  },
290
290
  },
291
+ funcParam: () => {
292
+ const paramSet0 = ['hello', bigint];
293
+ const paramSet1 = ['goodbye', string, 43];
294
+ const paramSet01 = or(paramSet0, paramSet1);
295
+ const v0 = validate(paramSet0);
296
+ const v1 = validate(paramSet1);
297
+ const func = (f0, f1) => (...args) => {
298
+ {
299
+ const [t, r] = v0(args);
300
+ if (t === 'ok') {
301
+ return f0(...r);
302
+ }
303
+ }
304
+ {
305
+ const [t, r] = v1(args);
306
+ if (t === 'ok') {
307
+ return f1(...r);
308
+ }
309
+ }
310
+ throw 'unreachable: args did not match any parameter set';
311
+ };
312
+ const f0 = (a, b) => 42;
313
+ const f1 = (a, b, c) => 13;
314
+ const x = func(f0, f1);
315
+ return () => {
316
+ assertEq(x('hello', 42n), 42);
317
+ assertEq(x('goodbye', 'world', 43), 13);
318
+ };
319
+ },
320
+ funcObj: () => {
321
+ const param0 = { a: 'hello', b: bigint };
322
+ const param1 = { a: 'goodbye', b: string, c: 43 };
323
+ const param01 = or(param0, param1);
324
+ const v0 = validate(param0);
325
+ const v1 = validate(param1);
326
+ const func = (f0, f1) => (args) => {
327
+ {
328
+ const [t, r] = v0(args);
329
+ if (t === 'ok') {
330
+ return f0(r);
331
+ }
332
+ }
333
+ {
334
+ const [t, r] = v1(args);
335
+ if (t === 'ok') {
336
+ return f1(r);
337
+ }
338
+ }
339
+ throw 'unreachable: args did not match any parameter set';
340
+ };
341
+ const f0 = (args) => Number(args.b) + args.a.length;
342
+ const f1 = (args) => args.c;
343
+ const x = func(f0, f1);
344
+ return () => {
345
+ assertEq(x({ a: 'hello', b: 42n }), 47);
346
+ assertEq(x({ a: 'goodbye', b: 'world', c: 43 }), 43);
347
+ };
348
+ }
291
349
  };
@@ -88,6 +88,15 @@ export declare const proof: {
88
88
  };
89
89
  function: () => void;
90
90
  };
91
+ mul: () => {
92
+ nullish: () => void;
93
+ boolean: () => void;
94
+ number: () => void;
95
+ bigint: () => void;
96
+ string: () => void;
97
+ array: () => void;
98
+ object: () => void;
99
+ };
91
100
  stringCoercion: {
92
101
  number: () => void;
93
102
  bool: () => void;
@@ -1,18 +1,19 @@
1
+ import { assert } from '../../fjs/asserts/module.f.js';
1
2
  const { is } = Object;
2
3
  const ois = (a) => (b) => {
3
- if (is(a, b)) { }
4
- else {
5
- throw [a, 'is', b];
6
- }
4
+ assert(is(a, b), [a, 'is', b]);
7
5
  };
8
6
  const { isNaN } = Number;
9
7
  const nanRes = (op) => (n) => {
10
8
  const result = op(n);
11
- if (!isNaN(result)) {
12
- throw result;
13
- }
9
+ assert(isNaN(result), result);
14
10
  };
15
11
  const stringCoercion = String;
12
+ const multiply = (a) => (b) => a * b;
13
+ const multiplyEq = (a) => (b) => (expected) => {
14
+ ois(multiply(a)(b))(expected);
15
+ ois(multiply(b)(a))(expected);
16
+ };
16
17
  export const proof = {
17
18
  eq: () => {
18
19
  const e = (a) => (b) => {
@@ -174,6 +175,56 @@ export const proof = {
174
175
  function: () => nan(op(() => { }))
175
176
  };
176
177
  },
178
+ mul: () => {
179
+ return {
180
+ nullish: () => {
181
+ multiplyEq(null)(null)(0);
182
+ multiplyEq(null)(0)(0);
183
+ multiplyEq(undefined)(0)(NaN);
184
+ },
185
+ boolean: () => {
186
+ multiplyEq(true)(0)(0);
187
+ multiplyEq(true)(1)(1);
188
+ multiplyEq(true)(10)(10);
189
+ multiplyEq(false)(0)(0);
190
+ multiplyEq(false)(1)(0);
191
+ multiplyEq(false)(10)(0);
192
+ },
193
+ number: () => {
194
+ multiplyEq(0)(0)(0);
195
+ multiplyEq(0)(1)(0);
196
+ multiplyEq(1)(1)(1);
197
+ multiplyEq(1)(-1)(-1);
198
+ multiplyEq(1)(10)(10);
199
+ multiplyEq(-1)(10)(-10);
200
+ multiplyEq(10)(10)(100);
201
+ multiplyEq(-10)(10)(-100);
202
+ },
203
+ bigint: () => {
204
+ multiplyEq(0n)(0n)(0n);
205
+ multiplyEq(0n)(1n)(0n);
206
+ multiplyEq(1n)(1n)(1n);
207
+ multiplyEq(1n)(-1n)(-1n);
208
+ multiplyEq(1n)(10n)(10n);
209
+ multiplyEq(-1n)(10n)(-10n);
210
+ multiplyEq(10n)(10n)(100n);
211
+ multiplyEq(-10n)(10n)(-100n);
212
+ },
213
+ string: () => {
214
+ multiplyEq('')(1)(0);
215
+ multiplyEq('10')(1)(10);
216
+ multiplyEq('a')(1)(NaN);
217
+ multiplyEq('1n')(1)(NaN);
218
+ },
219
+ array: () => {
220
+ multiplyEq([])(1)(0);
221
+ multiplyEq([10])(1)(10);
222
+ multiplyEq(['10'])(1)(10);
223
+ multiplyEq([0, 0])(1)(NaN);
224
+ },
225
+ object: () => multiplyEq({})(1)(NaN)
226
+ };
227
+ },
177
228
  stringCoercion: {
178
229
  number: () => {
179
230
  if (stringCoercion(123) !== '123') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "functionalscript",
3
- "version": "0.41.0",
3
+ "version": "0.42.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "**/*.js",
@@ -10,7 +10,7 @@
10
10
  "scripts": {
11
11
  "prepack": "tsc --NoEmit false",
12
12
  "test": "tsc && node ./fjs/module.ts t",
13
- "cov": "node --test --experimental-test-coverage --test-coverage-include=**/module.f.ts",
13
+ "cov": "node --test --experimental-test-coverage --test-coverage-include=**/module.f.ts --test-coverage-include=**/module.f.mjs",
14
14
  "start": "node ./fjs/module.ts",
15
15
  "ci-update": "node ./fjs/module.ts ci",
16
16
  "dev-update": "node ./fjs/module.ts r ./fjs/dev/update/module.f.ts",