functionalscript 0.40.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.
- package/fjs/cas/evo/module.f.d.ts +27 -7
- package/fjs/cas/evo/module.f.js +42 -9
- package/fjs/cas/evo/proof.f.d.ts +4 -0
- package/fjs/cas/evo/proof.f.js +81 -0
- package/fjs/ci/config/module.f.d.ts +5 -5
- package/fjs/ci/config/module.f.js +5 -5
- package/fjs/ci/deno/module.f.d.ts +8 -0
- package/fjs/ci/deno/module.f.js +9 -1
- package/fjs/ci/deno/proof.f.d.ts +5 -0
- package/fjs/ci/deno/proof.f.js +26 -0
- package/fjs/ci/node/proof.f.d.ts +3 -0
- package/fjs/ci/node/proof.f.js +17 -0
- package/fjs/ci/proof.f.js +3 -2
- package/fjs/dev/module.f.d.ts +15 -4
- package/fjs/dev/module.f.js +38 -7
- package/fjs/dev/package_json/module.f.js +7 -4
- package/fjs/dev/proof.f.js +10 -1
- package/fjs/djs/parser/module.f.js +13 -3
- package/fjs/djs/parser/proof.f.d.ts +1 -0
- package/fjs/djs/parser/proof.f.js +26 -0
- package/fjs/djs/tokenizer/proof.f.js +11 -4
- package/fjs/effects/module.f.d.ts +20 -0
- package/fjs/effects/module.f.js +25 -1
- package/fjs/effects/proof.f.d.ts +5 -0
- package/fjs/effects/proof.f.js +22 -0
- package/fjs/emergent_testing/proof.f.js +20 -3
- package/fjs/js/tokenizer/proof.f.js +7 -0
- package/fjs/mcp/cas/module.f.js +4 -1
- package/fjs/mcp/evo/module.f.d.ts +7 -2
- package/fjs/mcp/evo/module.f.js +10 -5
- package/fjs/mcp/evo/proof.f.d.ts +1 -0
- package/fjs/mcp/evo/proof.f.js +22 -1
- package/fjs/mcp/module.f.d.ts +1 -1
- package/fjs/mcp/module.f.js +1 -1
- package/fjs/mcp/proof.f.js +37 -18
- package/fjs/media/json/module.f.d.ts +15 -1
- package/fjs/media/json/module.f.js +17 -1
- package/fjs/media/json/parser/module.f.js +13 -3
- package/fjs/media/json/parser/proof.f.d.ts +1 -0
- package/fjs/media/json/parser/proof.f.js +37 -1
- package/fjs/media/json/proof.f.d.ts +4 -0
- package/fjs/media/json/proof.f.js +14 -2
- package/fjs/media/json/serializer/module.f.d.ts +4 -0
- package/fjs/media/json/serializer/module.f.js +39 -2
- package/fjs/media/json/serializer/proof.f.js +32 -8
- package/fjs/media/module.f.d.ts +92 -10
- package/fjs/media/module.f.js +73 -8
- package/fjs/media/proof.f.d.ts +10 -0
- package/fjs/media/proof.f.js +79 -10
- package/fjs/media/revision/module.f.d.ts +9 -1
- package/fjs/media/revision/module.f.js +14 -5
- package/fjs/protocol/mcp/stdio/module.f.js +2 -5
- package/fjs/text/utf16/module.f.d.ts +24 -0
- package/fjs/text/utf16/module.f.js +24 -0
- package/fjs/text/utf16/proof.f.d.ts +1 -0
- package/fjs/text/utf16/proof.f.js +10 -1
- package/fjs/types/rtti/validate/proof.f.d.ts +2 -0
- package/fjs/types/rtti/validate/proof.f.js +58 -0
- package/nanvm-lib/tests/proof.f.d.ts +9 -0
- package/nanvm-lib/tests/proof.f.js +58 -7
- package/package.json +2 -2
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
* subject (see [`fjs/media/revision/README.md`](../../media/revision/README.md)).
|
|
18
18
|
* `Cache` therefore tracks, per subject, every revision hash seen and every
|
|
19
19
|
* hash referenced as somebody's parent; heads are the set difference between
|
|
20
|
-
* the two, computed at read time ({@link headsOf}).
|
|
20
|
+
* the two, computed at read time ({@link headsOf}). Alongside them it records
|
|
21
|
+
* which of the seen revisions are `archived`, so {@link Evo.list} can classify
|
|
22
|
+
* a subject as active or archived from its heads' flags
|
|
23
|
+
* ({@link subjectListed}) without touching the store. Storing both sets rather
|
|
21
24
|
* than a running head list is what makes folding revisions truly order
|
|
22
25
|
* independent: `cas.list()` (used by {@link buildCache} to scan an existing
|
|
23
26
|
* store) returns hashes in hash order, not revision ancestry, so a child can
|
|
@@ -97,14 +100,22 @@ export type RevisionData = {
|
|
|
97
100
|
readonly generation?: number | undefined;
|
|
98
101
|
};
|
|
99
102
|
/**
|
|
100
|
-
* Per-subject bookkeeping: every revision hash seen for the subject,
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* {@link headsOf} for how heads are
|
|
103
|
+
* Per-subject bookkeeping: every revision hash seen for the subject, every
|
|
104
|
+
* hash any of those revisions names as a parent, and which of the seen
|
|
105
|
+
* revisions are `archived`. See the module doc for why the first two sets are
|
|
106
|
+
* kept (rather than a running head list), {@link headsOf} for how heads are
|
|
107
|
+
* derived from them, and {@link subjectListed} for how `archived` classifies a
|
|
108
|
+
* subject once its heads are known.
|
|
109
|
+
*
|
|
110
|
+
* `archived` is keyed by revision hash, not by subject, for the same reason
|
|
111
|
+
* heads are computed at read time: which revisions are heads is only known
|
|
112
|
+
* once the whole store has been folded in, so a per-subject archived flag
|
|
113
|
+
* would have to be revised every time a later fold changes the head set.
|
|
104
114
|
*/
|
|
105
115
|
export type SubjectState = {
|
|
106
116
|
readonly hashes: readonly Hash[];
|
|
107
117
|
readonly parents: readonly Hash[];
|
|
118
|
+
readonly archived: readonly Hash[];
|
|
108
119
|
};
|
|
109
120
|
/** In-memory index: subject → its {@link SubjectState}. */
|
|
110
121
|
export type Cache = {
|
|
@@ -182,8 +193,17 @@ export declare const addRevision: <O extends Operation>(cas: Cas<O>) => (cacheKe
|
|
|
182
193
|
export declare const readRevision: <O extends Operation>(cas: Cas<O>) => (hash: Hash) => Effect<O, Result<RevisionData, string>>;
|
|
183
194
|
/** The Evo API described in `fjs/cas/evo/README.md`, bound to a `Cas<O>` and its cache slot. */
|
|
184
195
|
export type Evo<O extends Operation> = {
|
|
185
|
-
/**
|
|
186
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Returns the subjects matching a status filter: the active ones by
|
|
198
|
+
* default, the archived ones when `archived` is `true`. A subject's status
|
|
199
|
+
* is derived from its current heads — see {@link subjectListed}, which
|
|
200
|
+
* also explains why a subject with no current heads is in neither result.
|
|
201
|
+
*
|
|
202
|
+
* There is deliberately no all-subjects mode: nothing needs one yet, and
|
|
203
|
+
* adding it later is a compatible extension of this parameter, while
|
|
204
|
+
* removing it would not be.
|
|
205
|
+
*/
|
|
206
|
+
readonly list: (archived?: true) => Effect<MemOp, readonly Subject[]>;
|
|
187
207
|
/** Returns the current head hashes of `subject` (empty if unknown). */
|
|
188
208
|
readonly head: (subject: Subject) => Effect<MemOp, readonly Hash[]>;
|
|
189
209
|
/** Adds a new head; see {@link addRevision}. */
|
package/fjs/cas/evo/module.f.js
CHANGED
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
* subject (see [`fjs/media/revision/README.md`](../../media/revision/README.md)).
|
|
18
18
|
* `Cache` therefore tracks, per subject, every revision hash seen and every
|
|
19
19
|
* hash referenced as somebody's parent; heads are the set difference between
|
|
20
|
-
* the two, computed at read time ({@link headsOf}).
|
|
20
|
+
* the two, computed at read time ({@link headsOf}). Alongside them it records
|
|
21
|
+
* which of the seen revisions are `archived`, so {@link Evo.list} can classify
|
|
22
|
+
* a subject as active or archived from its heads' flags
|
|
23
|
+
* ({@link subjectListed}) without touching the store. Storing both sets rather
|
|
21
24
|
* than a running head list is what makes folding revisions truly order
|
|
22
25
|
* independent: `cas.list()` (used by {@link buildCache} to scan an existing
|
|
23
26
|
* store) returns hashes in hash order, not revision ancestry, so a child can
|
|
@@ -58,7 +61,7 @@ import { isNotFound } from '../../effects/node/module.f.js';
|
|
|
58
61
|
export const emptyCache = { bySubject: {} };
|
|
59
62
|
/** Canonical JSON encoder for a `Revision` — key order carries no meaning for detection. */
|
|
60
63
|
const toJson = stringify(identity);
|
|
61
|
-
const emptySubjectState = { hashes: [], parents: [] };
|
|
64
|
+
const emptySubjectState = { hashes: [], parents: [], archived: [] };
|
|
62
65
|
/** Adds every item of `items` to `set` that isn't already there, preserving `set`'s existing order. */
|
|
63
66
|
const union = (set) => (items) => items.reduce((acc, h) => acc.includes(h) ? acc : [...acc, h], set);
|
|
64
67
|
/**
|
|
@@ -76,12 +79,37 @@ const union = (set) => (items) => items.reduce((acc, h) => acc.includes(h) ? acc
|
|
|
76
79
|
const canonicalHash = (h) => vecToCBase32(unwrap(cBase32ToVec(h)));
|
|
77
80
|
/** A subject's current heads: revision hashes seen that no other revision of the same subject names as a parent. */
|
|
78
81
|
const headsOf = (state) => state.hashes.filter(h => !state.parents.includes(h));
|
|
82
|
+
/**
|
|
83
|
+
* Whether a subject in `state` belongs in {@link Evo.list}'s result for the
|
|
84
|
+
* given `archived` filter — the subject-level status derived from its
|
|
85
|
+
* revision-level `archived` flags:
|
|
86
|
+
*
|
|
87
|
+
* - **active** — at least one current head is not archived. This is the
|
|
88
|
+
* default result set (`archived` omitted).
|
|
89
|
+
* - **archived** — the subject has at least one current head and every one of
|
|
90
|
+
* them is archived (`archived: true`).
|
|
91
|
+
*
|
|
92
|
+
* Concurrent heads can disagree, and the two rules resolve that the same way:
|
|
93
|
+
* one unarchived head keeps the whole subject active, because a subject is
|
|
94
|
+
* only done evolving when nothing left to build on remains. A subject with no
|
|
95
|
+
* current heads is neither active nor archived and appears in no result — the
|
|
96
|
+
* status is a statement about heads, and there is nothing to state. That case
|
|
97
|
+
* needs the explicit `heads.length` test only in the archived branch, since
|
|
98
|
+
* "every head is archived" is vacuously true of no heads at all.
|
|
99
|
+
*/
|
|
100
|
+
const subjectListed = (archived) => (state) => {
|
|
101
|
+
const heads = headsOf(state);
|
|
102
|
+
const unarchived = heads.filter(h => !state.archived.includes(h));
|
|
103
|
+
return archived === undefined
|
|
104
|
+
? unarchived.length !== 0
|
|
105
|
+
: heads.length !== 0 && unarchived.length === 0;
|
|
106
|
+
};
|
|
79
107
|
/**
|
|
80
108
|
* Folds one more stored revision into `cache`: `hash` joins its subject's
|
|
81
|
-
* `hashes` set,
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* incremental `add`.
|
|
109
|
+
* `hashes` set, `revision.parents` (canonicalized, see {@link canonicalHash})
|
|
110
|
+
* join its `parents` set, and `hash` also joins the `archived` set when the
|
|
111
|
+
* revision carries `archived: true`. Order independent (see the module doc) —
|
|
112
|
+
* used both for a full-store scan and for a single incremental `add`.
|
|
85
113
|
*
|
|
86
114
|
* Looks `revision.subject` up via {@link at} (own-property only), not plain
|
|
87
115
|
* bracket indexing: a subject is an arbitrary caller-supplied string
|
|
@@ -96,6 +124,7 @@ const addRevisionToCache = (hash, revision) => (cache) => {
|
|
|
96
124
|
const state = {
|
|
97
125
|
hashes: union(existing.hashes)([hash]),
|
|
98
126
|
parents: union(existing.parents)(revision.parents.map(canonicalHash)),
|
|
127
|
+
archived: union(existing.archived)(revision.archived === undefined ? [] : [hash]),
|
|
99
128
|
};
|
|
100
129
|
return { bySubject: { ...cache.bySubject, [revision.subject]: state } };
|
|
101
130
|
};
|
|
@@ -407,9 +436,13 @@ export const readRevision = (cas) => (hash) => {
|
|
|
407
436
|
};
|
|
408
437
|
/** Builds the {@link Evo} API over `cas`, backed by the cache at `cacheKey` (see {@link initEvo}). */
|
|
409
438
|
export const evo = (cas) => (cacheKey) => ({
|
|
410
|
-
list:
|
|
411
|
-
|
|
412
|
-
|
|
439
|
+
list: archived => {
|
|
440
|
+
const listed = subjectListed(archived);
|
|
441
|
+
return eff(read(cacheKey))
|
|
442
|
+
.step(cache => pure(definedEntries(cache.bySubject)
|
|
443
|
+
.flatMap(([subject, state]) => listed(state) ? [subject] : [])))
|
|
444
|
+
.value;
|
|
445
|
+
},
|
|
413
446
|
head: subject => eff(read(cacheKey))
|
|
414
447
|
.step(cache => {
|
|
415
448
|
const state = at(subject)(cache.bySubject);
|
package/fjs/cas/evo/proof.f.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ export declare const proof: {
|
|
|
9
9
|
buildCacheOrderIndependentWhenChildScannedBeforeParent: () => void;
|
|
10
10
|
buildCacheCanonicalizesNonCanonicalParentHashes: () => void;
|
|
11
11
|
addRevisionBuildsHeadsAcrossChainAndFork: () => void;
|
|
12
|
+
listPartitionsSubjectsByHeadArchivedFlag: () => void;
|
|
13
|
+
listTreatsDisagreeingHeadsAsActive: () => void;
|
|
14
|
+
listIgnoresArchivedRevisionsThatAreNoLongerHeads: () => void;
|
|
15
|
+
listExcludesSubjectWithNoCurrentHeads: () => void;
|
|
12
16
|
addRevisionIdempotentOnDuplicateContent: () => void;
|
|
13
17
|
addRevisionCanonicalizesParentSpellingBeforeSerializing: () => void;
|
|
14
18
|
addRevisionResolvesSubjectFromSingleParent: () => void;
|
package/fjs/cas/evo/proof.f.js
CHANGED
|
@@ -183,6 +183,87 @@ export const proof = {
|
|
|
183
183
|
assertEq(subjects.length, 1);
|
|
184
184
|
assertEq(subjects[0], 'doc');
|
|
185
185
|
},
|
|
186
|
+
// `list()` and `list(true)` partition subjects by the archived flags of
|
|
187
|
+
// their current heads: a lone root revision carrying `archived: true`
|
|
188
|
+
// makes its subject archived, one without it leaves the subject active,
|
|
189
|
+
// and neither subject appears in the other's result.
|
|
190
|
+
listPartitionsSubjectsByHeadArchivedFlag: () => {
|
|
191
|
+
const c = fileCas(sha256)(home);
|
|
192
|
+
const [state0, cacheKey] = virtual(emptyState)(initEvo(c));
|
|
193
|
+
const e = evo(c)(cacheKey);
|
|
194
|
+
const [state1, live] = virtual(state0)(e.add({ parents: [], subject: 'live', snapshot: vecToCBase32(vec8(0x40n)) }));
|
|
195
|
+
assert(live[0] === 'ok', ['expected the active add to succeed', live]);
|
|
196
|
+
const [state2, gone] = virtual(state1)(e.add({ parents: [], subject: 'gone', snapshot: vecToCBase32(vec8(0x41n)), archived: true }));
|
|
197
|
+
assert(gone[0] === 'ok', ['expected the archived add to succeed', gone]);
|
|
198
|
+
const [state3, active] = virtual(state2)(e.list());
|
|
199
|
+
assertEq(active.length, 1);
|
|
200
|
+
assertEq(active[0], 'live');
|
|
201
|
+
const [, archived] = virtual(state3)(e.list(true));
|
|
202
|
+
assertEq(archived.length, 1);
|
|
203
|
+
assertEq(archived[0], 'gone');
|
|
204
|
+
},
|
|
205
|
+
// Concurrent heads can disagree about `archived`. One unarchived head
|
|
206
|
+
// keeps the whole subject active — there is still a head left to build on
|
|
207
|
+
// — so the subject stays out of the archived-only result.
|
|
208
|
+
listTreatsDisagreeingHeadsAsActive: () => {
|
|
209
|
+
const c = fileCas(sha256)(home);
|
|
210
|
+
const [state0, cacheKey] = virtual(emptyState)(initEvo(c));
|
|
211
|
+
const e = evo(c)(cacheKey);
|
|
212
|
+
const [state1, root] = virtual(state0)(e.add({ parents: [], subject: 'doc', snapshot: vecToCBase32(vec8(0x42n)) }));
|
|
213
|
+
assert(root[0] === 'ok', ['expected root ok', root]);
|
|
214
|
+
// The two children differ only in `archived`, which is enough to make
|
|
215
|
+
// them distinct blobs, hence two concurrent heads of one root.
|
|
216
|
+
const [state2, kept] = virtual(state1)(e.add({ parents: [root[1]], subject: 'doc' }));
|
|
217
|
+
assert(kept[0] === 'ok', ['expected the unarchived child ok', kept]);
|
|
218
|
+
const [state3, dropped] = virtual(state2)(e.add({ parents: [root[1]], subject: 'doc', archived: true }));
|
|
219
|
+
assert(dropped[0] === 'ok', ['expected the archived child ok', dropped]);
|
|
220
|
+
const [state4, heads] = virtual(state3)(e.head('doc'));
|
|
221
|
+
assertEq(heads.length, 2);
|
|
222
|
+
const [state5, active] = virtual(state4)(e.list());
|
|
223
|
+
assertEq(active.length, 1);
|
|
224
|
+
assertEq(active[0], 'doc');
|
|
225
|
+
const [, archived] = virtual(state5)(e.list(true));
|
|
226
|
+
assertEq(archived.length, 0);
|
|
227
|
+
},
|
|
228
|
+
// `archived` is a property of a revision, not of a subject: a subject
|
|
229
|
+
// archived at one revision is active again as soon as an unarchived child
|
|
230
|
+
// demotes that revision out of the head set.
|
|
231
|
+
listIgnoresArchivedRevisionsThatAreNoLongerHeads: () => {
|
|
232
|
+
const c = fileCas(sha256)(home);
|
|
233
|
+
const [state0, cacheKey] = virtual(emptyState)(initEvo(c));
|
|
234
|
+
const e = evo(c)(cacheKey);
|
|
235
|
+
const [state1, root] = virtual(state0)(e.add({ parents: [], subject: 'doc', snapshot: vecToCBase32(vec8(0x45n)), archived: true }));
|
|
236
|
+
assert(root[0] === 'ok', ['expected the archived root ok', root]);
|
|
237
|
+
const [state2, revived] = virtual(state1)(e.add({ parents: [root[1]], subject: 'doc' }));
|
|
238
|
+
assert(revived[0] === 'ok', ['expected the unarchived child ok', revived]);
|
|
239
|
+
const [state3, active] = virtual(state2)(e.list());
|
|
240
|
+
assertEq(active.length, 1);
|
|
241
|
+
assertEq(active[0], 'doc');
|
|
242
|
+
const [, archived] = virtual(state3)(e.list(true));
|
|
243
|
+
assertEq(archived.length, 0);
|
|
244
|
+
},
|
|
245
|
+
// A subject can end up with no current head at all, and a status is a
|
|
246
|
+
// statement about heads — so such a subject is neither active nor
|
|
247
|
+
// archived and belongs to neither result. Nothing verifies that a stored
|
|
248
|
+
// blob actually hashes to the key it sits under, so a hand-crafted or
|
|
249
|
+
// corrupt store can present a revision naming its own hash as its parent;
|
|
250
|
+
// `fixedCas` reproduces exactly that, which a real `fileCas` cannot.
|
|
251
|
+
listExcludesSubjectWithNoCurrentHeads: () => {
|
|
252
|
+
const selfHash = vec8(0x43n);
|
|
253
|
+
const snapshotHash = vecToCBase32(vec8(0x44n));
|
|
254
|
+
const text = `{"dialect":"${revisionDialect}","subject":"doc","parents":["${vecToCBase32(selfHash)}"],"snapshot":"${snapshotHash}","generation":1}`;
|
|
255
|
+
const bytes = tryUtf8(text);
|
|
256
|
+
assert(bytes !== null, 'expected the sample revision text to encode as UTF-8');
|
|
257
|
+
const cas = fixedCas([[selfHash, bytes]]);
|
|
258
|
+
const [state0, cacheKey] = virtual(emptyState)(initEvo(cas));
|
|
259
|
+
const e = evo(cas)(cacheKey);
|
|
260
|
+
const [state1, heads] = virtual(state0)(e.head('doc'));
|
|
261
|
+
assertEq(heads.length, 0);
|
|
262
|
+
const [state2, active] = virtual(state1)(e.list());
|
|
263
|
+
assertEq(active.length, 0);
|
|
264
|
+
const [, archived] = virtual(state2)(e.list(true));
|
|
265
|
+
assertEq(archived.length, 0);
|
|
266
|
+
},
|
|
186
267
|
// Adding the exact same revision twice yields the same (deduplicated)
|
|
187
268
|
// content hash and must not duplicate the head entry.
|
|
188
269
|
addRevisionIdempotentOnDuplicateContent: () => {
|
|
@@ -18,19 +18,19 @@ export declare const images: {
|
|
|
18
18
|
readonly arm: 'windows-11-arm';
|
|
19
19
|
};
|
|
20
20
|
};
|
|
21
|
-
export declare const functionalscript: '0.
|
|
21
|
+
export declare const functionalscript: '0.41.0';
|
|
22
22
|
export declare const bun = "1.3.14";
|
|
23
23
|
export declare const deno = "2.9.4";
|
|
24
24
|
export declare const node: {
|
|
25
|
-
readonly default: '26.
|
|
26
|
-
readonly node22: '22.23.
|
|
25
|
+
readonly default: '26.6.0';
|
|
26
|
+
readonly node22: '22.23.2';
|
|
27
27
|
readonly node24: '24.18.0';
|
|
28
28
|
};
|
|
29
29
|
export declare const nixpkgs: {
|
|
30
30
|
readonly ref: 'nixos-26.05';
|
|
31
|
-
readonly commit: '
|
|
31
|
+
readonly commit: '04607e1165ac22c5fde6dcc54c9e0b3c0487c555';
|
|
32
32
|
};
|
|
33
|
-
export declare const wasmtime = "47.0.
|
|
33
|
+
export declare const wasmtime = "47.0.3";
|
|
34
34
|
export declare const wasmer = "7.2.1";
|
|
35
35
|
export declare const actions: {
|
|
36
36
|
readonly 'actions/checkout': 'v7.0.1';
|
|
@@ -23,7 +23,7 @@ export const images = {
|
|
|
23
23
|
// published FunctionalScript release; do not tie it to package.json's current
|
|
24
24
|
// in-repo version.
|
|
25
25
|
// https://www.npmjs.com/package/functionalscript
|
|
26
|
-
export const functionalscript = '0.
|
|
26
|
+
export const functionalscript = '0.41.0';
|
|
27
27
|
// https://bun.sh/
|
|
28
28
|
export const bun = '1.3.14';
|
|
29
29
|
// https://deno.com/
|
|
@@ -36,8 +36,8 @@ export const deno = '2.9.4';
|
|
|
36
36
|
// it offers rather than the latest release.
|
|
37
37
|
// https://nodejs.org/en/download
|
|
38
38
|
export const node = {
|
|
39
|
-
default: '26.
|
|
40
|
-
node22: '22.23.
|
|
39
|
+
default: '26.6.0',
|
|
40
|
+
node22: '22.23.2',
|
|
41
41
|
node24: '24.18.0',
|
|
42
42
|
};
|
|
43
43
|
// Official Nixpkgs snapshot used by the generated CI flakes. `ref` is the
|
|
@@ -47,10 +47,10 @@ export const node = {
|
|
|
47
47
|
// https://channels.nixos.org/nixos-26.05/git-revision
|
|
48
48
|
export const nixpkgs = {
|
|
49
49
|
ref: 'nixos-26.05',
|
|
50
|
-
commit: '
|
|
50
|
+
commit: '04607e1165ac22c5fde6dcc54c9e0b3c0487c555',
|
|
51
51
|
};
|
|
52
52
|
// https://github.com/bytecodealliance/wasmtime/releases
|
|
53
|
-
export const wasmtime = '47.0.
|
|
53
|
+
export const wasmtime = '47.0.3';
|
|
54
54
|
// https://github.com/wasmerio/wasmer/releases
|
|
55
55
|
export const wasmer = '7.2.1';
|
|
56
56
|
// GitHub Action versions used by CI step builders. The key is the action
|
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
import { type MetaStep } from '../common/module.f.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The regular expression selecting FunctionalScript implementation modules for
|
|
4
|
+
* Deno coverage. Both authored extensions are included so a module migrated
|
|
5
|
+
* from `.f.ts` to `.f.mjs` stays in the report. Keep it semantically equal to
|
|
6
|
+
* the `--test-coverage-include` list in `package.json` and to the `cov` task in
|
|
7
|
+
* `deno.json`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const coverageInclude: '.*module\\.f\\.(ts|mjs)';
|
|
2
10
|
export declare const denoSteps: (version: string) => readonly MetaStep[];
|
package/fjs/ci/deno/module.f.js
CHANGED
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
import { deno } from '../config/module.f.js';
|
|
8
8
|
import { install, test, uses } from '../common/module.f.js';
|
|
9
9
|
const denoTest = 'deno test --allow-read --allow-env --allow-sys';
|
|
10
|
+
/**
|
|
11
|
+
* The regular expression selecting FunctionalScript implementation modules for
|
|
12
|
+
* Deno coverage. Both authored extensions are included so a module migrated
|
|
13
|
+
* from `.f.ts` to `.f.mjs` stays in the report. Keep it semantically equal to
|
|
14
|
+
* the `--test-coverage-include` list in `package.json` and to the `cov` task in
|
|
15
|
+
* `deno.json`.
|
|
16
|
+
*/
|
|
17
|
+
export const coverageInclude = '.*module\\.f\\.(ts|mjs)';
|
|
10
18
|
export const denoSteps = (version) => [
|
|
11
19
|
install(uses('denoland/setup-deno', { 'deno-version': deno })),
|
|
12
20
|
// We need --minimum-dependency-age=0 for functionalscript because we would like to use
|
|
@@ -16,5 +24,5 @@ export const denoSteps = (version) => [
|
|
|
16
24
|
install({ run: `deno install -g -A --minimum-dependency-age=0 npm:functionalscript@${version}` }),
|
|
17
25
|
test({ run: `deno run -A --minimum-dependency-age=0 npm:functionalscript@${version} t` }),
|
|
18
26
|
test({ run: 'deno install --frozen' }),
|
|
19
|
-
test({ run: `${denoTest} --coverage && deno coverage --include='
|
|
27
|
+
test({ run: `${denoTest} --coverage && deno coverage --include='${coverageInclude}'` }),
|
|
20
28
|
];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { coverageInclude, denoSteps } from './module.f.js';
|
|
2
|
+
import { toSteps } from '../common/module.f.js';
|
|
3
|
+
import { assert, assertEq } from '../../asserts/module.f.js';
|
|
4
|
+
const coverageRuns = (version) => toSteps(denoSteps(version))
|
|
5
|
+
.flatMap(s => s.run !== undefined && s.run.includes('deno coverage') ? [s.run] : []);
|
|
6
|
+
export const proof = {
|
|
7
|
+
// A regression guard: dropping either authored implementation extension
|
|
8
|
+
// from the Deno coverage filter silently removes those modules from the
|
|
9
|
+
// CI coverage report while CI still passes.
|
|
10
|
+
coverageInclude: () => {
|
|
11
|
+
assertEq(coverageInclude, '.*module\\.f\\.(ts|mjs)');
|
|
12
|
+
},
|
|
13
|
+
coverageStep: () => {
|
|
14
|
+
const runs = coverageRuns('0.0.0');
|
|
15
|
+
assertEq(runs.length, 1);
|
|
16
|
+
const [run] = runs;
|
|
17
|
+
assert(run !== undefined);
|
|
18
|
+
assert(run.includes(`deno coverage --include='${coverageInclude}'`));
|
|
19
|
+
},
|
|
20
|
+
installsPinnedVersion: () => {
|
|
21
|
+
const runs = toSteps(denoSteps('1.2.3'))
|
|
22
|
+
.flatMap(s => s.run !== undefined && s.run.includes('npm:functionalscript@') ? [s.run] : []);
|
|
23
|
+
assertEq(runs.length, 2);
|
|
24
|
+
assert(runs.every(r => r.includes('npm:functionalscript@1.2.3')));
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { basicNode } from './module.f.js';
|
|
2
|
+
import { test } from '../common/module.f.js';
|
|
3
|
+
import { assertEq } from '../../asserts/module.f.js';
|
|
4
|
+
export const proof = {
|
|
5
|
+
basicNode: () => {
|
|
6
|
+
const extra = [test({ run: 'echo extra' })];
|
|
7
|
+
const steps = basicNode('22.0.0')(extra);
|
|
8
|
+
assertEq(steps.length, 3);
|
|
9
|
+
const [setupNode, npmCi, extraStep] = steps;
|
|
10
|
+
assertEq(setupNode.type, 'install');
|
|
11
|
+
assertEq(setupNode.type === 'install' ? setupNode.step.uses : undefined, 'actions/setup-node@v7.0.0');
|
|
12
|
+
assertEq(setupNode.type === 'install' ? setupNode.step.with?.['node-version'] : undefined, '22.0.0');
|
|
13
|
+
assertEq(npmCi.type, 'test');
|
|
14
|
+
assertEq(npmCi.type === 'test' ? npmCi.step.run : undefined, 'npm ci');
|
|
15
|
+
assertEq(extraStep, extra[0]);
|
|
16
|
+
},
|
|
17
|
+
};
|
package/fjs/ci/proof.f.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ci, main } from './module.f.js';
|
|
2
2
|
import { functionalscript, node } from './config/module.f.js';
|
|
3
3
|
import { nodeNixJobs } from './node/module.f.js';
|
|
4
|
+
import { coverageInclude } from './deno/module.f.js';
|
|
4
5
|
import { utf8, utf8ToString } from '../text/module.f.js';
|
|
5
6
|
import { empty as emptyVec, isVec } from '../types/bit_vec/module.f.js';
|
|
6
7
|
import { test, ubuntu, parseGitHubAction } from './common/module.f.js';
|
|
@@ -33,7 +34,7 @@ const text = (dir, name) => {
|
|
|
33
34
|
const path = (dir, names) => names.reduce(subDir, dir);
|
|
34
35
|
const workflow = (state) => {
|
|
35
36
|
const workflows = path(state.root, ['.github', 'workflows']);
|
|
36
|
-
return unwrap(parseGitHubAction(jsonParse(text(workflows, 'ci.yml'))));
|
|
37
|
+
return unwrap(parseGitHubAction(unwrap(jsonParse(text(workflows, 'ci.yml')))));
|
|
37
38
|
};
|
|
38
39
|
const flake = (state, id) => text(path(state.root, ['nix', 'generated', id]), 'flake.nix');
|
|
39
40
|
const run = (rust, nodeExtra = () => []) => {
|
|
@@ -132,7 +133,7 @@ export const proof = {
|
|
|
132
133
|
assert(hasRun(`deno install -g -A --minimum-dependency-age=0 npm:functionalscript@${functionalscript}`)(gha), 'expected configured-version deno install cache');
|
|
133
134
|
assert(hasRun('deno install --frozen')(gha), 'expected deno lock install');
|
|
134
135
|
assert(hasRun(`deno run -A --minimum-dependency-age=0 npm:functionalscript@${functionalscript} t`)(gha), 'expected configured-version deno install');
|
|
135
|
-
assert(hasRun(
|
|
136
|
+
assert(hasRun(`deno test --allow-read --allow-env --allow-sys --coverage && deno coverage --include='${coverageInclude}'`)(gha), 'expected limited-permission deno coverage');
|
|
136
137
|
assert(hasRun(`bun install -g functionalscript@${functionalscript}`)(gha), 'expected configured-version bun cache');
|
|
137
138
|
assert(hasRun('bun install --frozen-lockfile')(gha), 'expected bun lock install');
|
|
138
139
|
assert(hasRun(`bunx functionalscript@${functionalscript} t`)(gha), 'expected configured-version bun install');
|
package/fjs/dev/module.f.d.ts
CHANGED
|
@@ -14,10 +14,19 @@ export type ModuleMap = StringMap<string, Module>;
|
|
|
14
14
|
/**
|
|
15
15
|
* Returns `true` if the file should be loaded for proof discovery.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* Two symmetrical rules, each covering all four TS/JS module extensions:
|
|
18
|
+
*
|
|
19
|
+
* - **FunctionalScript modules** — anything ending in `.f.ts`, `.f.mts`,
|
|
20
|
+
* `.f.js`, or `.f.mjs`. They are safe to bulk-load by construction, since
|
|
21
|
+
* they have no import side effects, so the whole module is loaded and its
|
|
22
|
+
* internal `proof` export (if any) is discovered. `.f.ts` and `.f.mjs` are
|
|
23
|
+
* the authored extensions; `.f.js` is generated from `.f.ts`.
|
|
24
|
+
* - **Impure JavaScript/TypeScript proofs** — anything ending in `proof.ts`,
|
|
25
|
+
* `proof.mts`, `proof.js`, or `proof.mjs`. Outside FunctionalScript a module
|
|
26
|
+
* may have import side effects, so the load gate stays opt-in by filename.
|
|
27
|
+
*
|
|
28
|
+
* A `proof.f.mts` / `proof.f.mjs` matches the FunctionalScript rule by its
|
|
29
|
+
* `.f.` infix, not the vanilla proof rule.
|
|
21
30
|
*
|
|
22
31
|
* Whether a loaded module actually _contains_ a proof is determined at
|
|
23
32
|
* runtime by checking for an exported `proof` property.
|
|
@@ -42,5 +51,7 @@ export type LoadModuleOperations = Access | Import | All | Readdir;
|
|
|
42
51
|
export declare const loadModuleMap: (env: Env) => Effect<LoadModuleOperations, ModuleMap>;
|
|
43
52
|
export declare const proof: {
|
|
44
53
|
isSourceFile: () => void;
|
|
54
|
+
allFilesFindsFunctionalScript: () => void;
|
|
45
55
|
allFilesSkipsNodeModules: () => void;
|
|
56
|
+
loadModuleMapDefaultsToCwdWhenInitCwdUnset: () => void;
|
|
46
57
|
};
|
package/fjs/dev/module.f.js
CHANGED
|
@@ -13,17 +13,27 @@ import { emptyState, virtual } from '../effects/node/virtual/module.f.js';
|
|
|
13
13
|
/**
|
|
14
14
|
* Returns `true` if the file should be loaded for proof discovery.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
16
|
+
* Two symmetrical rules, each covering all four TS/JS module extensions:
|
|
17
|
+
*
|
|
18
|
+
* - **FunctionalScript modules** — anything ending in `.f.ts`, `.f.mts`,
|
|
19
|
+
* `.f.js`, or `.f.mjs`. They are safe to bulk-load by construction, since
|
|
20
|
+
* they have no import side effects, so the whole module is loaded and its
|
|
21
|
+
* internal `proof` export (if any) is discovered. `.f.ts` and `.f.mjs` are
|
|
22
|
+
* the authored extensions; `.f.js` is generated from `.f.ts`.
|
|
23
|
+
* - **Impure JavaScript/TypeScript proofs** — anything ending in `proof.ts`,
|
|
24
|
+
* `proof.mts`, `proof.js`, or `proof.mjs`. Outside FunctionalScript a module
|
|
25
|
+
* may have import side effects, so the load gate stays opt-in by filename.
|
|
26
|
+
*
|
|
27
|
+
* A `proof.f.mts` / `proof.f.mjs` matches the FunctionalScript rule by its
|
|
28
|
+
* `.f.` infix, not the vanilla proof rule.
|
|
20
29
|
*
|
|
21
30
|
* Whether a loaded module actually _contains_ a proof is determined at
|
|
22
31
|
* runtime by checking for an exported `proof` property.
|
|
23
32
|
*/
|
|
24
|
-
export const shouldLoad = (s) => s.endsWith('.f.ts') || s.endsWith('.f.
|
|
25
|
-
s.endsWith('
|
|
26
|
-
s.endsWith('proof.
|
|
33
|
+
export const shouldLoad = (s) => s.endsWith('.f.ts') || s.endsWith('.f.mts') ||
|
|
34
|
+
s.endsWith('.f.js') || s.endsWith('.f.mjs') ||
|
|
35
|
+
s.endsWith('proof.ts') || s.endsWith('proof.mts') ||
|
|
36
|
+
s.endsWith('proof.js') || s.endsWith('proof.mjs');
|
|
27
37
|
const isSourceFile = (path) => path.endsWith('.js') || path.endsWith('.ts') || path.endsWith('.mts') || path.endsWith('.mjs');
|
|
28
38
|
const allFiles = (s, predicate) => {
|
|
29
39
|
const load = (p) => {
|
|
@@ -91,6 +101,19 @@ export const proof = {
|
|
|
91
101
|
assert(!isSourceFile('readme.md'));
|
|
92
102
|
assert(!isSourceFile('module.json'));
|
|
93
103
|
},
|
|
104
|
+
allFilesFindsFunctionalScript: () => {
|
|
105
|
+
// Every FunctionalScript extension is discovered, so a module migrated
|
|
106
|
+
// from `.f.ts` to `.f.mjs` keeps its proofs. An ordinary `.mjs` is
|
|
107
|
+
// still skipped.
|
|
108
|
+
const root = {
|
|
109
|
+
'a.f.ts': [],
|
|
110
|
+
'b.f.mjs': [],
|
|
111
|
+
'c.f.mts': [],
|
|
112
|
+
'd.mjs': [],
|
|
113
|
+
};
|
|
114
|
+
const [, result] = virtual({ ...emptyState, root })(allFiles('.', shouldLoad));
|
|
115
|
+
assertEq(result.join(','), './a.f.ts,./b.f.mjs,./c.f.mts');
|
|
116
|
+
},
|
|
94
117
|
allFilesSkipsNodeModules: () => {
|
|
95
118
|
// `node_modules` is skipped without descending into it, even though
|
|
96
119
|
// it contains a file that would otherwise match the predicate.
|
|
@@ -101,4 +124,12 @@ export const proof = {
|
|
|
101
124
|
const [, result] = virtual({ ...emptyState, root })(allFiles('.', shouldLoad));
|
|
102
125
|
assertEq(result.join(','), './a.f.ts');
|
|
103
126
|
},
|
|
127
|
+
loadModuleMapDefaultsToCwdWhenInitCwdUnset: () => {
|
|
128
|
+
// With no `INIT_CWD` (e.g. `fjs t` invoked outside `npm run`), `env`
|
|
129
|
+
// lookup returns `undefined` and discovery falls back to `.`, so
|
|
130
|
+
// every path keeps its own `./`-prefix instead of having one stripped.
|
|
131
|
+
const root = { 'a.f.ts': () => ({}) };
|
|
132
|
+
const [, result] = virtual({ ...emptyState, root })(loadModuleMap({}));
|
|
133
|
+
assertEq(Object.keys(result).join(','), './a.f.ts');
|
|
134
|
+
},
|
|
104
135
|
};
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for reading package.json-style metadata without losing unrelated
|
|
3
|
+
* fields before write-back.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { parse as parseJsonText } from '../../media/json/module.f.js';
|
|
4
8
|
import { option, record, string } from '../../types/rtti/module.f.js';
|
|
5
9
|
import { validate as rttiValidate } from '../../types/rtti/validate/module.f.js';
|
|
6
10
|
import { error, ok } from '../../types/result/module.f.js';
|
|
@@ -10,7 +14,6 @@ export const packageJsonSchema = {
|
|
|
10
14
|
scripts: option(record(string)),
|
|
11
15
|
};
|
|
12
16
|
export const validatePackageJson = rttiValidate(packageJsonSchema);
|
|
13
|
-
const parseJsonText = (text) => jsonParse(jsonTokenize(stringToList(text)));
|
|
14
17
|
export const validatePackageJsonText = (text) => {
|
|
15
18
|
const [t, v] = parseJsonText(text);
|
|
16
19
|
if (t === 'error') {
|
package/fjs/dev/proof.f.js
CHANGED
|
@@ -2,13 +2,22 @@ import { assert, assertEq, todo } from '../asserts/module.f.js';
|
|
|
2
2
|
import { shouldLoad } from './module.f.js';
|
|
3
3
|
export const proof = {
|
|
4
4
|
shouldLoad: () => {
|
|
5
|
+
// Every FunctionalScript extension is loaded whatever the file name.
|
|
5
6
|
assert(shouldLoad('foo.f.ts'));
|
|
7
|
+
assert(shouldLoad('foo.f.mts'));
|
|
6
8
|
assert(shouldLoad('bar.f.js'));
|
|
9
|
+
assert(shouldLoad('module.f.mjs'));
|
|
10
|
+
assert(shouldLoad('proof.f.mts'));
|
|
11
|
+
assert(shouldLoad('proof.f.mjs'));
|
|
12
|
+
// Every impure JS/TS extension is loaded only under the `proof` name.
|
|
7
13
|
assert(shouldLoad('proof.ts'));
|
|
8
|
-
assert(shouldLoad('proof.js'));
|
|
9
14
|
assert(shouldLoad('proof.mts'));
|
|
15
|
+
assert(shouldLoad('proof.js'));
|
|
10
16
|
assert(shouldLoad('proof.mjs'));
|
|
11
17
|
assert(!shouldLoad('module.ts'));
|
|
18
|
+
assert(!shouldLoad('module.mts'));
|
|
19
|
+
assert(!shouldLoad('module.js'));
|
|
20
|
+
assert(!shouldLoad('module.mjs'));
|
|
12
21
|
assert(!shouldLoad('readme.md'));
|
|
13
22
|
},
|
|
14
23
|
shouldPass: () => ({
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
import { error, ok } from '../../types/result/module.f.js';
|
|
7
|
-
import { fold,
|
|
7
|
+
import { fold, next, toArray, length, concat } from '../../types/list/module.f.js';
|
|
8
8
|
import { setReplace, at } from '../../types/ordered_map/module.f.js';
|
|
9
9
|
import { fromMap } from '../../types/object/module.f.js';
|
|
10
10
|
import { assertEq } from '../../asserts/module.f.js';
|
|
@@ -164,9 +164,19 @@ const startArray = state => {
|
|
|
164
164
|
const newStack = state.top === null ? null : { first: state.top, tail: state.stack };
|
|
165
165
|
return { ...state, valueState: '[', top: ['array', null], stack: newStack };
|
|
166
166
|
};
|
|
167
|
+
// Pops the enclosing container off `stack`. `next` is forced here rather than
|
|
168
|
+
// left as a `drop(1)` thunk: the stack is written only by startArray/startObject,
|
|
169
|
+
// always as a literal cons, and a lazy pop leaves one unforced thunk per closed
|
|
170
|
+
// container — a chain that overflows the call stack when it is finally forced.
|
|
171
|
+
const popStack = state => {
|
|
172
|
+
const ne = next(state.stack);
|
|
173
|
+
return ne === null
|
|
174
|
+
? { ...state, valueState: '', top: null, stack: null }
|
|
175
|
+
: { ...state, valueState: '', top: ne.first, stack: ne.tail };
|
|
176
|
+
};
|
|
167
177
|
const endArray = state => {
|
|
168
178
|
const top = state.top;
|
|
169
|
-
const newState =
|
|
179
|
+
const newState = popStack(state);
|
|
170
180
|
if (top !== null && top[0] === 'array') {
|
|
171
181
|
const array = ['array', toArray(top[1])];
|
|
172
182
|
return pushValue(newState)(array);
|
|
@@ -179,7 +189,7 @@ const startObject = state => {
|
|
|
179
189
|
};
|
|
180
190
|
const endObject = state => {
|
|
181
191
|
const obj = state?.top !== null && state?.top[0] === 'object' ? fromMap(state.top[1]) : null;
|
|
182
|
-
const newState =
|
|
192
|
+
const newState = popStack(state);
|
|
183
193
|
return pushValue(newState)(obj);
|
|
184
194
|
};
|
|
185
195
|
const tokenToValue = token => {
|