functionalscript 0.29.0 → 0.30.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.
@@ -1,2 +1,2 @@
1
- import { type Architecture, type MetaStep, type Os } from '../common/module.f.ts';
2
- export declare const bunSteps: (extra: readonly MetaStep[]) => (v: Os, a: Architecture) => readonly MetaStep[];
1
+ import { type MetaStep } from '../common/module.f.ts';
2
+ export declare const bunSteps: (version: string) => readonly MetaStep[];
@@ -1,22 +1,15 @@
1
1
  /**
2
- * CI step builder for Bun: installs the pinned Bun version (with a PowerShell
3
- * fallback for Windows ARM) and runs `bun install` and `bun test`.
2
+ * CI step builder for Bun: installs the pinned Bun version and runs the
3
+ * FunctionalScript package smoke test plus Bun coverage in one canonical job.
4
4
  *
5
5
  * @module
6
6
  */
7
7
  import { bun } from "../config/module.f.js";
8
8
  import { clean, install, test, uses } from "../common/module.f.js";
9
- const installOnWindowsArm = ({ def, name, path }) => (v) => (a) => install(v === 'windows' && a === 'arm'
10
- ? { run: `irm ${path}/install.ps1 | iex; "$env:USERPROFILE\\.${name}\\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append` }
11
- : def);
12
- const installBun = installOnWindowsArm({
13
- def: uses('oven-sh/setup-bun', { 'bun-version': bun }),
14
- name: 'bun',
15
- path: 'bun.sh',
16
- });
17
- export const bunSteps = (extra) => (v, a) => clean([
18
- installBun(v)(a),
9
+ export const bunSteps = (version) => clean([
10
+ install(uses('oven-sh/setup-bun', { 'bun-version': bun })),
11
+ install({ run: `bun install -g functionalscript@${version}` }),
19
12
  test({ run: 'bun install --frozen-lockfile' }),
20
- test({ run: 'bun test --timeout 20000' }),
21
- ...extra,
13
+ test({ run: `bunx functionalscript@${version} t` }),
14
+ test({ run: 'bun test --coverage' }),
22
15
  ]);
@@ -39,6 +39,7 @@ export declare const gitHubActionSchema: {
39
39
  readonly pull_request: import("../../types/rtti/module.f.ts").Or<readonly [{}, undefined]>;
40
40
  readonly merge_group: import("../../types/rtti/module.f.ts").Or<readonly [{}, undefined]>;
41
41
  };
42
+ readonly permissions: import("../../types/rtti/module.f.ts").Type1<"record", import("../../types/rtti/module.f.ts").String>;
42
43
  readonly jobs: import("../../types/rtti/module.f.ts").Type1<"record", {
43
44
  readonly 'runs-on': import("../../types/rtti/module.f.ts").String;
44
45
  readonly steps: import("../../types/rtti/module.f.ts").Type1<"array", {
@@ -58,6 +59,7 @@ export declare const parseGitHubAction: import("../../types/rtti/parse/module.f.
58
59
  readonly pull_request: import("../../types/rtti/module.f.ts").Or<readonly [{}, undefined]>;
59
60
  readonly merge_group: import("../../types/rtti/module.f.ts").Or<readonly [{}, undefined]>;
60
61
  };
62
+ readonly permissions: import("../../types/rtti/module.f.ts").Type1<"record", import("../../types/rtti/module.f.ts").String>;
61
63
  readonly jobs: import("../../types/rtti/module.f.ts").Type1<"record", {
62
64
  readonly 'runs-on': import("../../types/rtti/module.f.ts").String;
63
65
  readonly steps: import("../../types/rtti/module.f.ts").Type1<"array", {
@@ -84,4 +86,4 @@ export declare const test: (step: Step) => MetaStep;
84
86
  export declare const clean: (steps: readonly MetaStep[]) => readonly MetaStep[];
85
87
  export declare const toSteps: (m: readonly MetaStep[]) => readonly Step[];
86
88
  export declare const ubuntu: (ms: readonly MetaStep[]) => Job;
87
- export declare const findTgz: (v: Os) => "(Get-ChildItem *.tgz).FullName" | "./*.tgz";
89
+ export declare const ubuntuArm: (ms: readonly MetaStep[]) => Job;
@@ -27,6 +27,7 @@ export const gitHubActionSchema = {
27
27
  pull_request: option({}),
28
28
  merge_group: option({})
29
29
  },
30
+ permissions: record(string),
30
31
  jobs: jobsSchema
31
32
  };
32
33
  export const parseGitHubAction = rttiParse(gitHubActionSchema);
@@ -47,7 +48,10 @@ export const toSteps = (m) => {
47
48
  const targets = m.flatMap(v => v.type === 'rust' && v.target !== undefined ? [v.target] : []).join(',');
48
49
  return [
49
50
  ...(aptGet !== '' ? [{ run: `sudo apt-get update && sudo apt-get install -y ${aptGet}` }] : []),
50
- ...(needRust ? [uses('dtolnay/rust-toolchain', { components: 'rustfmt,clippy', targets })] : []),
51
+ ...(needRust ? [uses('dtolnay/rust-toolchain', {
52
+ components: 'rustfmt,clippy',
53
+ ...(targets === '' ? {} : { targets }),
54
+ })] : []),
51
55
  ...filter('install'),
52
56
  uses('actions/checkout'),
53
57
  ...filter('test'),
@@ -57,4 +61,7 @@ export const ubuntu = (ms) => ({
57
61
  'runs-on': images.ubuntu.intel,
58
62
  steps: toSteps(ms)
59
63
  });
60
- export const findTgz = (v) => v === 'windows' ? '(Get-ChildItem *.tgz).FullName' : './*.tgz';
64
+ export const ubuntuArm = (ms) => ({
65
+ 'runs-on': images.ubuntu.arm,
66
+ steps: toSteps(ms)
67
+ });
@@ -19,12 +19,14 @@ export declare const images: {
19
19
  readonly arm: "windows-11-arm";
20
20
  };
21
21
  };
22
+ export declare const functionalscript: "0.29.1";
22
23
  export declare const bun = "1.3.14";
23
24
  export declare const deno = "2.8.2";
24
25
  export declare const playwright = "1.60.0";
25
26
  export declare const node: {
26
27
  readonly default: "26.3.0";
27
- readonly others: readonly ["24.16.0"];
28
+ readonly node22: "22.22.2";
29
+ readonly node24: "24.16.0";
28
30
  };
29
31
  export declare const wasmtime = "45.0.0";
30
32
  export declare const wasmer = "7.1.0";
@@ -20,6 +20,11 @@ export const images = {
20
20
  arm: 'windows-11-arm',
21
21
  }
22
22
  };
23
+ // Bootstrap package version used by generated smoke tests. Keep this on a
24
+ // published FunctionalScript release; do not tie it to package.json's current
25
+ // in-repo version. A separate maintenance job advances this pin.
26
+ // https://www.npmjs.com/package/functionalscript
27
+ export const functionalscript = '0.29.1';
23
28
  // https://bun.sh/
24
29
  export const bun = '1.3.14';
25
30
  // https://deno.com/
@@ -29,7 +34,8 @@ export const playwright = '1.60.0';
29
34
  // https://nodejs.org/en/download
30
35
  export const node = {
31
36
  default: '26.3.0',
32
- others: ['24.16.0'],
37
+ node22: '22.22.2',
38
+ node24: '24.16.0',
33
39
  };
34
40
  // https://github.com/bytecodealliance/wasmtime/releases
35
41
  export const wasmtime = '45.0.0';
@@ -1,2 +1,2 @@
1
1
  import { type MetaStep } from '../common/module.f.ts';
2
- export declare const denoSteps: (extra: readonly MetaStep[]) => readonly MetaStep[];
2
+ export declare const denoSteps: (version: string) => readonly MetaStep[];
@@ -1,14 +1,16 @@
1
1
  /**
2
- * CI step builder for Deno: installs the pinned Deno version and runs
3
- * `deno install` followed by `deno task test`.
2
+ * CI step builder for Deno: installs the pinned Deno version and runs the
3
+ * FunctionalScript package smoke test plus Deno coverage in one canonical job.
4
4
  *
5
5
  * @module
6
6
  */
7
7
  import { deno } from "../config/module.f.js";
8
8
  import { clean, install, test, uses } from "../common/module.f.js";
9
- export const denoSteps = (extra) => clean([
9
+ const denoTest = 'deno test --allow-read --allow-env';
10
+ export const denoSteps = (version) => clean([
10
11
  install(uses('denoland/setup-deno', { 'deno-version': deno })),
12
+ install({ run: `deno install -g -A npm:functionalscript@${version}` }),
11
13
  test({ run: 'deno install --frozen' }),
12
- test({ run: 'deno task test' }),
13
- ...extra,
14
+ test({ run: `deno run -A npm:functionalscript@${version} t` }),
15
+ test({ run: `${denoTest} --coverage && deno coverage --include='.*module\\.f\\.ts'` }),
14
16
  ]);
@@ -3,8 +3,6 @@ import { type NodeOp } from '../effects/node/module.f.ts';
3
3
  import { type MetaStep, type Os } from './common/module.f.ts';
4
4
  export type Setup = {
5
5
  readonly nodeExtra: (os: Os) => readonly MetaStep[];
6
- readonly denoExtra: readonly MetaStep[];
7
- readonly bunExtra: readonly MetaStep[];
8
6
  };
9
- export declare const ci: ({ nodeExtra, denoExtra, bunExtra }: Setup) => Effect<NodeOp, number>;
7
+ export declare const ci: ({ nodeExtra }: Setup) => Effect<NodeOp, number>;
10
8
  export declare const main: () => Effect<NodeOp, number>;
package/fs/ci/module.f.js CHANGED
@@ -3,48 +3,38 @@
3
3
  *
4
4
  * @module
5
5
  */
6
- import { utf8, utf8ToString } from "../text/module.f.js";
7
- import { validatePackageJsonText } from "../dev/package_json/module.f.js";
6
+ import { utf8 } from "../text/module.f.js";
8
7
  import { pure } from "../effects/module.f.js";
9
- import { access, readFile, writeFile } from "../effects/node/module.f.js";
10
- import { images } from "./config/module.f.js";
11
- import { architecture, findTgz, os, test, toSteps } from "./common/module.f.js";
12
- import { rustSteps } from "./rust/module.f.js";
13
- import { nodeMainSteps, nodeVersions } from "./node/module.f.js";
8
+ import { access, writeFile } from "../effects/node/module.f.js";
9
+ import { functionalscript, images } from "./config/module.f.js";
10
+ import { architecture, os, toSteps, ubuntuArm } from "./common/module.f.js";
11
+ import { rustPlatformSteps, rustWasmSteps } from "./rust/module.f.js";
12
+ import { nodeMainSteps, nodeVersionJobs } from "./node/module.f.js";
14
13
  import { playwrightJob } from "./playwright/module.f.js";
15
14
  import { bunSteps } from "./bun/module.f.js";
16
15
  import { denoSteps } from "./deno/module.f.js";
17
- const job = (rust, nodeExtra, denoExtra, bunExtra) => (o) => (a) => {
16
+ const job = (rust, nodeExtra) => (o) => (a) => {
18
17
  const id = `${o}-${a}`;
19
18
  const image = images[o][a];
20
19
  const result = [
21
- ...(rust ? rustSteps(o, a) : []),
22
- ...nodeMainSteps(nodeExtra),
23
- ...denoSteps(denoExtra),
24
- ...bunSteps(bunExtra)(o, a),
20
+ ...(rust ? rustPlatformSteps(o, a) : []),
21
+ ...nodeMainSteps(functionalscript),
22
+ ...nodeExtra,
25
23
  ];
26
24
  return [id, { 'runs-on': image, steps: toSteps(result) }];
27
25
  };
28
- const functionalscriptPackageName = 'functionalscript';
29
- const fallbackPackageInfo = {
30
- name: functionalscriptPackageName,
31
- functionalscript: false,
32
- };
33
- const packageInfoFromPackageJson = ({ name }) => name !== undefined
34
- ? { name, functionalscript: name === functionalscriptPackageName }
35
- : fallbackPackageInfo;
36
- const packageInfoFromText = (text) => {
37
- const result = validatePackageJsonText(text);
38
- return result[0] === 'ok' ? packageInfoFromPackageJson(result[1]) : fallbackPackageInfo;
39
- };
40
- const readPackageInfo = readFile('package.json')
41
- .step(result => pure(result[0] === 'ok' ? packageInfoFromText(utf8ToString(result[1])) : fallbackPackageInfo));
42
- export const ci = ({ nodeExtra, denoExtra, bunExtra }) => access('Cargo.toml').step(result => {
26
+ const canonicalJobs = (rust) => ({
27
+ ...(rust ? { wasm: ubuntuArm(rustWasmSteps) } : {}),
28
+ deno: ubuntuArm(denoSteps(functionalscript)),
29
+ bun: ubuntuArm(bunSteps(functionalscript)),
30
+ ...nodeVersionJobs(functionalscript),
31
+ playwright: playwrightJob,
32
+ });
33
+ export const ci = ({ nodeExtra }) => access('Cargo.toml').step(result => {
43
34
  const rust = result[0] === 'ok';
44
35
  const jobs = {
45
- ...Object.fromEntries(os.flatMap(o => architecture.map(job(rust, nodeExtra(o), denoExtra, bunExtra)(o)))),
46
- ...nodeVersions,
47
- playwright: playwrightJob,
36
+ ...Object.fromEntries(os.flatMap(o => architecture.map(job(rust, nodeExtra(o))(o)))),
37
+ ...canonicalJobs(rust),
48
38
  };
49
39
  const gha = {
50
40
  name: 'CI',
@@ -52,31 +42,12 @@ export const ci = ({ nodeExtra, denoExtra, bunExtra }) => access('Cargo.toml').s
52
42
  pull_request: {},
53
43
  merge_group: {},
54
44
  },
45
+ permissions: {
46
+ contents: 'read',
47
+ },
55
48
  jobs,
56
49
  };
57
50
  return writeFile('.github/workflows/ci.yml', utf8(JSON.stringify(gha, null, ' ')))
58
51
  .step(() => pure(0));
59
52
  });
60
- const demoCompile = [
61
- test({ run: 'fjs compile issues/demo/data/tree.json _tree.f.js' }),
62
- test({ run: 'fjs t' }),
63
- ];
64
- const denoDemoCompile = [
65
- test({ run: 'deno task fjs compile issues/demo/data/tree.json _tree.f.js' }),
66
- test({ run: 'deno task fjs t' }),
67
- ];
68
- const bunDemoCompile = [
69
- test({ run: 'bun ./fs/fjs/module.ts t' }),
70
- ];
71
- const defaultNodeExtra = ({ name, functionalscript }) => (o) => [
72
- test({ run: 'npm pack' }),
73
- test({ run: `npm install -g ${findTgz(o)}` }),
74
- ...(functionalscript ? demoCompile : []),
75
- test({ run: `npm uninstall ${name} -g` }),
76
- ];
77
- const defaultEffect = (info) => ci({
78
- nodeExtra: defaultNodeExtra(info),
79
- denoExtra: info.functionalscript ? [...denoDemoCompile] : [],
80
- bunExtra: info.functionalscript ? bunDemoCompile : [],
81
- });
82
- export const main = () => readPackageInfo.step(defaultEffect);
53
+ export const main = () => ci({ nodeExtra: () => [] });
@@ -1,6 +1,6 @@
1
1
  import { type Jobs, type MetaStep } from '../common/module.f.ts';
2
2
  export declare const major: (v: string) => string;
3
3
  export declare const basicNode: (version: string) => (extra: readonly MetaStep[]) => readonly MetaStep[];
4
- export declare const nodeTests: (version: string) => (extra: readonly MetaStep[]) => readonly MetaStep[];
5
- export declare const nodeVersions: Jobs;
6
- export declare const nodeMainSteps: (extra: readonly MetaStep[]) => readonly MetaStep[];
4
+ export declare const platformNodeSteps: (version: string) => readonly MetaStep[];
5
+ export declare const nodeVersionJobs: (version: string) => Jobs;
6
+ export declare const nodeMainSteps: (version: string) => readonly MetaStep[];
@@ -1,11 +1,11 @@
1
1
  /**
2
- * CI step builders for Node.js: setup-node installation, common npm
3
- * install/test sequences, per-version job matrices, and the main TSGO step.
2
+ * CI step builders for Node.js: setup-node installation, platform smoke tests,
3
+ * per-version canonical jobs, and the main TSGO step.
4
4
  *
5
5
  * @module
6
6
  */
7
7
  import { node, tsgo } from "../config/module.f.js";
8
- import { clean, findTgz, install, test, ubuntu, uses } from "../common/module.f.js";
8
+ import { clean, install, test, ubuntuArm, uses } from "../common/module.f.js";
9
9
  export const major = (v) => v.split('.')[0];
10
10
  const installNode = (version) => uses('actions/setup-node', { 'node-version': version });
11
11
  const nodeInstall = (v) => [
@@ -16,28 +16,33 @@ export const basicNode = (version) => (extra) => clean([
16
16
  ...nodeInstall(version),
17
17
  ...extra,
18
18
  ]);
19
- const basicTests = [
20
- test({ run: 'npx tsc' }),
21
- test({ run: 'npm test' }),
22
- test({ run: 'node --test' }),
23
- test({ run: 'npm run cov' }),
19
+ const fjsGlobalInstall = (version) => install({ run: `npm install -g functionalscript@${version}` });
20
+ export const platformNodeSteps = (version) => [
21
+ ...nodeInstall(node.default),
22
+ fjsGlobalInstall(version),
23
+ test({ run: 'fjs t' }),
24
24
  ];
25
- export const nodeTests = (version) => (extra) => basicNode(version)([
26
- ...basicTests,
27
- ...extra,
25
+ const node22Steps = (version) => clean([
26
+ ...nodeInstall(node.node22),
27
+ fjsGlobalInstall(version),
28
+ test({ run: 'fjs t' }),
28
29
  ]);
29
- const nodeSteps = (v) => [
30
- ...nodeInstall(v),
31
- ...basicTests,
32
- ];
33
- export const nodeVersions = Object.fromEntries(node.others.map(v => [
34
- `node${major(v)}`,
35
- ubuntu(nodeSteps(v))
36
- ]));
37
- export const nodeMainSteps = (extra) => nodeTests(node.default)([
38
- // TypeScript Preview
30
+ const node24Steps = clean([
31
+ ...nodeInstall(node.node24),
32
+ test({ run: 'node --test' }),
33
+ ]);
34
+ const node26Steps = clean([
35
+ ...nodeInstall(node.default),
39
36
  install({ run: `npm install -g @typescript/native-preview@${tsgo}` }),
37
+ test({ run: 'npx tsc' }),
40
38
  test({ run: 'tsgo' }),
41
- // extra
42
- ...extra,
39
+ test({ run: 'npm run cov' }),
40
+ test({ run: 'npm pack' }),
43
41
  ]);
42
+ const nodeJob = (steps) => ubuntuArm(steps);
43
+ export const nodeVersionJobs = (version) => ({
44
+ [`node${major(node.node22)}`]: nodeJob(node22Steps(version)),
45
+ [`node${major(node.node24)}`]: nodeJob(node24Steps),
46
+ [`node${major(node.default)}`]: nodeJob(node26Steps),
47
+ });
48
+ export const nodeMainSteps = platformNodeSteps;
@@ -7,7 +7,7 @@
7
7
  import { images, node, playwright } from "../config/module.f.js";
8
8
  import { install, test, toSteps, uses } from "../common/module.f.js";
9
9
  import { basicNode } from "../node/module.f.js";
10
- const playwrightImage = images.ubuntu.intel;
10
+ const playwrightImage = images.ubuntu.arm;
11
11
  export const playwrightJob = {
12
12
  'runs-on': playwrightImage,
13
13
  steps: toSteps(basicNode(node.default)([
@@ -1,4 +1,5 @@
1
1
  export declare const proof: {
2
+ matrixShape: () => void;
2
3
  rust: () => void;
3
4
  noRust: () => void;
4
5
  extra: {
@@ -6,12 +7,9 @@ export declare const proof: {
6
7
  osSpecific: () => void;
7
8
  };
8
9
  defaultSetup: {
9
- functionalscriptDemo: () => void;
10
- otherPackageNoDemo: () => void;
11
- uninstallPackageName: () => void;
12
- malformedPackageJsonFallback: () => void;
13
- missingPackageJsonFallback: () => void;
14
- missingNameFallback: () => void;
15
- nonObjectFallback: () => void;
10
+ functionalscriptPackage: () => void;
11
+ otherPackage: () => void;
12
+ configuredPackageVersion: () => void;
13
+ missingPackageJson: () => void;
16
14
  };
17
15
  };
package/fs/ci/proof.f.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ci, main } from "./module.f.js";
2
+ import { functionalscript } from "./config/module.f.js";
2
3
  import { utf8, utf8ToString } from "../text/module.f.js";
3
4
  import { empty as emptyVec, isVec } from "../types/bit_vec/module.f.js";
4
5
  import { test, parseGitHubAction } from "./common/module.f.js";
@@ -26,7 +27,7 @@ const workflow = (state) => {
26
27
  return unwrap(parseGitHubAction(jsonParse(utf8ToString(file))));
27
28
  };
28
29
  const run = (rust, nodeExtra = () => []) => {
29
- const [state, result] = virtual(makeState(rust))(ci({ nodeExtra, denoExtra: [], bunExtra: [] }));
30
+ const [state, result] = virtual(makeState(rust))(ci({ nodeExtra }));
30
31
  assert(result === 0, result);
31
32
  return workflow(state);
32
33
  };
@@ -36,6 +37,41 @@ const runDefault = (packageJson) => {
36
37
  return workflow(state);
37
38
  };
38
39
  export const proof = {
40
+ matrixShape: () => {
41
+ const gha = run(true);
42
+ assert(Object.keys(gha.jobs).length === 13, 'expected 13 CI jobs');
43
+ assert(gha.permissions.contents === 'read', 'expected read-only contents permission');
44
+ assert(Object.keys(gha.permissions).length === 1, 'expected least-privilege workflow permissions');
45
+ assert(hasRunInJob('ubuntu-intel', 'cargo test --target i686-unknown-linux-gnu')(gha), 'expected Ubuntu Intel i686 check');
46
+ assert(hasRunInJob('ubuntu-intel', 'cargo test --target i686-unknown-linux-gnu --release')(gha), 'expected Ubuntu Intel i686 release check');
47
+ assert(hasRunInJob('ubuntu-intel', 'cargo clippy --target i686-unknown-linux-gnu -- -D warnings')(gha), 'expected Ubuntu Intel i686 lint');
48
+ assert(hasRunInJob('ubuntu-intel', 'cargo clippy --target i686-unknown-linux-gnu --release -- -D warnings')(gha), 'expected Ubuntu Intel i686 release lint');
49
+ assert(hasRunInJob('ubuntu-arm', 'cargo test --release')(gha), 'expected native platform Rust release check');
50
+ assert(hasRunInJob('ubuntu-arm', 'cargo clippy -- -D warnings')(gha), 'expected native platform Rust lint');
51
+ assert(hasRunInJob('ubuntu-arm', 'cargo clippy --release -- -D warnings')(gha), 'expected native platform Rust release lint');
52
+ assert(hasRunInJob('wasm', 'cargo test --target wasm32-wasip1 --release')(gha), 'expected target-specific WASM release check');
53
+ assert(hasRunInJob('wasm', 'cargo clippy --target wasm32-wasip1 -- -D warnings')(gha), 'expected target-specific WASM Rust lint');
54
+ assert(hasRunInJob('wasm', 'cargo clippy --target wasm32-wasip1 --release -- -D warnings')(gha), 'expected target-specific WASM release lint');
55
+ assert(hasRunInJob('node22', 'fjs t')(gha), 'expected Node 22 FunctionalScript smoke test');
56
+ assert(hasRunInJob('node26', 'npm pack')(gha), 'expected Node 26 package check');
57
+ assert(!hasRun('npm publish --dry-run')(gha), 'unexpected npm publish dry-run');
58
+ for (const id of [
59
+ 'ubuntu-intel',
60
+ 'ubuntu-arm',
61
+ 'macos-intel',
62
+ 'macos-arm',
63
+ 'windows-intel',
64
+ 'windows-arm',
65
+ 'node22',
66
+ 'node24',
67
+ 'node26',
68
+ 'playwright',
69
+ ]) {
70
+ assert(hasRunInJob(id, 'npm ci')(gha), `expected npm ci in ${id}`);
71
+ }
72
+ assert(!hasRunInJob('deno', 'npm ci')(gha), 'unexpected npm ci in deno job');
73
+ assert(!hasRunInJob('bun', 'npm ci')(gha), 'unexpected npm ci in bun job');
74
+ },
39
75
  rust: () => {
40
76
  assert(hasRun('cargo')(run(true)), 'expected Rust steps');
41
77
  },
@@ -62,46 +98,31 @@ export const proof = {
62
98
  },
63
99
  },
64
100
  defaultSetup: {
65
- functionalscriptDemo: () => {
101
+ functionalscriptPackage: () => {
66
102
  const gha = runDefault('{"name":"functionalscript"}');
67
- assert(hasRun('fjs compile issues/demo/data/tree.json _tree.f.js')(gha), 'expected fjs demo compile');
68
103
  assert(hasRun('fjs t')(gha), 'expected fjs self-test');
69
- assert(hasRun('deno task fjs compile issues/demo/data/tree.json _tree.f.js')(gha), 'expected deno demo compile');
70
- assert(hasRun('deno task fjs t')(gha), 'expected deno self-test');
71
- assert(hasRun('bun ./fs/fjs/module.ts t')(gha), 'expected bun self-test');
72
- },
73
- otherPackageNoDemo: () => {
74
- const gha = runDefault('{"name":"other-package"}');
75
- assert(!hasRun('fjs compile issues/demo/data/tree.json _tree.f.js')(gha), 'unexpected fjs demo compile');
76
- assert(!hasRun('fjs t')(gha), 'unexpected fjs self-test');
77
- assert(!hasRun('deno task fjs compile issues/demo/data/tree.json _tree.f.js')(gha), 'unexpected deno demo compile');
78
- assert(!hasRun('deno task fjs t')(gha), 'unexpected deno self-test');
79
- assert(!hasRun('bun ./fs/fjs/module.ts t')(gha), 'unexpected bun self-test');
104
+ assert(hasRun(`deno run -A npm:functionalscript@${functionalscript} t`)(gha), 'expected deno self-test');
105
+ assert(hasRun(`bunx functionalscript@${functionalscript} t`)(gha), 'expected bun self-test');
80
106
  },
81
- uninstallPackageName: () => {
107
+ otherPackage: () => {
82
108
  const gha = runDefault('{"name":"other-package"}');
83
- assert(hasRun('npm uninstall other-package -g')(gha), 'expected package-specific uninstall');
84
- assert(!hasRun('npm uninstall functionalscript -g')(gha), 'unexpected functionalscript uninstall');
109
+ assert(hasRun(`deno run -A npm:functionalscript@${functionalscript} t`)(gha), 'expected canonical deno self-test');
110
+ assert(hasRun(`bunx functionalscript@${functionalscript} t`)(gha), 'expected canonical bun self-test');
85
111
  },
86
- malformedPackageJsonFallback: () => {
87
- const gha = runDefault('{');
88
- assert(!hasRun('fjs compile issues/demo/data/tree.json _tree.f.js')(gha), 'unexpected fjs demo compile');
89
- assert(hasRun('npm uninstall functionalscript -g')(gha), 'expected fallback uninstall');
112
+ configuredPackageVersion: () => {
113
+ const gha = runDefault('{"name":"other-package","version":"1.2.3"}');
114
+ assert(hasRun(`npm install -g functionalscript@${functionalscript}`)(gha), 'expected configured-version platform install');
115
+ assert(hasRun(`deno install -g -A npm:functionalscript@${functionalscript}`)(gha), 'expected configured-version deno install cache');
116
+ assert(hasRun('deno install --frozen')(gha), 'expected deno lock install');
117
+ assert(hasRun(`deno run -A npm:functionalscript@${functionalscript} t`)(gha), 'expected configured-version deno install');
118
+ assert(hasRun("deno test --allow-read --allow-env --coverage && deno coverage --include='.*module\\.f\\.ts'")(gha), 'expected limited-permission deno coverage');
119
+ assert(hasRun(`bun install -g functionalscript@${functionalscript}`)(gha), 'expected configured-version bun cache');
120
+ assert(hasRun('bun install --frozen-lockfile')(gha), 'expected bun lock install');
121
+ assert(hasRun(`bunx functionalscript@${functionalscript} t`)(gha), 'expected configured-version bun install');
90
122
  },
91
- missingPackageJsonFallback: () => {
123
+ missingPackageJson: () => {
92
124
  const gha = runDefault();
93
- assert(!hasRun('fjs compile issues/demo/data/tree.json _tree.f.js')(gha), 'unexpected fjs demo compile');
94
- assert(hasRun('npm uninstall functionalscript -g')(gha), 'expected fallback uninstall');
95
- },
96
- missingNameFallback: () => {
97
- const gha = runDefault('{"version":"1.0.0"}');
98
- assert(!hasRun('fjs compile issues/demo/data/tree.json _tree.f.js')(gha), 'unexpected fjs demo compile');
99
- assert(hasRun('npm uninstall functionalscript -g')(gha), 'expected fallback uninstall');
100
- },
101
- nonObjectFallback: () => {
102
- const gha = runDefault('[]');
103
- assert(!hasRun('fjs compile issues/demo/data/tree.json _tree.f.js')(gha), 'unexpected fjs demo compile');
104
- assert(hasRun('npm uninstall functionalscript -g')(gha), 'expected fallback uninstall');
125
+ assert(hasRun(`npm install -g functionalscript@${functionalscript}`)(gha), 'expected configured-version install');
105
126
  },
106
127
  },
107
128
  };
@@ -1,2 +1,3 @@
1
1
  import { type Architecture, type MetaStep, type Os } from '../common/module.f.ts';
2
- export declare const rustSteps: (v: Os, a: Architecture) => readonly MetaStep[];
2
+ export declare const rustPlatformSteps: (v: Os, a: Architecture) => readonly MetaStep[];
3
+ export declare const rustWasmSteps: readonly MetaStep[];
@@ -1,50 +1,67 @@
1
1
  /**
2
- * CI step builder for the Rust crate: `cargo fmt`, `cargo clippy`, native and
3
- * `--release` test runs, plus matrix entries for WASM targets (Wasmtime,
4
- * Wasmer) and the 32-bit `i686` targets.
2
+ * CI step builder for the Rust crate: platform compatibility jobs run native
3
+ * tests and Clippy, Intel jobs also run 32-bit target tests and Clippy, and the
4
+ * canonical WASM job installs Wasmtime and Wasmer before exercising every WASM
5
+ * target.
5
6
  *
6
7
  * @module
7
8
  */
8
9
  import { wasmer, wasmtime } from "../config/module.f.js";
9
10
  import { install, test, uses } from "../common/module.f.js";
10
- const cargoTest = (target, config) => {
11
+ const cargoCommand = (tool, target, config) => {
11
12
  const to = target ? ` --target ${target}` : '';
12
13
  const co = config ? ` --config ${config}` : '';
13
- const main = `cargo test${to}${co}`;
14
+ return `cargo ${tool}${to}${co}`;
15
+ };
16
+ const cargoTest = (target, config) => test({ run: cargoCommand('test', target, config) });
17
+ const cargoClippy = (target) => test({ run: `${cargoCommand('clippy', target)} -- -D warnings` });
18
+ const cargoReleaseClippy = (target) => test({ run: `${cargoCommand('clippy', target)} --release -- -D warnings` });
19
+ const cargoTestPair = (target, config) => {
20
+ const main = cargoCommand('test', target, config);
14
21
  return [
15
- test({ run: main }),
22
+ cargoTest(target, config),
16
23
  test({ run: `${main} --release` })
17
24
  ];
18
25
  };
19
- const customTarget = (target) => [
26
+ const cargoReleaseTest = (target) => test({ run: `${cargoCommand('test', target)} --release` });
27
+ const targetChecks = (target) => [
28
+ cargoTest(target),
29
+ cargoReleaseTest(target),
30
+ cargoClippy(target),
31
+ cargoReleaseClippy(target)
32
+ ];
33
+ const rustTarget = (target) => [
20
34
  { type: 'rust', target },
21
- ...cargoTest(target)
35
+ ...targetChecks(target)
22
36
  ];
23
37
  const wasmTarget = (target) => [
24
- ...customTarget(target),
25
- ...cargoTest(target, '.cargo/config.wasmer.toml')
38
+ { type: 'rust', target },
39
+ ...targetChecks(target),
40
+ ...cargoTestPair(target, '.cargo/config.wasmer.toml')
26
41
  ];
27
42
  const i686 = (a, v) => {
28
43
  if (a === 'intel') {
29
44
  switch (v) {
30
- case 'windows': return customTarget('i686-pc-windows-msvc');
45
+ case 'windows': return rustTarget('i686-pc-windows-msvc');
31
46
  case 'ubuntu': return [
32
47
  { type: 'apt-get', package: 'libc6-dev-i386' },
33
- ...customTarget('i686-unknown-linux-gnu'),
48
+ ...rustTarget('i686-unknown-linux-gnu'),
34
49
  ];
35
50
  }
36
51
  }
37
52
  return [];
38
53
  };
39
- export const rustSteps = (v, a) => [
54
+ export const rustPlatformSteps = (v, a) => [
55
+ { type: 'rust' },
56
+ ...targetChecks(),
57
+ ...i686(a, v),
58
+ ];
59
+ export const rustWasmSteps = [
40
60
  test({ run: 'cargo fmt -- --check' }),
41
- test({ run: 'cargo clippy -- -D warnings' }),
42
- ...cargoTest(),
43
61
  install(uses('bytecodealliance/actions/wasmtime/setup', { version: wasmtime })),
44
62
  install(uses('wasmerio/setup-wasmer', { version: `v${wasmer}` })),
45
63
  ...wasmTarget('wasm32-wasip1'),
46
64
  ...wasmTarget('wasm32-wasip2'),
47
65
  ...wasmTarget('wasm32-unknown-unknown'),
48
66
  ...wasmTarget('wasm32-wasip1-threads'),
49
- ...i686(a, v),
50
67
  ];
@@ -130,6 +130,8 @@ const runModule = ({ result, test }) => (k, v) => (ts) => {
130
130
  .step(delta => pure(mergeState(ts, delta)));
131
131
  };
132
132
  const { entries } = Object;
133
+ const proofEntries = (moduleMap) => entries(moduleMap)
134
+ .flatMap(([k, v]) => v.proof !== undefined ? [[k, v.proof]] : []);
133
135
  /**
134
136
  * Runs all test modules in `moduleMap` whose names pass `isTest`, accumulates
135
137
  * pass/fail/time via `reporter`, and returns an exit code (0 = all passed,
@@ -137,8 +139,7 @@ const { entries } = Object;
137
139
  */
138
140
  export const runModuleMap = (reporter) => (moduleMap) => {
139
141
  const { summary } = reporter;
140
- const modules = entries(moduleMap)
141
- .flatMap(([k, v]) => v.proof !== undefined ? [[k, v.proof]] : []);
142
+ const modules = proofEntries(moduleMap);
142
143
  return all(...modules.map(([k, v]) => runModule(reporter)(k, v)(zero)))
143
144
  .step(m => pure(m.reduce(mergeState, zero)))
144
145
  .step(ts => summary(ts.pass, ts.fail, ts.time)
@@ -155,8 +156,7 @@ export const testAll = (reporter) => options => loadModuleMap(options.env).step(
155
156
  * `ctx`. Delegates to `registerModule` for each matching entry.
156
157
  */
157
158
  const registerModuleMap = (ctx, star) => (moduleMap) => {
158
- const modules = entries(moduleMap)
159
- .flatMap(([k, v]) => v.proof !== undefined ? [[k, v.proof]] : []);
159
+ const modules = proofEntries(moduleMap);
160
160
  if (modules.length === 0) {
161
161
  return pure(undefined);
162
162
  }
@@ -1,3 +1,4 @@
1
1
  export declare const proof: {
2
2
  range: () => void;
3
+ oneThrowsOnEmpty: () => void;
3
4
  };
@@ -1,4 +1,4 @@
1
- import { range } from "./module.f.js";
1
+ import { one, range } from "./module.f.js";
2
2
  import { stringify as jsonStringify } from "../../json/module.f.js";
3
3
  import { sort } from "../../types/object/module.f.js";
4
4
  const stringify = jsonStringify(sort);
@@ -8,5 +8,17 @@ export const proof = {
8
8
  if (r !== '[65,65]') {
9
9
  throw r;
10
10
  }
11
- }
11
+ },
12
+ oneThrowsOnEmpty: () => {
13
+ let threw = false;
14
+ try {
15
+ one('');
16
+ }
17
+ catch (_) {
18
+ threw = true;
19
+ }
20
+ if (!threw) {
21
+ throw 'expected throw';
22
+ }
23
+ },
12
24
  };
@@ -26,7 +26,13 @@ export type PrimeField = {
26
26
  readonly pow3: Unary;
27
27
  /** Reduces an arbitrary `bigint` into `[0, p)`. */
28
28
  readonly reduce: Unary;
29
- /** Euler criterion: `true` when `x` is a quadratic residue mod `p`. */
29
+ /**
30
+ * `true` when `x` is a square modulo `p`, including `0`.
31
+ *
32
+ * Nonzero values are tested with Euler's criterion:
33
+ * `x^((p - 1) / 2) === 1 (mod p)`.
34
+ * For `p === 2n`, both field elements are squares.
35
+ */
30
36
  readonly quadRes: (x: bigint) => boolean;
31
37
  };
32
38
  /**
@@ -42,12 +42,19 @@ export const prime_field = (p) => {
42
42
  const r = x % p;
43
43
  return r < 0n ? add(p)(r) : r;
44
44
  };
45
- const half = (p - 1n) / 2n;
46
- const quadRes = (x) => pow(half)(reduce(x)) === 1n;
45
+ const max = p - 1n;
46
+ // Euler's exponent is `(p - 1) / 2`; use `max`, not `p`, so `p === 2n`
47
+ // gives exponent `0n` instead of `1n`.
48
+ // 0 is a square mod p; Euler's criterion needs a separate case because 0^e = 0.
49
+ const powHalf = pow(max >> 1n);
50
+ const quadRes = (x) => {
51
+ const v = reduce(x);
52
+ return v === 0n || powHalf(v) === 1n;
53
+ };
47
54
  return {
48
55
  p,
49
56
  middle,
50
- max: p - 1n,
57
+ max,
51
58
  neg: a => a === 0n ? 0n : p - a,
52
59
  sub,
53
60
  add,
@@ -153,12 +153,25 @@ export const proof = {
153
153
  }
154
154
  },
155
155
  quadRes: () => {
156
+ if (!f.quadRes(0n)) {
157
+ throw 0n;
158
+ }
156
159
  if (!f.quadRes(1n)) {
157
160
  throw 1n;
158
161
  }
162
+ if (!f.quadRes(p)) {
163
+ throw p;
164
+ }
159
165
  if (f.quadRes(3n)) {
160
166
  throw 3n;
161
167
  }
168
+ const f2 = prime_field(2n);
169
+ if (!f2.quadRes(0n)) {
170
+ throw '0 mod 2';
171
+ }
172
+ if (!f2.quadRes(1n)) {
173
+ throw '1 mod 2';
174
+ }
162
175
  },
163
176
  modSqrt: () => {
164
177
  const root = modSqrt(f);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "functionalscript",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "**/*.js",
@@ -18,7 +18,7 @@
18
18
  "website": "npm run prepack && npm run index-html"
19
19
  },
20
20
  "engines": {
21
- "node": ">=24"
21
+ "node": ">=22"
22
22
  },
23
23
  "bin": {
24
24
  "fjs": "fs/fjs/module.js"