functionalscript 0.41.0 → 0.43.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 (60) hide show
  1. package/fjs/bnf/data/module.f.d.ts +2 -2
  2. package/fjs/bnf/ll1/module.f.d.ts +1 -1
  3. package/fjs/bnf/module.f.d.ts +1 -1
  4. package/fjs/cas/evo/module.f.d.ts +1 -1
  5. package/fjs/ci/config/module.f.d.ts +3 -3
  6. package/fjs/ci/config/module.f.js +3 -3
  7. package/fjs/ci/deno/module.f.d.ts +8 -0
  8. package/fjs/ci/deno/module.f.js +9 -1
  9. package/fjs/ci/deno/proof.f.d.ts +5 -0
  10. package/fjs/ci/deno/proof.f.js +26 -0
  11. package/fjs/ci/proof.f.js +3 -2
  12. package/fjs/dev/module.f.d.ts +16 -5
  13. package/fjs/dev/module.f.js +38 -7
  14. package/fjs/dev/package_json/module.f.js +7 -4
  15. package/fjs/dev/proof.f.js +10 -1
  16. package/fjs/djs/parser/module.f.js +13 -3
  17. package/fjs/djs/parser/proof.f.d.ts +1 -0
  18. package/fjs/djs/parser/proof.f.js +26 -0
  19. package/fjs/djs/tokenizer/proof.f.js +11 -4
  20. package/fjs/effects/node/module.f.d.ts +2 -2
  21. package/fjs/emergent_testing/proof.f.js +20 -3
  22. package/fjs/fsm/module.f.d.ts +1 -1
  23. package/fjs/js/tokenizer/proof.f.js +7 -0
  24. package/fjs/mcp/cas/module.f.js +4 -1
  25. package/fjs/mcp/evo/proof.f.js +6 -1
  26. package/fjs/mcp/proof.f.js +37 -18
  27. package/fjs/media/html/module.f.d.ts +1 -1
  28. package/fjs/media/json/module.f.d.ts +15 -1
  29. package/fjs/media/json/module.f.js +17 -1
  30. package/fjs/media/json/parser/module.f.js +13 -3
  31. package/fjs/media/json/parser/proof.f.d.ts +1 -0
  32. package/fjs/media/json/parser/proof.f.js +37 -1
  33. package/fjs/media/json/proof.f.d.ts +4 -0
  34. package/fjs/media/json/proof.f.js +14 -2
  35. package/fjs/media/json/serializer/module.f.d.ts +4 -0
  36. package/fjs/media/json/serializer/module.f.js +39 -2
  37. package/fjs/media/json/serializer/proof.f.js +32 -8
  38. package/fjs/media/module.f.d.ts +92 -10
  39. package/fjs/media/module.f.js +73 -8
  40. package/fjs/media/proof.f.d.ts +10 -0
  41. package/fjs/media/proof.f.js +79 -10
  42. package/fjs/media/revision/module.f.d.ts +9 -1
  43. package/fjs/media/revision/module.f.js +14 -5
  44. package/fjs/protocol/mcp/stdio/module.f.js +2 -5
  45. package/fjs/text/utf16/module.f.d.ts +24 -0
  46. package/fjs/text/utf16/module.f.js +24 -0
  47. package/fjs/text/utf16/proof.f.d.ts +1 -0
  48. package/fjs/text/utf16/proof.f.js +10 -1
  49. package/fjs/types/object/module.f.d.ts +31 -13
  50. package/fjs/types/object/module.f.js +4 -3
  51. package/fjs/types/object/proof.f.d.ts +0 -14
  52. package/fjs/types/object/proof.f.js +0 -4
  53. package/fjs/types/rtti/common/module.f.d.ts +2 -2
  54. package/fjs/types/rtti/module.f.d.ts +1 -1
  55. package/fjs/types/rtti/ts/module.f.d.ts +1 -1
  56. package/fjs/types/rtti/validate/proof.f.d.ts +2 -0
  57. package/fjs/types/rtti/validate/proof.f.js +58 -0
  58. package/nanvm-lib/tests/proof.f.d.ts +9 -0
  59. package/nanvm-lib/tests/proof.f.js +58 -7
  60. package/package.json +2 -2
@@ -11,7 +11,7 @@ export type TerminalRange = number;
11
11
  */
12
12
  export type Sequence = readonly string[];
13
13
  /** A variant of rule names. */
