lawspec 0.7.0 → 0.8.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/API-MIGRATION.md +83 -65
- package/LANGUAGE.md +136 -0
- package/PRIMITIVES.md +2 -2
- package/README.md +32 -26
- package/REFINEMENTS.md +1 -1
- package/RUST.md +97 -0
- package/api.mjs +1 -1
- package/bin/lawspec.mjs +15 -43
- package/build.json +50 -20
- package/compatibility.json +102 -19
- package/core.wasm +0 -0
- package/doctor.mjs +31 -2
- package/examples/specs/algebra.lawspec +36 -8
- package/examples/specs/currying.lawspec +1 -1
- package/examples/specs/refinements.lawspec +11 -0
- package/examples-command.mjs +1 -1
- package/index.d.ts +19 -19
- package/package.json +2 -2
- package/templates.mjs +24 -0
package/bin/lawspec.mjs
CHANGED
|
@@ -104,6 +104,7 @@ async function init() {
|
|
|
104
104
|
throw new Error("Target is already configured");
|
|
105
105
|
await mkdir(root, { recursive: true });
|
|
106
106
|
const buildFiles = [
|
|
107
|
+
"Cargo.toml",
|
|
107
108
|
"pom.xml",
|
|
108
109
|
"pyproject.toml",
|
|
109
110
|
"package.json",
|
|
@@ -153,46 +154,17 @@ async function init() {
|
|
|
153
154
|
`Configured ${language}. ${hasBuild ? "Existing build files preserved." : "Created missing project build files."}\n${setup[language]}\nNext: lawspec doctor, then lawspec generate.`,
|
|
154
155
|
);
|
|
155
156
|
}
|
|
156
|
-
function
|
|
157
|
-
if (
|
|
158
|
-
if (
|
|
159
|
-
return
|
|
160
|
-
}
|
|
161
|
-
function showExpression(expr) {
|
|
162
|
-
const value = expr.contents;
|
|
163
|
-
if (expr.tag === "Var" || expr.tag === "Number") return value;
|
|
164
|
-
if (expr.tag === "DecimalNumber") return `${value[0]}e${value[1]}`;
|
|
165
|
-
if (expr.tag === "ScalarLit") return showScalar(value);
|
|
166
|
-
if (expr.tag === "Binary") return `(${showExpression(value[1])} ${value[0]} ${showExpression(value[2])})`;
|
|
167
|
-
if (expr.tag === "Unary") return `${value[0]}(${showExpression(value[1])})`;
|
|
168
|
-
if (expr.tag === "Annotate") return `(${showExpression(value[0])} :: ${showType(value[1])})`;
|
|
169
|
-
if (
|
|
170
|
-
expr.tag === "Number" ||
|
|
171
|
-
expr.tag === "StringLit" ||
|
|
172
|
-
expr.tag === "BoolLit"
|
|
173
|
-
)
|
|
174
|
-
return JSON.stringify(value);
|
|
175
|
-
if (expr.tag === "Apply")
|
|
176
|
-
return `${showExpression(value[0])} (${showExpression(value[1])})`;
|
|
177
|
-
return `(${showExpression(value[0])} . ${showExpression(value[1])})`;
|
|
157
|
+
function showAssertion(assertion) {
|
|
158
|
+
if (assertion.kind === "equal") return `${assertion.left.text} = ${assertion.right.text}`;
|
|
159
|
+
if (assertion.kind === "implies") return `${assertion.guard.text} implies ${showAssertion(assertion.body)}`;
|
|
160
|
+
return assertion.items.map(showAssertion).join(" and ");
|
|
178
161
|
}
|
|
179
162
|
function explainExamples(law) {
|
|
180
|
-
return law.
|
|
181
|
-
.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
.map(([n, v]) => ` ${n} = ${showScalar(v)}`)
|
|
186
|
-
.join("\n") +
|
|
187
|
-
"\n" +
|
|
188
|
-
ex.expectations
|
|
189
|
-
.map(
|
|
190
|
-
(e) =>
|
|
191
|
-
` expect ${showExpression(e.actual)} = ${showScalar(e.expected)}`,
|
|
192
|
-
)
|
|
193
|
-
.join("\n"),
|
|
194
|
-
)
|
|
195
|
-
.join("\n");
|
|
163
|
+
return law.examples.map(ex =>
|
|
164
|
+
`\nexample ${JSON.stringify(ex.name)}\n` +
|
|
165
|
+
ex.bindings.map(b => ` ${b.name} = ${showScalar(b.value)}`).join("\n") + "\n" +
|
|
166
|
+
ex.expectations.map(e => ` expect ${showAssertion(e)}`).join("\n")
|
|
167
|
+
).join("\n");
|
|
196
168
|
}
|
|
197
169
|
async function main() {
|
|
198
170
|
if (options['machine-bits'] !== undefined) {
|
|
@@ -201,13 +173,13 @@ async function main() {
|
|
|
201
173
|
}
|
|
202
174
|
if (!verb || ["help", "--help", "-h"].includes(verb)) {
|
|
203
175
|
output(
|
|
204
|
-
"LawSpec 0.
|
|
176
|
+
"LawSpec 0.8.0\nUsage: lawspec init --target <language> [--project <directory>]\n lawspec check | doctor | explain <unit>::<law> | generate\n lawspec examples [--target <language>] [--output example_artifacts]\nOptions: --config <path>, --target <language>, --machine-bits <32|64>, --json\nGeneration: --dry-run, --check\nTargets: " +
|
|
205
177
|
targets.join(", "),
|
|
206
178
|
);
|
|
207
179
|
return;
|
|
208
180
|
}
|
|
209
181
|
if (verb === "--version") {
|
|
210
|
-
output("0.
|
|
182
|
+
output("0.8.0");
|
|
211
183
|
return;
|
|
212
184
|
}
|
|
213
185
|
if (positional.length > (verb === "explain" ? 1 : 0))
|
|
@@ -280,7 +252,7 @@ async function main() {
|
|
|
280
252
|
output(
|
|
281
253
|
options.json
|
|
282
254
|
? result
|
|
283
|
-
: `Checked ${result.laws.length}
|
|
255
|
+
: `Checked ${result.laws.length} law(s).`,
|
|
284
256
|
);
|
|
285
257
|
return;
|
|
286
258
|
}
|
|
@@ -291,7 +263,7 @@ async function main() {
|
|
|
291
263
|
.filter(
|
|
292
264
|
({ e }) => !positional[0] || `${e.owner}::${e.name}` === positional[0],
|
|
293
265
|
);
|
|
294
|
-
if (!indices.length) throw new Error("No matching
|
|
266
|
+
if (!indices.length) throw new Error("No matching law");
|
|
295
267
|
output(
|
|
296
268
|
options.json
|
|
297
269
|
? indices.map(({ e, i }) => ({ ...e, expansion: result.expansions[i] }))
|
|
@@ -319,7 +291,7 @@ async function main() {
|
|
|
319
291
|
).files,
|
|
320
292
|
);
|
|
321
293
|
const reports = await Promise.all(
|
|
322
|
-
selected.map((t, i) => doctor(t, roots[i])),
|
|
294
|
+
selected.map((t, i) => doctor(t, roots[i], artifacts[i])),
|
|
323
295
|
);
|
|
324
296
|
const failed = reports.filter((r) => !r.ok);
|
|
325
297
|
if (failed.length)
|
package/build.json
CHANGED
|
@@ -7,20 +7,35 @@
|
|
|
7
7
|
"runtime/lawspec_runtime.go",
|
|
8
8
|
"runtime/lawspec_runtime.mjs",
|
|
9
9
|
"runtime/lawspec_runtime.py",
|
|
10
|
+
"runtime/lawspec_runtime.rs",
|
|
11
|
+
"runtime/lawspec_strategies.rs",
|
|
12
|
+
"runtime/rust/Cargo.lock",
|
|
13
|
+
"runtime/rust/Cargo.toml",
|
|
10
14
|
"src/LawSpec/Api.hs",
|
|
15
|
+
"src/LawSpec/Backend.hs",
|
|
16
|
+
"src/LawSpec/Common.hs",
|
|
11
17
|
"src/LawSpec/Compile.hs",
|
|
18
|
+
"src/LawSpec/Core.hs",
|
|
19
|
+
"src/LawSpec/Core/Eval.hs",
|
|
20
|
+
"src/LawSpec/Core/Semantics.hs",
|
|
21
|
+
"src/LawSpec/Core/Validate.hs",
|
|
22
|
+
"src/LawSpec/CoreEmit.hs",
|
|
23
|
+
"src/LawSpec/CoreNativeScalarEmit.hs",
|
|
24
|
+
"src/LawSpec/CoreScalarEmit.hs",
|
|
12
25
|
"src/LawSpec/Domain.hs",
|
|
13
26
|
"src/LawSpec/Emit.hs",
|
|
14
27
|
"src/LawSpec/Eval.hs",
|
|
28
|
+
"src/LawSpec/Frontend.hs",
|
|
15
29
|
"src/LawSpec/Gen.hs",
|
|
16
30
|
"src/LawSpec/Model.hs",
|
|
17
|
-
"src/LawSpec/NativeScalarEmit.hs",
|
|
18
31
|
"src/LawSpec/Parser.hs",
|
|
19
32
|
"src/LawSpec/Prelude.hs",
|
|
33
|
+
"src/LawSpec/Public.hs",
|
|
20
34
|
"src/LawSpec/Refinement.hs",
|
|
21
35
|
"src/LawSpec/RuntimeSources.hs",
|
|
36
|
+
"src/LawSpec/RustEmit.hs",
|
|
22
37
|
"src/LawSpec/Scalar.hs",
|
|
23
|
-
"src/LawSpec/
|
|
38
|
+
"src/LawSpec/Testing.hs",
|
|
24
39
|
"stack.yaml",
|
|
25
40
|
"stack.yaml.lock",
|
|
26
41
|
"tools/embed-runtimes.py",
|
|
@@ -30,36 +45,51 @@
|
|
|
30
45
|
"wasm/lawspec-wasm.cabal"
|
|
31
46
|
],
|
|
32
47
|
"digests": {
|
|
33
|
-
"package.yaml": "
|
|
48
|
+
"package.yaml": "90923d07cf41977f62a201faf26667ae4fd76de84c6f25ce47917ac92a6a1f4b",
|
|
34
49
|
"runtime/LawSpecRuntime.hs": "1c3ca22232c64227717d836a57d375d906f748ecde6b79705944a2319ce5064d",
|
|
35
50
|
"runtime/LawSpecRuntime.java": "5637914bd491f75ad0c75fb19d1bec798164295b7bcf9d23e1549e8ebf780d01",
|
|
36
51
|
"runtime/lawspec_runtime.go": "62d2041c33760b67db619e0a803501768a505c4dfd9837bca85e158fff81fda9",
|
|
37
52
|
"runtime/lawspec_runtime.mjs": "7d0abd53fa45dd9828c10ff2ffa7876162e965e7bb4913f4c460af13e06142fa",
|
|
38
53
|
"runtime/lawspec_runtime.py": "7541353effebd133e50cd00a3f4d8999651c93ba751a6d951164b1f390ce0c15",
|
|
39
|
-
"
|
|
40
|
-
"
|
|
54
|
+
"runtime/lawspec_runtime.rs": "bf9cf5fa546346a59c85248936395fe11946974ce0bad053aeeaf4e26b6a829d",
|
|
55
|
+
"runtime/lawspec_strategies.rs": "a5dcee400574a69a5d2aa066bb8959e366728b3f21a578de84f50feadadf6641",
|
|
56
|
+
"runtime/rust/Cargo.lock": "91533e20939436e117c0f09ded695a8a05a86e79efc29fe4b4b93ad8ef1771c8",
|
|
57
|
+
"runtime/rust/Cargo.toml": "7ec66cf82dcc73a4a0b4a44d6048b6c3a340897c488aaaace9e8d4bac90b5e0a",
|
|
58
|
+
"src/LawSpec/Api.hs": "4810c05d53e370daf7520b9116c34997b7b4ba9c8d3f1a349e2afdb7b1424552",
|
|
59
|
+
"src/LawSpec/Backend.hs": "15ba04b69739e3891f73757d9cdc0a29acca6b7f7e067fd11d83d718474d0f20",
|
|
60
|
+
"src/LawSpec/Common.hs": "baa849dad0cc6227af334fdc7488389df50c0fe3a74a2ba1d84eb60c6b6edd6e",
|
|
61
|
+
"src/LawSpec/Compile.hs": "6c8bcea938f2ec3f83afb348c5ca932d96ce070f4e916fc9233a3a02ffcc89f2",
|
|
62
|
+
"src/LawSpec/Core.hs": "27ce2bb1274485cc190e10f8520118d5b381c90ac4ab6343223c46fb3cd10869",
|
|
63
|
+
"src/LawSpec/Core/Eval.hs": "87bf2acfdfff1431f1358947044c0474df40b5bc601076f5e5051e089f1d2fa4",
|
|
64
|
+
"src/LawSpec/Core/Semantics.hs": "a9d019433e3b9731f8cd0f65ec0ccc53179824bea58247b72df26760b878c2b6",
|
|
65
|
+
"src/LawSpec/Core/Validate.hs": "bd67926c872e190279f3758ad57b45c0f80d5be8381cedc8ac56a5b88b115201",
|
|
66
|
+
"src/LawSpec/CoreEmit.hs": "7ba225a262372f61edd6fe2ba0f2dbade44dfa2f767cdbcdd3f4c323eb5e9e7b",
|
|
67
|
+
"src/LawSpec/CoreNativeScalarEmit.hs": "f20382952f5854605ae617609c1ff9f5627e5acb134417cf3a67b97054844acb",
|
|
68
|
+
"src/LawSpec/CoreScalarEmit.hs": "db564e0cb07c69dee63b7dcb9cd524c30d4e7d658264d72f834adcc9fa05899d",
|
|
41
69
|
"src/LawSpec/Domain.hs": "8ad50ad3a37aacb3a429c2d7924279e2a3b973204ce3c5c04c634c1b9ada0151",
|
|
42
|
-
"src/LawSpec/Emit.hs": "
|
|
43
|
-
"src/LawSpec/Eval.hs": "
|
|
44
|
-
"src/LawSpec/
|
|
45
|
-
"src/LawSpec/
|
|
46
|
-
"src/LawSpec/
|
|
47
|
-
"src/LawSpec/Parser.hs": "
|
|
70
|
+
"src/LawSpec/Emit.hs": "9f34f5765915a2a12a66a6dca647ddaef9289372956802c6d99cf0cfec447c7a",
|
|
71
|
+
"src/LawSpec/Eval.hs": "bbaa50d3febee690c3b09ceb3f2592964cdb458dd1b4c38b6ec68a80f3e077ea",
|
|
72
|
+
"src/LawSpec/Frontend.hs": "b483732416f23960f0f03c30079a6db3ba630cf013b38fd6beede08b186585b8",
|
|
73
|
+
"src/LawSpec/Gen.hs": "f0a153e05fb0599a7cb2303dd62a269b2561738466c7211efc1927af2d5604b0",
|
|
74
|
+
"src/LawSpec/Model.hs": "75cedd730b8e8f7bff2dd39cfe3c1ed8f64e74471f6231248474957df446328f",
|
|
75
|
+
"src/LawSpec/Parser.hs": "e64356888d78f02572ff8c0a39b782248dd6a4bb9f0b1e30900f9dc12c3cd946",
|
|
48
76
|
"src/LawSpec/Prelude.hs": "b1e155c12f58e7c346551a0c65c5d5af76d045b3f1625977ca2a5d1c97cb8c46",
|
|
49
|
-
"src/LawSpec/
|
|
50
|
-
"src/LawSpec/
|
|
51
|
-
"src/LawSpec/
|
|
52
|
-
"src/LawSpec/
|
|
77
|
+
"src/LawSpec/Public.hs": "eec6c9684b3c5cb42fa882f89460104856cb02f963822b66d9b9583dc011dfeb",
|
|
78
|
+
"src/LawSpec/Refinement.hs": "9902897edc0a803c2d440cf6d5a1d44e524f94b7384ea55a6f9dcd16dbfa8957",
|
|
79
|
+
"src/LawSpec/RuntimeSources.hs": "69dadd138bb784eaebd74869d03e30079e6d73e1724179794d8ec32be178e171",
|
|
80
|
+
"src/LawSpec/RustEmit.hs": "702652cc590c5db148f35d2e591d90e45270eee47e78c31772749943509aafe7",
|
|
81
|
+
"src/LawSpec/Scalar.hs": "2907d94b72e66584b762b5ab33da64adb26f9bf84b885151ef408dc3de1a440d",
|
|
82
|
+
"src/LawSpec/Testing.hs": "a5dc1c15a45539bb72d1c7cb24cbd04888bbb8ef2f066c829654c3bdbf32c609",
|
|
53
83
|
"stack.yaml": "20ccf4d599e355e60b7aa4f814a7cd4299fe2048616cc2e6dbdc22a7bd8cec73",
|
|
54
84
|
"stack.yaml.lock": "ae222b9c81af920c56e50fa4596fa57786e7fa5a7b461390362b2b2ff63818c7",
|
|
55
|
-
"tools/embed-runtimes.py": "
|
|
85
|
+
"tools/embed-runtimes.py": "26dda6ecf944ac065d10165d84ecdf44f134945fabbd48492a67d2495b5faba4",
|
|
56
86
|
"wasm/app/Exports.hs": "4ecbdac8faa2449e6fc61b93c82e6278fa14374154f29b4dacf0eef433f06a47",
|
|
57
87
|
"wasm/cabal.project": "021e560afdc5eb4cb7169e7119ecb8f92c9ee170245af909c94b612941bff5cc",
|
|
58
88
|
"wasm/cabal.project.freeze": "733dbed3d2ecccb26e874fd58f136296dad772184deb1658c196b5d54a0814dc",
|
|
59
|
-
"wasm/lawspec-wasm.cabal": "
|
|
60
|
-
"npm/core.wasm": "
|
|
89
|
+
"wasm/lawspec-wasm.cabal": "22590b2a6fcd384ef8957bd179e04a055e90535f7eaaa2b5ac3d314fd0b686ed",
|
|
90
|
+
"npm/core.wasm": "09efa9e767c3d33e718a8a0909cdf23b24d158813570cc610f56e72d37a59fa3",
|
|
61
91
|
"npm/core_jsffi.js": "88d136efe92f7cff5758c8fec8d9b6bbc9707fe37741cebeeb415fe34ce3d72b",
|
|
62
|
-
"npm/api.mjs": "
|
|
63
|
-
"npm/index.d.ts": "
|
|
92
|
+
"npm/api.mjs": "7415fc057ea5f4da68f9d255ce862152d6bc45d6b0053d44f61f9110911ac64b",
|
|
93
|
+
"npm/index.d.ts": "d19ecaea0be4ca01c8667a76e1bcfe758fd200d728ef67a44ad14e1b1dd15b72"
|
|
64
94
|
}
|
|
65
95
|
}
|
package/compatibility.json
CHANGED
|
@@ -2,37 +2,120 @@
|
|
|
2
2
|
"version": 1,
|
|
3
3
|
"targets": {
|
|
4
4
|
"java": {
|
|
5
|
-
"java": [
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
"java": [
|
|
6
|
+
"25",
|
|
7
|
+
"26"
|
|
8
|
+
],
|
|
9
|
+
"jetCheck": [
|
|
10
|
+
"0.3.0",
|
|
11
|
+
"0.3.1"
|
|
12
|
+
],
|
|
13
|
+
"junit-jupiter": [
|
|
14
|
+
"5.14.0",
|
|
15
|
+
"5.15.0"
|
|
16
|
+
]
|
|
8
17
|
},
|
|
9
18
|
"python": {
|
|
10
|
-
"python": [
|
|
11
|
-
|
|
12
|
-
|
|
19
|
+
"python": [
|
|
20
|
+
"3.13",
|
|
21
|
+
"3.15"
|
|
22
|
+
],
|
|
23
|
+
"pytest": [
|
|
24
|
+
"8.4.0",
|
|
25
|
+
"8.5.0"
|
|
26
|
+
],
|
|
27
|
+
"hypothesis": [
|
|
28
|
+
"6.135.26",
|
|
29
|
+
"7"
|
|
30
|
+
]
|
|
13
31
|
},
|
|
14
32
|
"javascript": {
|
|
15
|
-
"fast-check": [
|
|
33
|
+
"fast-check": [
|
|
34
|
+
"4",
|
|
35
|
+
"5"
|
|
36
|
+
]
|
|
16
37
|
},
|
|
17
38
|
"typescript": {
|
|
18
|
-
"fast-check": [
|
|
19
|
-
|
|
20
|
-
|
|
39
|
+
"fast-check": [
|
|
40
|
+
"4",
|
|
41
|
+
"5"
|
|
42
|
+
],
|
|
43
|
+
"typescript": [
|
|
44
|
+
"5.9",
|
|
45
|
+
"5.10"
|
|
46
|
+
],
|
|
47
|
+
"@types/node": [
|
|
48
|
+
"22",
|
|
49
|
+
"23"
|
|
50
|
+
]
|
|
21
51
|
},
|
|
22
52
|
"go": {
|
|
23
|
-
"go": [
|
|
24
|
-
|
|
53
|
+
"go": [
|
|
54
|
+
"1.22",
|
|
55
|
+
"1.27"
|
|
56
|
+
],
|
|
57
|
+
"rapid": [
|
|
58
|
+
"1.2.0",
|
|
59
|
+
"1.2.1"
|
|
60
|
+
]
|
|
25
61
|
},
|
|
26
62
|
"haskell": {
|
|
27
|
-
"hspec": [
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
63
|
+
"hspec": [
|
|
64
|
+
"2.11",
|
|
65
|
+
"2.12"
|
|
66
|
+
],
|
|
67
|
+
"hedgehog": [
|
|
68
|
+
"1.5",
|
|
69
|
+
"1.6"
|
|
70
|
+
],
|
|
71
|
+
"hspec-hedgehog": [
|
|
72
|
+
"0.3.0.0",
|
|
73
|
+
"0.3.0.1"
|
|
74
|
+
],
|
|
75
|
+
"ghc": [
|
|
76
|
+
"9.10",
|
|
77
|
+
"9.11"
|
|
78
|
+
]
|
|
31
79
|
},
|
|
32
80
|
"kotlin": {
|
|
33
|
-
"java": [
|
|
34
|
-
|
|
35
|
-
|
|
81
|
+
"java": [
|
|
82
|
+
"25",
|
|
83
|
+
"26"
|
|
84
|
+
],
|
|
85
|
+
"gradle": [
|
|
86
|
+
"9.1",
|
|
87
|
+
"9.4"
|
|
88
|
+
],
|
|
89
|
+
"kotlin": [
|
|
90
|
+
"2.3.21",
|
|
91
|
+
"2.3.22"
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
"rust": {
|
|
95
|
+
"rust": [
|
|
96
|
+
"1.85",
|
|
97
|
+
"2"
|
|
98
|
+
],
|
|
99
|
+
"proptest": [
|
|
100
|
+
"1.11.0",
|
|
101
|
+
"1.11.1"
|
|
102
|
+
],
|
|
103
|
+
"num-bigint": [
|
|
104
|
+
"0.4.8",
|
|
105
|
+
"0.4.9"
|
|
106
|
+
],
|
|
107
|
+
"num-rational": [
|
|
108
|
+
"0.4.2",
|
|
109
|
+
"0.4.3"
|
|
110
|
+
],
|
|
111
|
+
"num-complex": [
|
|
112
|
+
"0.4.6",
|
|
113
|
+
"0.4.7"
|
|
114
|
+
],
|
|
115
|
+
"num-traits": [
|
|
116
|
+
"0.2.19",
|
|
117
|
+
"0.2.20"
|
|
118
|
+
]
|
|
36
119
|
}
|
|
37
120
|
}
|
|
38
121
|
}
|
package/core.wasm
CHANGED
|
Binary file
|
package/doctor.mjs
CHANGED
|
@@ -67,14 +67,14 @@ async function installed(req, name) {
|
|
|
67
67
|
dir = parent;
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
-
export async function doctor(target, root) {
|
|
70
|
+
export async function doctor(target, root, plannedArtifacts = []) {
|
|
71
71
|
const name = target.language;
|
|
72
72
|
const defaults =
|
|
73
73
|
name === "java"
|
|
74
74
|
? ["src/main/java", "src/test/java"]
|
|
75
75
|
: name === "kotlin"
|
|
76
76
|
? ["src/main/kotlin", "src/test/kotlin"]
|
|
77
|
-
: ["src",
|
|
77
|
+
: ["src", ["python", "rust"].includes(name) ? "tests" : "test"];
|
|
78
78
|
const sourceDir = target.sourceDir ?? defaults[0];
|
|
79
79
|
const testDir = target.testDir ?? defaults[1];
|
|
80
80
|
try {
|
|
@@ -257,6 +257,8 @@ export async function doctor(target, root) {
|
|
|
257
257
|
"The Stack profile requires one package per target root",
|
|
258
258
|
);
|
|
259
259
|
const cabal = await readFile(path.join(root, cabals[0]), "utf8");
|
|
260
|
+
const components = cabal.split(/^(?:library(?:[ \t]+\S+)?|test-suite[ \t]+\S+)[ \t]*\n/m).slice(1).map(s => s.split(/\n(?=\S)/)[0]);
|
|
261
|
+
requireThat(components.some(s => s.includes(sourceDir) && ['text','bytestring'].every(dep => new RegExp(`(?:^|[\\s,])${dep}(?=[\\s,<>=]|$)`).test(s))), "The component compiling generated source must directly depend on text and bytestring");
|
|
260
262
|
const suites = cabal
|
|
261
263
|
.split(/^test-suite\s+/m)
|
|
262
264
|
.slice(1)
|
|
@@ -355,6 +357,33 @@ export async function doctor(target, root) {
|
|
|
355
357
|
),
|
|
356
358
|
"The JUnit Jupiter test engine must be on the test classpath",
|
|
357
359
|
);
|
|
360
|
+
} else if (name === "rust") {
|
|
361
|
+
const compiler = target.rustc || "rustc";
|
|
362
|
+
const cargo = target.cargo || "cargo";
|
|
363
|
+
versions.rust = (await run(compiler, ["--version"], root)).match(/^rustc (\d+\.\d+\.\d+)/)?.[1];
|
|
364
|
+
requireThat(versions.rust && compare(versions.rust, "1.85.0") >= 0, "Rust 1.85+ is required");
|
|
365
|
+
const data = JSON.parse(await run(cargo, ["metadata", "--format-version", "1"], root));
|
|
366
|
+
const pkg = data.packages.find(p => path.resolve(p.manifest_path) === path.join(root, "Cargo.toml"));
|
|
367
|
+
requireThat(pkg, "Select a Cargo package directory containing Cargo.toml");
|
|
368
|
+
requireThat(pkg.edition === "2024", "Cargo package must use edition 2024");
|
|
369
|
+
const node = data.resolve.nodes.find(n => n.id === pkg.id);
|
|
370
|
+
for (const dep of ["proptest", "num-bigint", "num-rational", "num-complex", "num-traits"]) {
|
|
371
|
+
const reference = node.deps.find(d => d.name === dep.replaceAll("-", "_"));
|
|
372
|
+
versions[dep] = data.packages.find(p => p.id === reference?.pkg)?.version;
|
|
373
|
+
requireThat(versions[dep], `Missing direct Cargo dependency: ${dep}`);
|
|
374
|
+
}
|
|
375
|
+
const manifest = await readFile(path.join(root, "Cargo.toml"), "utf8");
|
|
376
|
+
requireThat(!/harness\s*=\s*false/.test(manifest), "Rust generated tests require the standard Cargo test harness");
|
|
377
|
+
const testRoot = path.resolve(root, testDir);
|
|
378
|
+
const expectedTests = plannedArtifacts.length
|
|
379
|
+
? plannedArtifacts.filter(f => f.placement === "test" && f.path.endsWith("_lawspec.rs")).map(f => path.resolve(root, f.path))
|
|
380
|
+
: (await readdir(testRoot).catch(error => {if(error.code === "ENOENT") return []; throw error;})).filter(n => n.endsWith("_lawspec.rs")).map(n => path.join(testRoot,n));
|
|
381
|
+
if (testDir !== "tests" || /autotests\s*=\s*false/.test(manifest)) {
|
|
382
|
+
const registered = new Set(pkg.targets.filter(t => t.kind.includes("test") && t.test).map(t => path.resolve(t.src_path)));
|
|
383
|
+
requireThat(expectedTests.length ? expectedTests.every(file => registered.has(file)) : [...registered].some(file => file.startsWith(testRoot + path.sep)), "Register every generated Rust test file with Cargo [[test]] entries for this custom layout");
|
|
384
|
+
}
|
|
385
|
+
const library = pkg.targets.find(t => t.kind.includes("lib"));
|
|
386
|
+
requireThat(!library || path.dirname(path.resolve(library.src_path)) === path.resolve(root, sourceDir), "Cargo library path must match the configured sourceDir");
|
|
358
387
|
} else if (name === "kotlin") {
|
|
359
388
|
const command =
|
|
360
389
|
target.gradle ||
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
unit example.algebra
|
|
2
2
|
|
|
3
|
-
--
|
|
4
|
-
add ::
|
|
5
|
-
multiply ::
|
|
6
|
-
negateValue ::
|
|
7
|
-
maximumValue ::
|
|
8
|
-
subtractValue ::
|
|
9
|
-
divideLeft ::
|
|
10
|
-
divideRight ::
|
|
3
|
+
-- Integer denotes mathematical integers. Adapter results must remain exact.
|
|
4
|
+
add :: Integer -> Integer -> Integer
|
|
5
|
+
multiply :: Integer -> Integer -> Integer
|
|
6
|
+
negateValue :: Integer -> Integer
|
|
7
|
+
maximumValue :: Integer -> Integer -> Integer
|
|
8
|
+
subtractValue :: Integer -> Integer -> Integer
|
|
9
|
+
divideLeft :: Integer -> Integer -> Integer
|
|
10
|
+
divideRight :: Integer -> Integer -> Integer
|
|
11
11
|
|
|
12
12
|
law `addition is commutative` is
|
|
13
13
|
definition is
|
|
@@ -20,6 +20,20 @@ law `addition is commutative` is
|
|
|
20
20
|
expect add x y = 8
|
|
21
21
|
expect add y x = 8
|
|
22
22
|
end
|
|
23
|
+
|
|
24
|
+
example `addition exceeds the Int32 maximum without wrapping` is
|
|
25
|
+
x = 2147483647
|
|
26
|
+
y = 1
|
|
27
|
+
expect add x y = 2147483648
|
|
28
|
+
expect add y x = 2147483648
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
example `addition remains exact beyond machine and safe-number bounds` is
|
|
32
|
+
x = 18446744073709551615
|
|
33
|
+
y = 2
|
|
34
|
+
expect add x y = 18446744073709551617
|
|
35
|
+
expect add y x = 18446744073709551617
|
|
36
|
+
end
|
|
23
37
|
end
|
|
24
38
|
|
|
25
39
|
law `addition is associative` is
|
|
@@ -116,6 +130,14 @@ law `multiplication distributes over addition left` is
|
|
|
116
130
|
expect multiply x (add y z) = 36
|
|
117
131
|
expect add (multiply x y) (multiply x z) = 36
|
|
118
132
|
end
|
|
133
|
+
|
|
134
|
+
example `multiplication preserves products outside Int32` is
|
|
135
|
+
x = 2147483647
|
|
136
|
+
y = 2147483647
|
|
137
|
+
z = 1
|
|
138
|
+
expect multiply x y = 4611686014132420609
|
|
139
|
+
expect multiply x (add y z) = 4611686016279904256
|
|
140
|
+
end
|
|
119
141
|
end
|
|
120
142
|
|
|
121
143
|
law `multiplication distributes over addition right` is
|
|
@@ -251,4 +273,10 @@ law `negation is an involution` is
|
|
|
251
273
|
expect negateValue x = 3
|
|
252
274
|
expect negateValue (negateValue x) = -3
|
|
253
275
|
end
|
|
276
|
+
|
|
277
|
+
example `negating the Int32 minimum produces a positive integer` is
|
|
278
|
+
x = -2147483648
|
|
279
|
+
expect negateValue x = 2147483648
|
|
280
|
+
expect negateValue (negateValue x) = -2147483648
|
|
281
|
+
end
|
|
254
282
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
unit example.currying
|
|
2
2
|
|
|
3
|
-
sumFour ::
|
|
3
|
+
sumFour :: Integer -> Integer -> Integer -> Integer -> Integer
|
|
4
4
|
format :: Text -> Bool -> Int32 -> Text -> Text
|
|
5
5
|
referenceFormat :: Text -> Bool -> Int32 -> Text -> Text
|
|
6
6
|
trim :: Text -> Text
|
|
@@ -73,3 +73,14 @@ end
|
|
|
73
73
|
law `machine bound` is
|
|
74
74
|
definition is `for all` (x :: IntSize where x == IntSize.max) . x = IntSize.max end
|
|
75
75
|
end
|
|
76
|
+
|
|
77
|
+
law `symbol fixture refinement` is
|
|
78
|
+
definition is
|
|
79
|
+
`for all` (x :: Symbol where x == symbol("selected", "fixture")) .
|
|
80
|
+
x = symbol("selected", "fixture")
|
|
81
|
+
end
|
|
82
|
+
example `selected identity` is
|
|
83
|
+
x = symbol("selected", "fixture")
|
|
84
|
+
expect x = symbol("selected", "fixture")
|
|
85
|
+
end
|
|
86
|
+
end
|
package/examples-command.mjs
CHANGED
|
@@ -43,7 +43,7 @@ export async function generateExamples(options) {
|
|
|
43
43
|
return plans.map((plan, i) => ({
|
|
44
44
|
target: selected[i],
|
|
45
45
|
directory: plan.root,
|
|
46
|
-
files: artifacts[i].map((f) => ({ path: f.path, ownership: f.ownership })),
|
|
46
|
+
files: artifacts[i].map((f) => ({ path: f.path, ownership: f.ownership, placement: f.placement })),
|
|
47
47
|
changes: plan.changes.length,
|
|
48
48
|
preservedAdapters: plan.preserved,
|
|
49
49
|
adapterUpdates: plan.adapterUpdates,
|
package/index.d.ts
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
// Generated from LawSpec.Gen. Do not edit.
|
|
2
|
-
export type Target = 'java' | 'python' | 'javascript' | 'typescript' | 'go' | 'haskell' | 'kotlin';
|
|
2
|
+
export type Target = 'java' | 'python' | 'javascript' | 'typescript' | 'go' | 'haskell' | 'kotlin' | 'rust';
|
|
3
3
|
export interface Source { path: string; content: string }
|
|
4
4
|
export interface Location { file: string; line: number; column: number }
|
|
5
|
+
export interface Span { start: Location; end: Location }
|
|
5
6
|
export interface Diagnostic { code: string; message: string; at: Location | null }
|
|
6
7
|
export interface Artifact { path: string; content: string; ownership: 'user' | 'generated'; placement: 'source' | 'test' }
|
|
7
|
-
export type Type = {
|
|
8
|
-
export
|
|
9
|
-
export type
|
|
10
|
-
export
|
|
11
|
-
export interface
|
|
8
|
+
export type Type = {kind:'constructor'; name:string; arguments:TypeArgument[]} | {kind:'variable'; id:string} | {kind:'function'; parameter:Type; result:Type};
|
|
9
|
+
export type TypeArgument = {kind:'type'; type:Type} | {kind:'natural'; value:string} | {kind:'indexVariable'; id:string};
|
|
10
|
+
export type Origin = {kind:'source'; span:Span} | {kind:'generated'; declaration:string};
|
|
11
|
+
export type Evidence = {kind:'numeric'|'structural'; type:Type};
|
|
12
|
+
export interface Expr {type:Type; origin:Origin; text:string; node:ExprNode}
|
|
13
|
+
export type ExprNode = {kind:'constant'; value:ScalarValue} | {kind:'local'; id:string} | {kind:'call'; declaration:string; arguments:Expr[]} | {kind:'binary'; operator:string; evidence:Evidence; left:Expr; right:Expr} | {kind:'unary'; operator:'-'|'!'; argument:Expr} | {kind:'shortCircuit'; operator:'&&'|'||'; left:Expr; right:Expr} | {kind:'convert'; conversion:'explicit'|'checked'; argument:Expr} | {kind:'helper'; name:string; arguments:Expr[]};
|
|
14
|
+
export type Assertion = {kind:'equal'; evidence:Evidence; left:Expr; right:Expr} | {kind:'implies'; guard:Expr; body:Assertion} | {kind:'all'; items:Assertion[]};
|
|
12
15
|
export interface Generation {cases:number; maxAttempts:number; maxShrinks:number; exhaustiveLimit:number}
|
|
13
|
-
export interface
|
|
14
|
-
export interface
|
|
16
|
+
export interface Binder {id:string; name:string; type:Type}
|
|
17
|
+
export interface Input extends Binder {predicates:Expr[]; bounds:{operator:string; value:Expr}[]}
|
|
18
|
+
export interface Example {name:string; bindings:(Binder & {value:ScalarValue})[]; expectations:Assertion[]}
|
|
19
|
+
export interface Law {id:string; owner:string; name:string; inputs:Input[]; assertion:Assertion; examples:Example[]; description:string; rationale:string; references:string[]; location:Location; trace:string[]; generation:Generation}
|
|
20
|
+
export interface Unit {id:string; declarations:(Binder & {origin:Origin})[]}
|
|
21
|
+
export interface Contract {id:string; name:string; arguments:Binder[]; result:Binder; preconditions:Expr[]; postconditions:Expr[]}
|
|
22
|
+
export interface Refinement {owner:string; name:string; parameters:{name:string; kind:'type'|'value'; type:string}[]; requirements:{capability:string; type:string}[]; definition:string}
|
|
23
|
+
export interface CheckRequest {sources:Source[]; schemaVersion?:3; machineBits?:32|64; generation?:Partial<Generation>}
|
|
24
|
+
export interface GenerationRequest extends CheckRequest {target:Target; sourceDir?:string; testDir?:string}
|
|
25
|
+
export interface Result {schemaVersion:3; machineBits?:32|64; generation?:Generation; units?:Unit[]; laws?:Law[]; contracts?:{owner:string; contract:Contract}[]; refinements?:Refinement[]; diagnostics:Diagnostic[]; expansions?:string[]; files?:Artifact[]}
|
|
15
26
|
export type IntegerType = 'Int8' | 'Int16' | 'Int32' | 'Int64' | 'UInt8' | 'UInt16' | 'UInt32' | 'UInt64' | 'IntSize' | 'UIntSize' | 'UIntPtr' | 'Integer' | 'BigInt' | 'BigUInt';
|
|
16
27
|
export type ScalarValue = {type: IntegerType; value: string} | {type: 'Bool'; value: boolean} | {type: 'Decimal'; coefficient: string; exponent: string} | {type: 'Rational'; numerator: string; denominator: string} | {type: 'Float32' | 'Float64'; bits: string} | {type: 'Complex64' | 'Complex128'; real: ScalarValue; imaginary: ScalarValue} | {type: 'Char' | 'CodePoint' | 'CodeUnit16'; value: number} | {type: 'Text' | 'CodePointText' | 'Utf16Text' | 'Bytes'; units: number[]} | {type: 'Symbol'; id: string; description: string} | {type: 'Unit' | 'Null' | 'Undefined'} | {type: 'Nullable' | 'Optional'; value: ScalarValue | null};
|
|
17
|
-
export type Expr = {tag: 'Var' | 'StringLit'; contents: string} | {tag: 'Number'; contents: string} | {tag: 'DecimalNumber'; contents: [string, string]} | {tag: 'BoolLit'; contents: boolean} | {tag: 'Apply' | 'Compose'; contents: [Expr, Expr]} | {tag: 'ScalarLit'; contents: ScalarValue} | {tag: 'Annotate'; contents: [Expr, Type]} | {tag: 'Unary'; contents: [string, Expr]} | {tag: 'Binary'; contents: [string, Expr, Expr]} | {tag:'TypeBound'; contents:['min'|'max',Type]};
|
|
18
|
-
export type Definition = {tag: 'Forall'; contents: [[string, Type][], Definition]} | {tag: 'Equal'; contents: [Expr, Expr]} | {tag: 'Holds'; contents: Expr} | {tag: 'Implies'; contents: [Expr, Definition]} | {tag: 'And'; contents: [Definition, Definition]} | {tag: 'Invoke'; contents: [string, Expr[]]};
|
|
19
|
-
export interface Expectation { actual: Expr; expected: ScalarValue }
|
|
20
|
-
export interface Example { exampleName: string; bindings: [string, ScalarValue][]; expectations: Expectation[] }
|
|
21
|
-
export interface Law { lawName: string; parameters: [string, Type][]; requirements: Constraint[]; definition: Definition; description: string; rationale: string; examples: Example[]; references: string[]; location: Location }
|
|
22
|
-
export type Assertion = {tag: 'AssertEqual'; contents: [Expr, Expr]} | {tag: 'AssertImplies'; contents: [Expr, Assertion]} | {tag: 'AssertAll'; contents: Assertion[]};
|
|
23
|
-
export interface Expanded { owner: string; name: string; inputs: Input[]; left: Expr; right: Expr; guards: Expr[]; assertion: Assertion; trace: string[]; original: Law; typedExpressions: TypedExpr[]; propertyKind: 'law'|'contract'; generation:Generation; generationPlan:DomainPlan[] }
|
|
24
|
-
export interface TypedExpr { expressionType: Type; expression: Expr; operands: TypedExpr[]; requiredConversion: Type | null }
|
|
25
|
-
export interface CheckRequest { sources: Source[]; schemaVersion?: 2; machineBits?: 32 | 64; generation?:Partial<Generation> }
|
|
26
|
-
export interface GenerationRequest extends CheckRequest { target: Target; sourceDir?: string; testDir?: string }
|
|
27
|
-
export interface Result { schemaVersion: 2; machineBits?: 32 | 64; generation?:Generation; refinements?:{owner:string; declaration:Refinement}[]; contracts?:{owner:string; contract:Contract}[]; diagnostics: Diagnostic[]; laws?: Expanded[]; expansions?: string[]; files?: Artifact[] }
|
|
28
28
|
export interface Compiler { check(input: CheckRequest): Promise<Result>; expand(input: CheckRequest): Promise<Result>; planGeneration(input: GenerationRequest): Promise<Result> }
|
|
29
29
|
export function createCompiler(): Promise<Compiler>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lawspec",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "State the law once. Check it everywhere.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
"bin": { "lawspec": "bin/lawspec.mjs" },
|
|
9
9
|
"exports": { ".": { "types": "./index.d.ts", "import": "./api.mjs" } },
|
|
10
10
|
"types": "index.d.ts",
|
|
11
|
-
"files": ["*.mjs", "*.json", "index.d.ts", "bin", "core.wasm", "core_jsffi.js", "README.md", "PRIMITIVES.md", "REFINEMENTS.md", "API-MIGRATION.md", "LICENSE", "starter.lawspec", "examples"],
|
|
11
|
+
"files": ["*.mjs", "*.json", "index.d.ts", "bin", "core.wasm", "core_jsffi.js", "README.md", "PRIMITIVES.md", "REFINEMENTS.md", "RUST.md", "LANGUAGE.md", "API-MIGRATION.md", "LICENSE", "starter.lawspec", "examples"],
|
|
12
12
|
"scripts": { "test": "node --test test/*.test.mjs" }
|
|
13
13
|
}
|
package/templates.mjs
CHANGED
|
@@ -6,8 +6,10 @@ export const targets = [
|
|
|
6
6
|
"go",
|
|
7
7
|
"haskell",
|
|
8
8
|
"kotlin",
|
|
9
|
+
"rust",
|
|
9
10
|
];
|
|
10
11
|
export const commands = {
|
|
12
|
+
rust: "cargo test",
|
|
11
13
|
java: "mvn test",
|
|
12
14
|
python: "python -m pytest",
|
|
13
15
|
javascript: "node --test test/*.test.mjs",
|
|
@@ -18,6 +20,7 @@ export const commands = {
|
|
|
18
20
|
kotlin: "gradle test",
|
|
19
21
|
};
|
|
20
22
|
export const setup = {
|
|
23
|
+
rust: "Use Rust 1.85+ with edition 2024, Proptest 1.11.0, num-bigint 0.4.8, num-rational 0.4.2, num-complex 0.4.6, and num-traits 0.2.19. Run cargo test.",
|
|
21
24
|
java: "Use JDK 25+, Maven, JetCheck 0.3.0, JUnit Jupiter 5.14.x, compiler plugin 3.14.1+, Surefire 3.5.x, and maven.compiler.release=25. Run mvn test-compile.",
|
|
22
25
|
python:
|
|
23
26
|
'Use Python 3.13+. Install pytest 8.4.x and Hypothesis 6.x into the selected environment: python -m pip install -e ".[test]". Configure pytest pythonpath=["src"] and testpaths=["tests"]. Set the target python field to the interpreter path if needed.',
|
|
@@ -41,6 +44,27 @@ export function templates(target) {
|
|
|
41
44
|
type: "module",
|
|
42
45
|
};
|
|
43
46
|
switch (target) {
|
|
47
|
+
case "rust":
|
|
48
|
+
return {
|
|
49
|
+
"src/lib.rs": '// Application library. LawSpec maintains the included module declarations.\ninclude!("lawspec_modules.rs");\n',
|
|
50
|
+
"Cargo.toml": `[package]
|
|
51
|
+
name = "lawspec-example"
|
|
52
|
+
version = "0.1.0"
|
|
53
|
+
edition = "2024"
|
|
54
|
+
rust-version = "1.85"
|
|
55
|
+
publish = false
|
|
56
|
+
|
|
57
|
+
[dependencies]
|
|
58
|
+
num-bigint = "=0.4.8"
|
|
59
|
+
num-rational = "=0.4.2"
|
|
60
|
+
num-complex = "=0.4.6"
|
|
61
|
+
num-traits = "=0.2.19"
|
|
62
|
+
|
|
63
|
+
[dev-dependencies]
|
|
64
|
+
proptest = "=1.11.0"
|
|
65
|
+
`,
|
|
66
|
+
};
|
|
67
|
+
|
|
44
68
|
case "javascript":
|
|
45
69
|
return {
|
|
46
70
|
"package.json": json({
|