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/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 showType(type) {
157
- if (type.tag === 'Named' || type.tag === 'Variable') return type.contents;
158
- if (type.tag === 'Applied') return `${type.contents[0]} (${showType(type.contents[1])})`;
159
- return `${showType(type.contents[0])} -> ${showType(type.contents[1])}`;
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.original.examples
181
- .map(
182
- (ex) =>
183
- `\nexample ${JSON.stringify(ex.exampleName)}\n` +
184
- ex.bindings
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.7.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: " +
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.7.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} executable law(s).`,
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 executable law");
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/ScalarEmit.hs",
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": "ca15186d8aa807c58258a4b0fef8fa7d64afaa828f2e71803b4e8741066422fe",
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
- "src/LawSpec/Api.hs": "f1a283fd3696e8e0dd1bd6240ba636a4c1abe19c6e0ea25d6d80a1af89f762cb",
40
- "src/LawSpec/Compile.hs": "a67ee7cb19ebeea644bbf8dbdc0647323a77197b15f8e87fe2227deb6c995b8e",
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": "0b29d449f762af635bb65e5fcbf1f4cfffb1b91b28db115b08b2309557498693",
43
- "src/LawSpec/Eval.hs": "9bdb694352ec4bd7140cdf8ab18045978b7407e93db5b661270160d08e4e9b7d",
44
- "src/LawSpec/Gen.hs": "d4b4e5a21f5a2886dcdc98f56f649980d961c0570d9463154bed459844ab66aa",
45
- "src/LawSpec/Model.hs": "67cad75f1d344bc2dfc997731c37f68994c6a6b568e03209e5b446484ae54674",
46
- "src/LawSpec/NativeScalarEmit.hs": "67f50a7ea381ddada8ecbfc7534917c53f4074f8ce1d4fb6948adb8b7dfb2007",
47
- "src/LawSpec/Parser.hs": "b6e5ee4a8c58b0ab2709420460f1fcabe287714f6defc0fab6cf52ccd0c416a5",
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/Refinement.hs": "060f7bdaaa9cebc17969fa28474e9402cc918ceea0f18cac54050d25010b473c",
50
- "src/LawSpec/RuntimeSources.hs": "4a49d98919971bdbe2b9eabda4eb2e7cda643aaa65ef1a4e6892b6da40ee8acd",
51
- "src/LawSpec/Scalar.hs": "d89d8233abd893f65bb20d4dabe11066340eb51b982ad6fa7bce04d3e22c3547",
52
- "src/LawSpec/ScalarEmit.hs": "e1f05d255db08c8634b6af98fd5d0b938f9a3735f8adf13a952f092a90ee884c",
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": "7520956a91f4b4ec8a6ab12078195cf1243c989b388a3f9d0b10a0fa60193bc3",
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": "afdb97eb4a08de190f7533b7be79c6ba11fab9ef30792aca71ef5fb448001106",
60
- "npm/core.wasm": "32e3f553dc8346a0d7aac63ad556eacc0e791803b8957f0804a609b883ed0ea8",
89
+ "wasm/lawspec-wasm.cabal": "22590b2a6fcd384ef8957bd179e04a055e90535f7eaaa2b5ac3d314fd0b686ed",
90
+ "npm/core.wasm": "09efa9e767c3d33e718a8a0909cdf23b24d158813570cc610f56e72d37a59fa3",
61
91
  "npm/core_jsffi.js": "88d136efe92f7cff5758c8fec8d9b6bbc9707fe37741cebeeb415fe34ce3d72b",
62
- "npm/api.mjs": "d6df654600172131ac66a55b86876fc29cde2a76a0a63bbefff3183488a4837b",
63
- "npm/index.d.ts": "ec19e731ed33e0d0481345888c73778ae0d3f2db58e4c3310ba684b801e698bc"
92
+ "npm/api.mjs": "7415fc057ea5f4da68f9d255ce862152d6bc45d6b0053d44f61f9110911ac64b",
93
+ "npm/index.d.ts": "d19ecaea0be4ca01c8667a76e1bcfe758fd200d728ef67a44ad14e1b1dd15b72"
64
94
  }
65
95
  }
@@ -2,37 +2,120 @@
2
2
  "version": 1,
3
3
  "targets": {
4
4
  "java": {
5
- "java": ["25", "26"],
6
- "jetCheck": ["0.3.0", "0.3.1"],
7
- "junit-jupiter": ["5.14.0", "5.15.0"]
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": ["3.13", "3.15"],
11
- "pytest": ["8.4.0", "8.5.0"],
12
- "hypothesis": ["6.135.26", "7"]
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": ["4", "5"]
33
+ "fast-check": [
34
+ "4",
35
+ "5"
36
+ ]
16
37
  },
17
38
  "typescript": {
18
- "fast-check": ["4", "5"],
19
- "typescript": ["5.9", "5.10"],
20
- "@types/node": ["22", "23"]
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": ["1.22", "1.27"],
24
- "rapid": ["1.2.0", "1.2.1"]
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": ["2.11", "2.12"],
28
- "hedgehog": ["1.5", "1.6"],
29
- "hspec-hedgehog": ["0.3.0.0", "0.3.0.1"],
30
- "ghc": ["9.10", "9.11"]
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": ["25", "26"],
34
- "gradle": ["9.1", "9.4"],
35
- "kotlin": ["2.3.21", "2.3.22"]
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", name === "python" ? "tests" : "test"];
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
- -- Int32 arithmetic in this example wraps modulo 2^32 on every target.
4
- add :: Int32 -> Int32 -> Int32
5
- multiply :: Int32 -> Int32 -> Int32
6
- negateValue :: Int32 -> Int32
7
- maximumValue :: Int32 -> Int32 -> Int32
8
- subtractValue :: Int32 -> Int32 -> Int32
9
- divideLeft :: Int32 -> Int32 -> Int32
10
- divideRight :: Int32 -> Int32 -> Int32
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 :: Int32 -> Int32 -> Int32 -> Int32 -> Int32
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
@@ -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 = {tag: 'Named' | 'Variable'; contents: string} | {tag: 'Arrow'; contents: [Type, Type]} | {tag: 'Applied'; contents: ['Nullable' | 'Optional', Type]} | {tag:'Refined'; contents:[string, Type, Expr | null]} | {tag:'Qualified'; contents:[Constraint[],Type]} | {tag:'CheckedType'; contents:[Expr[],Type]} | {tag:'RefinementApp'; contents:[string,RefinementArgument[]]};
8
- export interface Constraint {tag:'Capability'; contents:['Eq'|'Integer'|'Ordered'|'Bounded',Type]}
9
- export type RefinementArgument = {tag:'TypeArgument'; contents:Type} | {tag:'ValueArgument'; contents:Expr};
10
- export interface Refinement {refinementName:string; refinementParameters:[string,Type][]; refinementRequirements:Constraint[]; refinementBody:Type}
11
- export interface Contract {contractName:string; contractArguments:[string,Type][]; contractResult:[string,Type]; contractPreconditions:Expr[]; contractPostconditions:Expr[]}
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 Input {inputName:string; inputId:string; inputType:Type; inputRefinements:Expr[]}
14
- export interface DomainPlan {domainInput:Input; domainBounds:[string,Expr][]}
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.7.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({