14
- export type Variant = StringMap<string, string>;
14
+ export type Variant = StringMap<string>;
15
15
  /**
16
16
  * Grammar rule definition.
17
17
  *
@@ -29,7 +29,7 @@ export type RuleSet = Readonly<Record<string, Rule>>;
29
29
  * variant branch.
30
30
  */
31
31
  export type EmptyTag = string | true | undefined;
32
- type EmptyTagMap = StringMap<string, EmptyTag>;
32
+ type EmptyTagMap = StringMap<EmptyTag>;
33
33
  /**
34
34
  * Computes, for every rule in the set, whether it can match empty input, by
35
35
  * the standard nullable-set fixpoint: a sequence is nullable iff all of its
@@ -26,7 +26,7 @@ type DispatchRuleCollection = {
26
26
  readonly tag: string | undefined;
27
27
  readonly rules: DispatchRuleOrName[];
28
28
  };
29
- type DispatchMap = StringMap<string, DispatchRule>;
29
+ type DispatchMap = StringMap<DispatchRule>;
30
30
  /**
31
31
  * Represents a parsed AST sequence.
32
32
  */
@@ -89,7 +89,7 @@ export declare const range: (ab: string) => TerminalRange;
89
89
  /**
90
90
  * A set of terminal ranges compatible with the `Variant` rule.
91
91
  */
92
- export type RangeVariant = StringMap<string, TerminalRange>;
92
+ export type RangeVariant = StringMap<TerminalRange>;
93
93
  export declare const remove: (range: TerminalRange, v: RangeVariant) => RangeVariant;
94
94
  /**
95
95
  * Returns the complement set of the provided ranges over {@link fullRange}.
@@ -119,7 +119,7 @@ export type SubjectState = {
119
119
  };
120
120
  /** In-memory index: subject → its {@link SubjectState}. */
121
121
  export type Cache = {
122
- readonly bySubject: StringMap<string, SubjectState>;
122
+ readonly bySubject: StringMap<SubjectState>;
123
123
  };
124
124
  /** A cache with no known subjects yet — the starting point for {@link buildCache}. */
125
125
  export declare const emptyCache: Cache;
@@ -18,17 +18,17 @@ export declare const images: {
18
18
  readonly arm: 'windows-11-arm';
19
19
  };
20
20
  };
21
- export declare const functionalscript: '0.39.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.5.1';
25
+ readonly default: '26.6.0';
26
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: '6d65bfc1bcef2ef39a239d38e577e92a89fb0f07';
31
+ readonly commit: '04607e1165ac22c5fde6dcc54c9e0b3c0487c555';
32
32
  };
33
33
  export declare const wasmtime = "47.0.3";
34
34
  export declare const wasmer = "7.2.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.39.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,7 +36,7 @@ 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.5.1',
39
+ default: '26.6.0',
40
40
  node22: '22.23.2',
41
41
  node24: '24.18.0',
42
42
  };
@@ -47,7 +47,7 @@ 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: '6d65bfc1bcef2ef39a239d38e577e92a89fb0f07',
50
+ commit: '04607e1165ac22c5fde6dcc54c9e0b3c0487c555',
51
51
  };
52
52
  // https://github.com/bytecodealliance/wasmtime/releases
53
53
  export const wasmtime = '47.0.3';
