tslaws 0.6.3 → 0.7.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/README.md +8 -8
- package/dist/api.d.ts +10 -0
- package/dist/api.js +7 -0
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +2 -6
- package/dist/check.d.ts +2 -0
- package/dist/check.js +133 -41
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +149 -54
- package/dist/compile.d.ts +12 -0
- package/dist/compile.js +207 -6
- package/dist/coverage.d.ts +13 -1
- package/dist/coverage.js +16 -23
- package/dist/derived.d.ts +8 -0
- package/dist/derived.js +52 -0
- package/dist/e2e.d.ts +0 -9
- package/dist/e2e.js +0 -20
- package/dist/generators.d.ts +8 -8
- package/dist/generators.js +59 -99
- package/dist/globs.d.ts +3 -0
- package/dist/globs.js +24 -0
- package/dist/init.js +5 -5
- package/dist/lawFiles.d.ts +21 -0
- package/dist/lawFiles.js +95 -0
- package/dist/lawKinds.d.ts +38 -0
- package/dist/lawKinds.js +210 -0
- package/dist/lawNames.d.ts +6 -0
- package/dist/lawNames.js +22 -0
- package/dist/list.d.ts +22 -0
- package/dist/list.js +36 -0
- package/dist/lock.d.ts +16 -3
- package/dist/lock.js +23 -4
- package/dist/lower.d.ts +10 -0
- package/dist/lower.js +178 -0
- package/dist/mcp.d.ts +2 -8
- package/dist/mcp.js +40 -61
- package/dist/migrate/arbitraries.d.ts +23 -0
- package/dist/migrate/arbitraries.js +202 -0
- package/dist/migrate/convert.d.ts +20 -0
- package/dist/migrate/convert.js +915 -0
- package/dist/migrate/framework.d.ts +48 -0
- package/dist/migrate/framework.js +249 -0
- package/dist/migrate/index.d.ts +16 -0
- package/dist/migrate/index.js +155 -0
- package/dist/migrate/report.d.ts +35 -0
- package/dist/migrate/report.js +40 -0
- package/dist/migrate/source.d.ts +42 -0
- package/dist/migrate/source.js +163 -0
- package/dist/migrate/tests.d.ts +44 -0
- package/dist/migrate/tests.js +184 -0
- package/dist/plan.d.ts +50 -0
- package/dist/plan.js +118 -0
- package/dist/pool.d.ts +21 -0
- package/dist/pool.js +54 -0
- package/dist/promote.d.ts +30 -2
- package/dist/promote.js +133 -7
- package/dist/property.d.ts +12 -26
- package/dist/property.js +33 -74
- package/dist/propertyRun.d.ts +24 -0
- package/dist/propertyRun.js +56 -0
- package/dist/propertyWorker.js +7 -9
- package/dist/proposals.d.ts +43 -0
- package/dist/proposals.js +192 -0
- package/dist/regressions.js +23 -2
- package/dist/smt.js +9 -8
- package/dist/types.d.ts +2 -0
- package/hooks/AGENTS.snippet.md +2 -2
- package/package.json +8 -6
package/dist/mcp.js
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
// `tslaws mcp`: a Model Context Protocol server over stdio (JSON-RPC 2.0, one message per line).
|
|
2
2
|
//
|
|
3
3
|
// Safety property: an agent that talks to this server can run checks and write PROPOSED laws, nothing else. No tool
|
|
4
|
-
// accepts laws, writes tsc-laws.lock or edits a
|
|
5
|
-
// TSLAWS_AGENT so nothing it spawns can run `accept`
|
|
4
|
+
// accepts laws, promotes a proposal, writes tsc-laws.lock or edits a file of the tree; the only files it writes are
|
|
5
|
+
// proposals in `.tslaws/proposals/`. The server sets TSLAWS_AGENT so nothing it spawns can run `accept` or `promote`
|
|
6
|
+
// either. A person promotes proposals with `tslaws promote`.
|
|
6
7
|
// stdout is the protocol channel: this module writes nothing else to it.
|
|
7
|
-
import { existsSync,
|
|
8
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
8
9
|
import { createInterface } from 'node:readline';
|
|
9
10
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
10
11
|
import { check } from "./check.js";
|
|
11
12
|
import { readConfig } from "./coverage.js";
|
|
12
13
|
import { hintFor } from "./hints.js";
|
|
14
|
+
import { discoverLaws, discoverProposals, ignoredDirs } from "./lawFiles.js";
|
|
15
|
+
import { listLawFiles } from "./list.js";
|
|
13
16
|
import { parseLawFile } from "./promote.js";
|
|
17
|
+
import { addProposal, mergeProposal, ProposalError } from "./proposals.js";
|
|
14
18
|
import { resolveSettings } from "./settings.js";
|
|
15
|
-
const ignoredDirs = new Set(['node_modules', '.tslaws', '.git']);
|
|
16
19
|
const passing = new Set(['proved', 'bounded', 'tested']);
|
|
17
20
|
const supportedVersions = ['2025-06-18', '2025-03-26', '2024-11-05'];
|
|
18
|
-
const defaultProposalFile = 'agent.
|
|
21
|
+
const defaultProposalFile = 'agent.ts';
|
|
19
22
|
export const tools = [
|
|
23
|
+
{
|
|
24
|
+
name: 'tslaws_list',
|
|
25
|
+
description: 'List the laws files and proposals of the project, as `tslaws list --json`: for each, the level it runs at (node, browser or e2e, from what it imports) and whether it is accepted, unaccepted (in the tree but not in the lock) or proposed. Runs nothing.',
|
|
26
|
+
inputSchema: { type: 'object', properties: {} },
|
|
27
|
+
},
|
|
20
28
|
{
|
|
21
29
|
name: 'tslaws_check',
|
|
22
30
|
description: 'Check the accepted laws (*.laws.ts) of the project, as `tslaws check --json`: each result has a status and a hint with the next action. Never changes any file the person owns.',
|
|
@@ -30,7 +38,7 @@ export const tools = [
|
|
|
30
38
|
},
|
|
31
39
|
{
|
|
32
40
|
name: 'tslaws_check_proposed',
|
|
33
|
-
description: 'Check
|
|
41
|
+
description: 'Check every proposal in .tslaws/proposals/ (and in-tree *.proposed.laws.ts files), as `tslaws check --proposed`: no lock, no coverage gate.',
|
|
34
42
|
inputSchema: { type: 'object', properties: {} },
|
|
35
43
|
},
|
|
36
44
|
{
|
|
@@ -44,15 +52,18 @@ export const tools = [
|
|
|
44
52
|
},
|
|
45
53
|
{
|
|
46
54
|
name: 'tslaws_propose',
|
|
47
|
-
description: 'Write a law into a
|
|
55
|
+
description: 'Write a law into a proposal file in .tslaws/proposals/ (appended; a law of the same name is replaced) and check it. Only that directory can be written. Write imports of project files relative to the project root with a leading ./ (for example ./src/cart); the level (Node, browser, end to end) comes from what the file imports. A person promotes proposals with `tslaws promote`.',
|
|
48
56
|
inputSchema: {
|
|
49
57
|
type: 'object',
|
|
50
58
|
properties: {
|
|
51
59
|
name: { type: 'string', description: 'the law name' },
|
|
52
|
-
source: {
|
|
60
|
+
source: {
|
|
61
|
+
type: 'string',
|
|
62
|
+
description: "the full law, as `law('name', ...)` or `export law name ...` (the project's syntax), with any imports it needs",
|
|
63
|
+
},
|
|
53
64
|
file: {
|
|
54
65
|
type: 'string',
|
|
55
|
-
description: `project-relative
|
|
66
|
+
description: `a plain .ts file name inside .tslaws/proposals/ (or its project-relative path); default ${defaultProposalFile}`,
|
|
56
67
|
},
|
|
57
68
|
},
|
|
58
69
|
required: ['name', 'source'],
|
|
@@ -65,20 +76,7 @@ function slash(path) {
|
|
|
65
76
|
return path.replace(/\\/g, '/');
|
|
66
77
|
}
|
|
67
78
|
function discover(cwd, proposed) {
|
|
68
|
-
|
|
69
|
-
const walk = (dir) => {
|
|
70
|
-
for (const entry of readdirSync(dir)) {
|
|
71
|
-
if (ignoredDirs.has(entry))
|
|
72
|
-
continue;
|
|
73
|
-
const full = join(dir, entry);
|
|
74
|
-
if (statSync(full).isDirectory())
|
|
75
|
-
walk(full);
|
|
76
|
-
else if (entry.endsWith('.laws.ts') && entry.endsWith('.proposed.laws.ts') === proposed)
|
|
77
|
-
found.push(full);
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
walk(cwd);
|
|
81
|
-
return found.map((f) => slash(relative(cwd, f))).sort();
|
|
79
|
+
return proposed ? [...discoverLaws(cwd, true), ...discoverProposals(cwd)] : discoverLaws(cwd, false);
|
|
82
80
|
}
|
|
83
81
|
/** A project-relative path inside the root that names an existing or new file; throws ToolError otherwise. */
|
|
84
82
|
function insideRoot(cwd, file) {
|
|
@@ -99,12 +97,6 @@ function insideRoot(cwd, file) {
|
|
|
99
97
|
throw new ToolError(`'${file}' is outside the project`);
|
|
100
98
|
return full;
|
|
101
99
|
}
|
|
102
|
-
/** The only files a tool may write. */
|
|
103
|
-
export function proposalPath(cwd, file) {
|
|
104
|
-
if (!file.endsWith('.proposed.laws.ts') || file.endsWith('/.proposed.laws.ts'))
|
|
105
|
-
throw new ToolError(`'${file}' is not a *.proposed.laws.ts file; only proposed laws can be written`);
|
|
106
|
-
return insideRoot(cwd, file);
|
|
107
|
-
}
|
|
108
100
|
function options(opts, files) {
|
|
109
101
|
const settings = resolveSettings(new Map(), readConfig(opts.cwd));
|
|
110
102
|
return {
|
|
@@ -142,7 +134,9 @@ export class Session {
|
|
|
142
134
|
}
|
|
143
135
|
async run(files, enforceLock) {
|
|
144
136
|
if (files.length === 0)
|
|
145
|
-
throw new ToolError(
|
|
137
|
+
throw new ToolError(enforceLock
|
|
138
|
+
? 'no laws files found (looking for *.laws.ts)'
|
|
139
|
+
: 'no proposals found (looking in .tslaws/proposals/, and for in-tree *.proposed.laws.ts files)');
|
|
146
140
|
const report = await check(options(this.opts, files), enforceLock);
|
|
147
141
|
this.remember(report);
|
|
148
142
|
return report;
|
|
@@ -160,6 +154,8 @@ export class Session {
|
|
|
160
154
|
const report = await this.run(files, true);
|
|
161
155
|
return args.summary === true ? summarised(report) : withHints(report);
|
|
162
156
|
}
|
|
157
|
+
case 'tslaws_list':
|
|
158
|
+
return { files: listLawFiles(cwd) };
|
|
163
159
|
case 'tslaws_check_proposed':
|
|
164
160
|
return withHints(await this.run(discover(cwd, true), false));
|
|
165
161
|
case 'tslaws_explain': {
|
|
@@ -185,26 +181,22 @@ export class Session {
|
|
|
185
181
|
};
|
|
186
182
|
}
|
|
187
183
|
case 'tslaws_propose': {
|
|
188
|
-
if (typeof args.name !== 'string' ||
|
|
189
|
-
throw new ToolError(
|
|
184
|
+
if (typeof args.name !== 'string' || args.name.trim() === '' || /[\r\n]/.test(args.name))
|
|
185
|
+
throw new ToolError("name must be the law name: an identifier, or the text of a law('name', ...) call");
|
|
190
186
|
if (typeof args.source !== 'string')
|
|
191
187
|
throw new ToolError('source must be a string');
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
throw new ToolError('source may contain only imports and law declarations');
|
|
203
|
-
const current = existsSync(target) ? readFileSync(target, 'utf8') : '';
|
|
204
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
205
|
-
writeFileSync(target, mergeProposal(current, proposal.imports, proposal.laws));
|
|
188
|
+
let written;
|
|
189
|
+
try {
|
|
190
|
+
written = addProposal(cwd, typeof args.file === 'string' ? args.file : defaultProposalFile, args.source, args.name);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
if (error instanceof ProposalError)
|
|
194
|
+
throw new ToolError(error.message);
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
const file = written.file;
|
|
206
198
|
const report = await this.run([file], false);
|
|
207
|
-
const names = new Set(
|
|
199
|
+
const names = new Set(written.laws);
|
|
208
200
|
const results = report.results.filter((r) => names.has(r.name));
|
|
209
201
|
return { file, ...withHints({ ...report, results }) };
|
|
210
202
|
}
|
|
@@ -213,20 +205,7 @@ export class Session {
|
|
|
213
205
|
}
|
|
214
206
|
}
|
|
215
207
|
}
|
|
216
|
-
|
|
217
|
-
export function mergeProposal(current, imports, laws) {
|
|
218
|
-
const parsed = parseLawFile(current);
|
|
219
|
-
let text = current;
|
|
220
|
-
for (const law of [...parsed.laws].reverse()) {
|
|
221
|
-
if (laws.some((l) => l.name === law.name))
|
|
222
|
-
text = text.slice(0, law.start) + text.slice(law.end);
|
|
223
|
-
}
|
|
224
|
-
const have = new Set(parseLawFile(text).imports.map((i) => i.replace(/\s+/g, ' ').trim()));
|
|
225
|
-
const missing = imports.filter((i) => !have.has(i.replace(/\s+/g, ' ').trim()));
|
|
226
|
-
const head = missing.length > 0 ? `${missing.join('\n')}\n` : '';
|
|
227
|
-
const body = `${text.trim() === '' ? '' : `${text.trim()}\n\n`}${laws.map((l) => l.text.trim()).join('\n\n')}\n`;
|
|
228
|
-
return `${head}${head !== '' && !body.startsWith('import') ? '\n' : ''}${body}`;
|
|
229
|
-
}
|
|
208
|
+
export { mergeProposal };
|
|
230
209
|
function reply(id, result) {
|
|
231
210
|
return `${JSON.stringify({ jsonrpc: '2.0', id, result })}\n`;
|
|
232
211
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import type { Bindings } from './framework.ts';
|
|
3
|
+
/** A tslaws type for one arbitrary and the conditions that narrow it back to what the arbitrary produced. */
|
|
4
|
+
export interface Typed {
|
|
5
|
+
type: string;
|
|
6
|
+
/** Which tslaws helpers the type and conditions use. */
|
|
7
|
+
uses: Set<'int' | 'forall'>;
|
|
8
|
+
/** The expression for "`value` is one this arbitrary could have produced", when the type alone is wider. */
|
|
9
|
+
condition?: (value: string, depth: number) => string;
|
|
10
|
+
}
|
|
11
|
+
export type Mapped = {
|
|
12
|
+
ok: true;
|
|
13
|
+
typed: Typed;
|
|
14
|
+
} | {
|
|
15
|
+
ok: false;
|
|
16
|
+
reason: string;
|
|
17
|
+
};
|
|
18
|
+
/** The name of the fast-check export an expression refers to (`fc.integer`, or `integer` when imported by name). */
|
|
19
|
+
export declare function fcMember(node: ts.Expression, b: Bindings): string | undefined;
|
|
20
|
+
/** Maps one arbitrary expression to a tslaws type, or says why it has none. */
|
|
21
|
+
export declare function mapArbitrary(node: ts.Expression, b: Bindings): Mapped;
|
|
22
|
+
/** The `assume(...)` argument that narrows `value` to what `typed` produced, or undefined when the type is already exact. */
|
|
23
|
+
export declare function assumption(typed: Typed, value: string): string | undefined;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// fast-check arbitraries that have a tslaws type: the law gets a typed parameter, and the arbitrary's own limits become
|
|
2
|
+
// `assume(...)` lines. Anything else has no faithful type, so the caller keeps the `fc.assert` as it was.
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
import { unwrap } from "./source.js";
|
|
5
|
+
const no = (reason) => ({ ok: false, reason });
|
|
6
|
+
/** The name of the fast-check export an expression refers to (`fc.integer`, or `integer` when imported by name). */
|
|
7
|
+
export function fcMember(node, b) {
|
|
8
|
+
const e = unwrap(node);
|
|
9
|
+
if (ts.isPropertyAccessExpression(e) && ts.isIdentifier(e.expression) && b.fcNamespaces.has(e.expression.text))
|
|
10
|
+
return e.name.text;
|
|
11
|
+
if (ts.isIdentifier(e))
|
|
12
|
+
return b.fcNamed.get(e.text);
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
function numberOf(node) {
|
|
16
|
+
if (node === undefined)
|
|
17
|
+
return undefined;
|
|
18
|
+
const e = unwrap(node);
|
|
19
|
+
if (ts.isNumericLiteral(e))
|
|
20
|
+
return Number(e.text);
|
|
21
|
+
if (ts.isPrefixUnaryExpression(e) && ts.isNumericLiteral(e.operand)) {
|
|
22
|
+
if (e.operator === ts.SyntaxKind.MinusToken)
|
|
23
|
+
return -Number(e.operand.text);
|
|
24
|
+
if (e.operator === ts.SyntaxKind.PlusToken)
|
|
25
|
+
return Number(e.operand.text);
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
/** The `{ key: value }` options of an arbitrary; only the keys in `allowed` are understood. */
|
|
30
|
+
function optionsOf(node, allowed, member) {
|
|
31
|
+
const values = new Map();
|
|
32
|
+
if (node === undefined)
|
|
33
|
+
return { ok: true, values };
|
|
34
|
+
const e = unwrap(node);
|
|
35
|
+
if (!ts.isObjectLiteralExpression(e))
|
|
36
|
+
return { ok: false, reason: `fc.${member} takes options that are not an object literal` };
|
|
37
|
+
for (const p of e.properties) {
|
|
38
|
+
if (!ts.isPropertyAssignment(p) || !ts.isIdentifier(p.name) || !allowed.includes(p.name.text))
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
reason: `fc.${member} has an option tslaws cannot express (${p.name?.getText() ?? 'spread'})`,
|
|
42
|
+
};
|
|
43
|
+
values.set(p.name.text, p.initializer);
|
|
44
|
+
}
|
|
45
|
+
return { ok: true, values };
|
|
46
|
+
}
|
|
47
|
+
const isTrue = (node) => node === undefined
|
|
48
|
+
? undefined
|
|
49
|
+
: unwrap(node).kind === ts.SyntaxKind.TrueKeyword
|
|
50
|
+
? true
|
|
51
|
+
: unwrap(node).kind === ts.SyntaxKind.FalseKeyword
|
|
52
|
+
? false
|
|
53
|
+
: undefined;
|
|
54
|
+
function combine(parts) {
|
|
55
|
+
const present = parts.filter((p) => p !== undefined);
|
|
56
|
+
return present.length === 0 ? undefined : present.join(' && ');
|
|
57
|
+
}
|
|
58
|
+
/** `min` / `max` as comparisons, when both are numeric literals or absent. */
|
|
59
|
+
function range(values, member) {
|
|
60
|
+
const out = {};
|
|
61
|
+
for (const key of ['min', 'max']) {
|
|
62
|
+
const node = values.get(key);
|
|
63
|
+
if (node === undefined)
|
|
64
|
+
continue;
|
|
65
|
+
const n = numberOf(node);
|
|
66
|
+
if (n === undefined || Number.isNaN(n))
|
|
67
|
+
return { ok: false, reason: `fc.${member} has a ${key} that is not a number literal` };
|
|
68
|
+
out[key] = n;
|
|
69
|
+
}
|
|
70
|
+
return { ok: true, ...out };
|
|
71
|
+
}
|
|
72
|
+
function bounds(value, r) {
|
|
73
|
+
return combine([
|
|
74
|
+
r.min === undefined ? undefined : `${value} >= ${r.min}`,
|
|
75
|
+
r.max === undefined ? undefined : `${value} <= ${r.max}`,
|
|
76
|
+
]);
|
|
77
|
+
}
|
|
78
|
+
/** Maps one arbitrary expression to a tslaws type, or says why it has none. */
|
|
79
|
+
export function mapArbitrary(node, b) {
|
|
80
|
+
const e = unwrap(node);
|
|
81
|
+
if (!ts.isCallExpression(e))
|
|
82
|
+
return no(`${e.getText().slice(0, 40)} is not a plain fc call`);
|
|
83
|
+
const member = fcMember(e.expression, b);
|
|
84
|
+
if (member === undefined)
|
|
85
|
+
return no(`${e.expression.getText().slice(0, 40)} is not an fc arbitrary`);
|
|
86
|
+
const [first, second] = e.arguments;
|
|
87
|
+
switch (member) {
|
|
88
|
+
case 'integer': {
|
|
89
|
+
const o = optionsOf(first, ['min', 'max'], member);
|
|
90
|
+
if (!o.ok)
|
|
91
|
+
return no(o.reason);
|
|
92
|
+
const r = range(o.values, member);
|
|
93
|
+
if (!r.ok)
|
|
94
|
+
return no(r.reason);
|
|
95
|
+
return { ok: true, typed: { type: 'int', uses: new Set(['int']), condition: (v) => bounds(v, r) ?? 'true' } };
|
|
96
|
+
}
|
|
97
|
+
case 'nat': {
|
|
98
|
+
const max = first === undefined ? undefined : numberOf(first);
|
|
99
|
+
let upper = max;
|
|
100
|
+
if (first !== undefined && max === undefined) {
|
|
101
|
+
const o = optionsOf(first, ['max'], member);
|
|
102
|
+
if (!o.ok)
|
|
103
|
+
return no(o.reason);
|
|
104
|
+
const r = range(o.values, member);
|
|
105
|
+
if (!r.ok)
|
|
106
|
+
return no(r.reason);
|
|
107
|
+
upper = r.max;
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
ok: true,
|
|
111
|
+
typed: { type: 'int', uses: new Set(['int']), condition: (v) => bounds(v, { min: 0, max: upper }) },
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
case 'float':
|
|
115
|
+
case 'double': {
|
|
116
|
+
const o = optionsOf(first, ['min', 'max', 'noNaN', 'noDefaultInfinity'], member);
|
|
117
|
+
if (!o.ok)
|
|
118
|
+
return no(o.reason);
|
|
119
|
+
const r = range(o.values, member);
|
|
120
|
+
if (!r.ok)
|
|
121
|
+
return no(r.reason);
|
|
122
|
+
const noNaN = isTrue(o.values.get('noNaN'));
|
|
123
|
+
const noInf = isTrue(o.values.get('noDefaultInfinity'));
|
|
124
|
+
if ((o.values.has('noNaN') && noNaN === undefined) || (o.values.has('noDefaultInfinity') && noInf === undefined))
|
|
125
|
+
return no(`fc.${member} has a flag that is not true or false`);
|
|
126
|
+
const finite = noInf === true && r.min === undefined && r.max === undefined;
|
|
127
|
+
return {
|
|
128
|
+
ok: true,
|
|
129
|
+
typed: {
|
|
130
|
+
type: 'number',
|
|
131
|
+
uses: new Set(),
|
|
132
|
+
condition: (v) => combine([
|
|
133
|
+
noNaN === true ? `!Number.isNaN(${v})` : undefined,
|
|
134
|
+
finite ? `(${v} !== Infinity && ${v} !== -Infinity)` : undefined,
|
|
135
|
+
bounds(v, r),
|
|
136
|
+
]) ?? 'true',
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
case 'string': {
|
|
141
|
+
const o = optionsOf(first, ['minLength', 'maxLength'], member);
|
|
142
|
+
if (!o.ok)
|
|
143
|
+
return no(o.reason);
|
|
144
|
+
const lo = o.values.has('minLength') ? numberOf(o.values.get('minLength')) : undefined;
|
|
145
|
+
const hi = o.values.has('maxLength') ? numberOf(o.values.get('maxLength')) : undefined;
|
|
146
|
+
if ((o.values.has('minLength') && lo === undefined) || (o.values.has('maxLength') && hi === undefined))
|
|
147
|
+
return no('fc.string has a length that is not a number literal');
|
|
148
|
+
return {
|
|
149
|
+
ok: true,
|
|
150
|
+
typed: {
|
|
151
|
+
type: 'string',
|
|
152
|
+
uses: new Set(),
|
|
153
|
+
condition: (v) => bounds(`${v}.length`, { min: lo, max: hi }) ?? 'true',
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
case 'boolean':
|
|
158
|
+
if (first !== undefined)
|
|
159
|
+
return no('fc.boolean takes no arguments');
|
|
160
|
+
return { ok: true, typed: { type: 'boolean', uses: new Set() } };
|
|
161
|
+
case 'array': {
|
|
162
|
+
if (first === undefined)
|
|
163
|
+
return no('fc.array needs an element arbitrary');
|
|
164
|
+
const inner = mapArbitrary(first, b);
|
|
165
|
+
if (!inner.ok)
|
|
166
|
+
return inner;
|
|
167
|
+
const o = optionsOf(second, ['minLength', 'maxLength'], member);
|
|
168
|
+
if (!o.ok)
|
|
169
|
+
return no(o.reason);
|
|
170
|
+
const lo = o.values.has('minLength') ? numberOf(o.values.get('minLength')) : undefined;
|
|
171
|
+
const hi = o.values.has('maxLength') ? numberOf(o.values.get('maxLength')) : undefined;
|
|
172
|
+
if ((o.values.has('minLength') && lo === undefined) || (o.values.has('maxLength') && hi === undefined))
|
|
173
|
+
return no('fc.array has a length that is not a number literal');
|
|
174
|
+
const t = inner.typed;
|
|
175
|
+
const element = t.condition?.('x', 0) === 'true' ? undefined : t.condition;
|
|
176
|
+
const uses = new Set(t.uses);
|
|
177
|
+
if (element !== undefined)
|
|
178
|
+
uses.add('forall');
|
|
179
|
+
return {
|
|
180
|
+
ok: true,
|
|
181
|
+
typed: {
|
|
182
|
+
type: `${t.type}[]`,
|
|
183
|
+
uses,
|
|
184
|
+
condition: (v, depth) => {
|
|
185
|
+
const e = `e${depth}`;
|
|
186
|
+
return (combine([
|
|
187
|
+
bounds(`${v}.length`, { min: lo, max: hi }),
|
|
188
|
+
element === undefined ? undefined : `forall(${v}, (${e}) => ${element(e, depth + 1)})`,
|
|
189
|
+
]) ?? 'true');
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
default:
|
|
195
|
+
return no(`fc.${member}(...) has no tslaws type`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/** The `assume(...)` argument that narrows `value` to what `typed` produced, or undefined when the type is already exact. */
|
|
199
|
+
export function assumption(typed, value) {
|
|
200
|
+
const text = typed.condition?.(value, 0);
|
|
201
|
+
return text === undefined || text === 'true' ? undefined : text;
|
|
202
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type SourceName } from './framework.ts';
|
|
2
|
+
import type { Entry } from './report.ts';
|
|
3
|
+
import { type Row } from './tests.ts';
|
|
4
|
+
export interface ConvertInput {
|
|
5
|
+
/** The source file, relative to the working directory. */
|
|
6
|
+
path: string;
|
|
7
|
+
text: string;
|
|
8
|
+
from: SourceName;
|
|
9
|
+
/** Maps a relative module specifier of the source to the one the converted file needs where it is written. */
|
|
10
|
+
rewrite: (specifier: string) => string;
|
|
11
|
+
}
|
|
12
|
+
export interface Converted {
|
|
13
|
+
entries: Entry[];
|
|
14
|
+
laws: string[];
|
|
15
|
+
output?: string;
|
|
16
|
+
/** Whether the laws drive a browser through `tslaws/e2e`, which names the file `*.browser.laws.ts`. */
|
|
17
|
+
e2e: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function convertTestFile(input: ConvertInput): Converted;
|
|
20
|
+
export type { Row };
|