@@ -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[];
@@ -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='.*module\\.f\\.ts'` }),
27
+ test({ run: `${denoTest} --coverage && deno coverage --include='${coverageInclude}'` }),
20
28
  ];
@@ -0,0 +1,5 @@
1
+ export declare const proof: {
2
+ coverageInclude: () => void;
3
+ coverageStep: () => void;
4
+ installsPinnedVersion: () => void;
5
+ };
@@ -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
+ };
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("deno test --allow-read --allow-env --allow-sys --coverage && deno coverage --include='.*module\\.f\\.ts'")(gha), 'expected limited-permission deno coverage');
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');
@@ -10,14 +10,23 @@ export type Module = {
10
10
  readonly proof?: unknown;
11
11
  readonly [k: string]: unknown;
12
12
  };
13
- export type ModuleMap = StringMap<string, Module>;
13
+ export type ModuleMap = StringMap<Module>;
14
14
  /**
15
15
  * Returns `true` if the file should be loaded for proof discovery.
16
16
  *
17
- * All FunctionalScript modules (`.f.ts` / `.f.js`) are safe to bulk-load by
18
- * construction — they have no import side effects. For vanilla TS/JS the
19
- * load gate stays opt-in by filename: any file ending in `proof.ts`,
20
- * `proof.js`, `proof.mts`, or `proof.mjs` is included.
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
  };
@@ -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
- * All FunctionalScript modules (`.f.ts` / `.f.js`) are safe to bulk-load by
17
- * construction — they have no import side effects. For vanilla TS/JS the
18
- * load gate stays opt-in by filename: any file ending in `proof.ts`,
19
- * `proof.js`, `proof.mts`, or `proof.mjs` is included.
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.js') ||
25
- s.endsWith('proof.ts') || s.endsWith('proof.js') ||
26
- s.endsWith('proof.mts') || s.endsWith('proof.mjs');
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
- import { parse as jsonParse } from '../../media/json/parser/module.f.js';
2
- import { tokenize as jsonTokenize } from '../../media/json/tokenizer/module.f.js';
3
- import { stringToList } from '../../text/utf16/module.f.js';
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') {
@@ -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, first, drop, toArray, length, concat } from '../../types/list/module.f.js';
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 = { ...state, valueState: '', top: first(null)(state.stack), stack: drop(1)(state.stack) };
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 = { ...state, valueState: '', top: first(null)(state.stack), stack: drop(1)(state.stack) };
192
+ const newState = popStack(state);
183
193
  return pushValue(newState)(obj);
184
194
  };
185
195
  const tokenToValue = token => {
@@ -10,4 +10,5 @@ export declare const proof: {
10
10
  validWithArgs: (() => void)[];
11
11
  invalidWithArgs: (() => void)[];
12
12
  comments: (() => void)[];
13
+ containerStackCost: (() => void)[];
13
14
  };
@@ -526,5 +526,31 @@ export const proof = {
526
526
  const result = stringifyDjsModule(obj[1]);
527
527
  assertEq(result, '[[],[null]]');
528
528
  },
529
+ ],
530
+ // Regression: closing a container popped the parser stack with `drop(1)`,
531
+ // which is lazy, so every closed container left one unforced thunk wrapping
532
+ // the stack. The chain was forced only at the end, costing a call-stack
533
+ // frame per container and overflowing at roughly 5000 of them — whether
534
+ // nested or flat siblings — while primitives were unbounded. `fjs/media/json`
535
+ // carried the same defect and was fixed in the same way.
536
+ containerStackCost: [
537
+ () => {
538
+ const [tag, value] = parseFromTokens(tokenizeString(`export default [${Array(20000).fill('{}').join(',')}]`));
539
+ assert(tag === 'ok', tag);
540
+ assertEq(value[1].length, 1);
541
+ },
542
+ () => {
543
+ const [tag] = parseFromTokens(tokenizeString(`export default [${Array(20000).fill('[]').join(',')}]`));
544
+ assert(tag === 'ok', tag);
545
+ },
546
+ () => {
547
+ const [tag] = parseFromTokens(tokenizeString('export default ' + '['.repeat(20000) + ']'.repeat(20000)));
548
+ assert(tag === 'ok', tag);
549
+ },
550
+ () => {
551
+ // primitives never touched the stack — the baseline that always passed
552
+ const [tag] = parseFromTokens(tokenizeString(`export default [${Array.from({ length: 20000 }, (_, i) => i).join(',')}]`));
553
+ assert(tag === 'ok', tag);
554
+ },
529
555
  ]
530
556
  };
@@ -1167,12 +1167,19 @@ export const proof = {
1167
1167
  () => {
1168
1168
  // many distinct short tokens, well past the ~1000-1500 token crash threshold
1169
1169
  // the old whole-file recursive match hit for this shape
1170
- const src = Array.from({ length: 3000 }, (_, i) => `a${i % 10}`).join(' ');
1170
+ const ids = Array.from({ length: 3000 }, (_, i) => `a${i % 10}`);
1171
+ const src = ids.join(' ');
1171
1172
  const result = tokenizeString(src);
1172
1173
  assert(result !== 'error', result);
1173
- const parsed = JSON.parse(result);
1174
- // 3000 ids + 2999 separating ws + a trailing eof token
1175
- assertEq(parsed.length, 3000 + 2999 + 1);
1174
+ // `tokenizeString` runs its own pipeline (`getTokensFromAstRule` +
1175
+ // filter/scan + `stringify`), not `tokenizeJs`'s, so only its dump
1176
+ // covers it. Compared whole rather than counted: at this size the
1177
+ // count was the only thing the old JSON round-trip could check,
1178
+ // while the reconstructed literal also pins every token's content.
1179
+ assertEq(result, `[${ids.map(v => `{"kind":"id","value":"${v}"}`).join(',{"kind":"ws"},')},{"kind":"eof"}]`);
1180
+ // The token list must agree with the dump: 3000 ids + 2999
1181
+ // separating ws + a trailing eof token.
1182
+ assertEq(toArray(tokenizeJs(stringToList(src))('a.js')).length, 3000 + 2999 + 1);
1176
1183
  },
1177
1184
  () => {
1178
1185
  // same many-short-tokens shape through the metadata-aware entry point
@@ -109,7 +109,7 @@ export type Stat = readonly ['stat', (path: string) => IoResult<FileStat>];
109
109
  export declare const stat: Func<Stat>;
110
110
  export type Fs = Mkdir | ReadFile | ReadBytes | Readdir | WriteFile | Rm | Rename | Exec | Access | CreateExclusive | WriteBytes | Stat;
111
111
  export type Server = Nominal<'server', `160855c4f69310fece3273c1853ac32de43dee1eb41bf59d821917f8eebe9272`, unknown>;
112
- export type Headers = StringMap<string, string>;
112
+ export type Headers = StringMap<string>;
113
113
  export type IncomingMessage = {
114
114
  readonly method: string;
115
115
  readonly url: string;
@@ -129,7 +129,7 @@ export declare const listen: Func<Listen>;
129
129
  export type Http = CreateServer | Listen;
130
130
  export type Forever = ['forever', () => never];
131
131
  export declare const forever: Func<Forever>;
132
- export type Module = StringMap<string, unknown>;
132
+ export type Module = StringMap<unknown>;
133
133
  export type Import = ['import', (path: string) => IoResult<Module>];
134
134
  export declare const import_: Func<Import>;
135
135
  /** Named output streams accepted by the `Write` effect. */
@@ -5,10 +5,27 @@ import { assert, assertEq } from '../asserts/module.f.js';
5
5
  import { testAll, defaultReporter, fmtPath, fmtTerm, fmtImport, ghEscape, isInteger, isIdentifier, registerModule, parseTestSet, defaultTest, } from './module.f.js';
6
6
  import { run as mockRun } from '../effects/mock/module.f.js';
7
7
  import { shouldLoad } from '../dev/module.f.js';
8
- const writeEvent = (event) => log(JSON.stringify(event));
9
- const parseEvents = (stdout) => stdout === '' ? [] : stdout.trimEnd().split('\n').map(line => JSON.parse(line));
8
+ import { parse as parseJson } from '../media/json/module.f.js';
9
+ import { array, number as rttiNumber, or, string as rttiString } from '../types/rtti/module.f.js';
10
+ import { parse as rttiParse } from '../types/rtti/parse/module.f.js';
11
+ import { unwrap } from '../types/result/module.f.js';
12
+ /**
13
+ * The mock reporter's stdout lines. A schema rather than a hand-written type:
14
+ * the events round-trip through JSON, and this is what turns a line back into
15
+ * a typed value without an `as` cast — a shape change fails at the parse
16
+ * instead of silently at an assertion.
17
+ *
18
+ * `Reporter.result` also receives the test's `SandboxResult`, which is not
19
+ * written: no assertion here reads it, and its `undefined` payloads have no
20
+ * JSON representation to round-trip through anyway.
21
+ */
22
+ const event = or(['result', rttiString, array(or(rttiString, null))], ['summary', rttiNumber, rttiNumber, rttiNumber]);
23
+ const parseEvent = rttiParse(event);
24
+ const writeEvent = (e) => log(JSON.stringify(e));
25
+ const parseEvents = (stdout) => stdout === '' ? [] : stdout.trimEnd().split('\n')
26
+ .map(line => unwrap(parseEvent(unwrap(parseJson(line)))));
10
27
  const makeReporter = () => ({
11
- result: (file, path, r, _throws) => writeEvent(['result', file, [...path], r]),
28
+ result: (file, path, _r, _throws) => writeEvent(['result', file, [...path]]),
12
29
  summary: (pass, fail, time) => writeEvent(['summary', pass, fail, time]),
13
30
  test: defaultTest,
14
31
  });
@@ -9,7 +9,7 @@ import { type ByteSet } from '../types/byte_set/module.f.ts';
9
9
  import { type RangeMapArray } from '../types/range_map/module.f.ts';
10
10
  type Rule = readonly [string, ByteSet, string];
11
11
  export type Grammar = List<Rule>;
12
- type Dfa = StringMap<string, RangeMapArray<string>>;
12
+ type Dfa = StringMap<RangeMapArray<string>>;
13
13
  export declare const toRange: (s: string) => ByteSet;
14
14
  export declare const toUnion: (s: string) => ByteSet;
15
15
  export declare const dfa: (grammar: Grammar) => Dfa;
@@ -357,6 +357,13 @@ export const proof = {
357
357
  throw result;
358
358
  }
359
359
  },
360
+ () => {
361
+ // a second "." after the fraction has started (covers fullStopToToken's default branch)
362
+ const result = stringify(tokenizeString('1..'));
363
+ if (result !== '[{"kind":"error","message":"invalid number"},{"kind":"eof"}]') {
364
+ throw result;
365
+ }
366
+ },
360
367
  () => {
361
368
  const result = stringify(tokenizeString('0e1'));
362
369
  if (result !== '[{"bf":[0n,1],"kind":"number","value":"0e1"},{"kind":"eof"}]') {
@@ -109,7 +109,8 @@ import { cBase32ToVec, vecToCBase32 } from '../../basen/cbase32/module.f.js';
109
109
  import { decode as base64Decode, encode as base64Encode } from '../../basen/base64/module.f.js';
110
110
  import { tryUtf8 } from '../../text/module.f.js';
111
111
  import { detectStream } from '../../media/type/module.f.js';
112
- import { detect as detectDialect } from '../../media/module.f.js';
112
+ import { detect } from '../../media/module.f.js';
113
+ import { revisionDialect } from '../../media/revision/module.f.js';
113
114
  import { maxLengthBytes } from '../../types/bit_vec/module.f.js';
114
115
  import { ok } from '../../types/result/module.f.js';
115
116
  import { toolEntry, errorResult, okResult, } from '../../protocol/mcp/module.f.js';
@@ -134,6 +135,8 @@ export const casGetArgs = {
134
135
  export const casListArgs = {};
135
136
  // ── Tool registry ──────────────────────────────────────────────────────────────
136
137
  const toJson = stringify(identity);
138
+ /** The dialect-aware classifier, bound to the dialects this server recognizes. */
139
+ const detectDialect = detect([revisionDialect]);
137
140
  /**
138
141
  * Registry of all CAS tools, bound to `home` and the Evo cache at
139
142
  * `cacheKey`. `cas_add` is the only tool that writes, so it is the only one
@@ -6,7 +6,12 @@ import { vec8 } from '../../types/bit_vec/module.f.js';
6
6
  import { vecToCBase32 } from '../../basen/cbase32/module.f.js';
7
7
  import { initEvo, evo } from '../../cas/evo/module.f.js';
8
8
  import { evoToolRegistry } from './module.f.js';
9
+ import { parse as parseJson } from '../../media/json/module.f.js';
10
+ import { array, string as rttiString } from '../../types/rtti/module.f.js';
11
+ import { parse as rttiParse } from '../../types/rtti/parse/module.f.js';
12
+ import { unwrap } from '../../types/result/module.f.js';
9
13
  const home = '/home/user';
14
+ const parseSubjects = rttiParse(array(rttiString));
10
15
  const findEntry = (registry, name) => {
11
16
  const entry = registry.find(e => e.name === name);
12
17
  assert(entry !== undefined, ['missing tool entry', name]);
@@ -52,7 +57,7 @@ export const proof = {
52
57
  const entry = findEntry(evoToolRegistry(e), 'evo_list');
53
58
  const [, result] = virtual(state2)(entry.handle({}));
54
59
  assert(!result.isError);
55
- const subjects = JSON.parse(textOf(result));
60
+ const subjects = unwrap(parseSubjects(unwrap(parseJson(textOf(result)))));
56
61
  assertEq(subjects.length, 2);
57
62
  assert(subjects.includes('line one\nline two'), ['unexpected subjects', subjects]);
58
63
  assert(subjects.includes(''), ['unexpected subjects', subjects]);