lawspec 0.1.0 → 0.2.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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **State the law once. Check it everywhere.**
4
4
 
5
- LawSpec 0.1 compiles reusable laws into native property tests, executable examples,
5
+ LawSpec 0.2 compiles reusable laws into native property tests, executable examples,
6
6
  and implementation adapters. The compiler is Haskell, distributed as prebuilt
7
7
  WebAssembly with a Node CLI and an asynchronous, typed JavaScript API.
8
8
 
@@ -12,12 +12,12 @@ The checkout includes the npm package in `npm/`. Build an installable archive:
12
12
 
13
13
  ```sh
14
14
  npm pack ./npm
15
- npm install --save-dev ./lawspec-0.1.0.tgz
15
+ npm install --save-dev ./lawspec-0.2.0.tgz
16
16
  ```
17
17
 
18
18
  No Haskell toolchain is needed to install or run the npm package. Node 22+ and the
19
19
  selected target's build tools are required. The reference platforms are macOS
20
- and Linux. This checkout has not been published to the npm registry.
20
+ and Linux. Stable releases are available as the `lawspec` package on npm.
21
21
 
22
22
  From an empty application directory, use the installed `lawspec` command:
23
23
 
@@ -54,7 +54,7 @@ properties with the selected framework's shrinking and failure reporting.
54
54
  | `kotlin` | JDK/JVM 25, Gradle 9.1–9.3, Kotlin 2.3.21 | Kotest 5.9.1 | `gradle test` |
55
55
 
56
56
  Java 25 and Python 3.13 are the minimum baselines. New JVM releases are admitted
57
- through compatibility profiles after testing; v0.1's current JVM profile certifies
57
+ through compatibility profiles after testing; v0.2's current JVM profile certifies
58
58
  25. Python templates declare `requires-python = ">=3.13"` and runtime checks
59
59
  currently recognize 3.13 and 3.14. Kotlin templates pin Gradle's supported build
60
60
  configuration to Kotlin 2.3.21 and target JVM 25.
@@ -138,7 +138,8 @@ law `round trip` is
138
138
  end
139
139
  ```
140
140
 
141
- The implicit prelude defines `left inverse` and `round trip identity is preserved`.
141
+ The implicit prelude defines `left inverse`, `round trip identity is preserved`,
142
+ and `equivalent`.
142
143
  A law may reference a local reusable law or a prelude law. The compiler performs
143
144
  capture-avoiding expansion and specializes types; it does not recognize codec
144
145
  function names specially. `explain` shows the final property:
@@ -150,7 +151,7 @@ for all (x :: Int32) . atoi (itoa (x)) = x
150
151
  Reusable laws can declare typed unary function parameters and `requires Eq a`.
151
152
  Definitions support law application, function application/composition, universal
152
153
  quantification, integer literals and equality. Function signatures use `Int32`
153
- and `Text`; generic variables are supported in reusable laws. v0.1 generates
154
+ and `Text`; generic variables are supported in reusable laws. v0.2 generates
154
155
  quantified `Int32` inputs, including multiple inputs. `Text` can be an intermediate
155
156
  or compared result. Functions are synchronous and unary.
156
157
 
@@ -165,6 +166,49 @@ Additional primitives, external law packages, cross-unit imports beyond the
165
166
  prelude, async functions, direct existing-symbol binding and browser hosting are
166
167
  outside this release.
167
168
 
169
+ ## Comparing alternative implementations
170
+
171
+ `equivalent` compares two functions with the same input and output types. Its
172
+ `Eq b` requirement applies to the **result**, so an `Int32 -> Text` comparison
173
+ uses text equality, while `Int32 -> Int32` uses integer equality.
174
+
175
+ ```lawspec
176
+ unit example.formatting
177
+
178
+ render :: Int32 -> Text
179
+ referenceRender :: Int32 -> Text
180
+
181
+ law `decimal renderers agree` is
182
+ definition is
183
+ `equivalent` render referenceRender
184
+ end
185
+ example `negative integer` is
186
+ x = -42
187
+ end
188
+ end
189
+ ```
190
+
191
+ This expands to `for all (x :: Int32) . render (x) = referenceRender (x)`.
192
+ The example inherits the input name `x` from the prelude. Both functions are
193
+ user-owned adapter functions; either may delegate to your existing code.
194
+
195
+ [The complete example](examples/specs/equivalent.lawspec) compares decimal
196
+ renderers and two implementations that clamp negative integers to zero. For
197
+ JavaScript, their adapters can be:
198
+
199
+ ```javascript
200
+ export const render = x => String(x);
201
+ export const referenceRender = x => x.toString(10);
202
+ export const clamp = x => Math.max(0, x);
203
+ export const referenceClamp = x => x < 0 ? 0 : x;
204
+ ```
205
+
206
+ The same specification generates native tests for all seven targets. The
207
+ integration suite checks both examples with matching implementations, then
208
+ breaks each alternative separately to verify detection. Agreement does not
209
+ establish that either implementation meets an independent specification; two
210
+ implementations can share the same bug. Quantified inputs remain `Int32` in v0.2.
211
+
168
212
  ## Ownership
169
213
 
170
214
  Implementation adapters are created once and belong to you. Implement them or
package/bin/lawspec.mjs CHANGED
@@ -153,13 +153,13 @@ async function init() {
153
153
  async function main() {
154
154
  if (!verb || ["help", "--help", "-h"].includes(verb)) {
155
155
  output(
156
- "LawSpec 0.1.0\nUsage: lawspec init --target <language> [--project <directory>]\n lawspec check | doctor | explain <unit>::<law> | generate\nOptions: --config <path>, --target <language>, --json\nGeneration: --dry-run, --check\nTargets: " +
156
+ "LawSpec 0.2.0\nUsage: lawspec init --target <language> [--project <directory>]\n lawspec check | doctor | explain <unit>::<law> | generate\nOptions: --config <path>, --target <language>, --json\nGeneration: --dry-run, --check\nTargets: " +
157
157
  targets.join(", "),
158
158
  );
159
159
  return;
160
160
  }
161
161
  if (verb === "--version") {
162
- output("0.1.0");
162
+ output("0.2.0");
163
163
  return;
164
164
  }
165
165
  if (positional.length > (verb === "explain" ? 1 : 0))
package/build.json CHANGED
@@ -17,21 +17,21 @@
17
17
  "wasm/lawspec-wasm.cabal"
18
18
  ],
19
19
  "digests": {
20
- "package.yaml": "cb68fcf21def8f7a880bbab7088208a86fcf313a83fe2c02e18957872cf65c67",
20
+ "package.yaml": "f8d2b6bbf4308890903787813994500d4843cd08ee5bc1ae3808a49461d51380",
21
21
  "src/LawSpec/Api.hs": "00b1e4bc81231ebd387f10652463904084ebf9c3c4486fd2c2217a6d4dbfc157",
22
- "src/LawSpec/Compile.hs": "5ea31f4a66df87ddba2ff0b498d12eea5cbbefb0e10e3bf1c1d2bf4a88f5792e",
23
- "src/LawSpec/Emit.hs": "27ba0cafbea8d5a82aac439f4e72f98e003cfdc177f99a902f8d11f033684b8e",
22
+ "src/LawSpec/Compile.hs": "c1727ba7d6ece9725567f9561d341d8660b3293ff845c2de1d936ebdd2cd5fc6",
23
+ "src/LawSpec/Emit.hs": "24edca3a078ea10349b3a229b56464c37bfd5303c688a11ed45f9479ce3b7bd1",
24
24
  "src/LawSpec/Gen.hs": "b283001bc18971ddb2159f2ec7e51ac58aa7e191b52577006140694b09a06987",
25
25
  "src/LawSpec/Model.hs": "8f501e134cd1480261ae8bb9e3df8255efd38a02eb883468c6bc2c342e200970",
26
26
  "src/LawSpec/Parser.hs": "f546502c6eedb0a4ccd30d11e3b017aedf5a7145080ea58cfd34753fd9b509c6",
27
- "src/LawSpec/Prelude.hs": "d4b0ae08eab0f30b4b8c819ce360f7cccb1f77d5ea38ded3b88dbb07936b10b6",
27
+ "src/LawSpec/Prelude.hs": "f04078dbc03eb793f9f3343db562cf56f84b5fbe6fd4aea6481bad38a7c6f328",
28
28
  "stack.yaml": "20ccf4d599e355e60b7aa4f814a7cd4299fe2048616cc2e6dbdc22a7bd8cec73",
29
29
  "stack.yaml.lock": "ae222b9c81af920c56e50fa4596fa57786e7fa5a7b461390362b2b2ff63818c7",
30
30
  "wasm/app/Exports.hs": "4ecbdac8faa2449e6fc61b93c82e6278fa14374154f29b4dacf0eef433f06a47",
31
31
  "wasm/cabal.project": "021e560afdc5eb4cb7169e7119ecb8f92c9ee170245af909c94b612941bff5cc",
32
32
  "wasm/cabal.project.freeze": "733dbed3d2ecccb26e874fd58f136296dad772184deb1658c196b5d54a0814dc",
33
- "wasm/lawspec-wasm.cabal": "d11ccf7991e406f0fd26f124c54d6552613b892328f8e22a964e38d03c7d6de4",
34
- "npm/core.wasm": "14d88c08f92b10ac2939d6b92bc428b0ee255d56e88890fd46abb1e959a26b2b",
33
+ "wasm/lawspec-wasm.cabal": "48a9a7587f183565043e313e20e531c72d2eeb4ec706a1052ff0e7b461961002",
34
+ "npm/core.wasm": "fe4048e604bbdf5ddbecf9ea2e9b27ac60161788fff35524cbcf9825e2e8faeb",
35
35
  "npm/core_jsffi.js": "88d136efe92f7cff5758c8fec8d9b6bbc9707fe37741cebeeb415fe34ce3d72b",
36
36
  "npm/api.mjs": "d6df654600172131ac66a55b86876fc29cde2a76a0a63bbefff3183488a4837b",
37
37
  "npm/index.d.ts": "8003304d88dbb01a07bd3dbdc4d680a193db1ec323c5712b9860e166ca9cc94b"
package/core.wasm CHANGED
Binary file
@@ -0,0 +1,31 @@
1
+ unit example.atoi_codec
2
+
3
+ itoa :: Int32 -> Text
4
+ atoi :: Text -> Int32
5
+
6
+ law `itoa and then atoi yields a` is
7
+ definition is
8
+ `round trip identity is preserved` itoa atoi
9
+ end
10
+
11
+ description is
12
+ "converting an Int32 to Text with {itoa} and then back with {atoi} yields the original Int32"
13
+ end
14
+
15
+ rationale is
16
+ "representing an Int32 as Text must not change its value"
17
+ end
18
+
19
+ example `negative integer` is
20
+ x = -42
21
+ end
22
+
23
+ example `zero` is
24
+ x = 0
25
+ end
26
+
27
+ references are
28
+ "left inverse"
29
+ "round-trip property"
30
+ end
31
+ end
@@ -0,0 +1,39 @@
1
+ unit example.alternatives
2
+
3
+ render :: Int32 -> Text
4
+ referenceRender :: Int32 -> Text
5
+ clamp :: Int32 -> Int32
6
+ referenceClamp :: Int32 -> Int32
7
+
8
+ law `decimal renderers agree` is
9
+ definition is
10
+ `equivalent` render referenceRender
11
+ end
12
+ description is
13
+ "{render} agrees with {referenceRender} on decimal formatting"
14
+ end
15
+ rationale is
16
+ "changing the formatting implementation must preserve its result"
17
+ end
18
+ example `negative integer` is
19
+ x = -42
20
+ end
21
+ example `zero` is
22
+ x = 0
23
+ end
24
+ end
25
+
26
+ law `nonnegative clamps agree` is
27
+ definition is
28
+ `equivalent` clamp referenceClamp
29
+ end
30
+ description is
31
+ "{clamp} agrees with {referenceClamp} when clamping negative inputs to zero"
32
+ end
33
+ example `negative boundary` is
34
+ x = -2147483648
35
+ end
36
+ example `positive boundary` is
37
+ x = 2147483647
38
+ end
39
+ end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lawspec",
3
- "version": "0.1.0",
3
+ "version": "0.2.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", "LICENSE", "starter.lawspec"],
11
+ "files": ["*.mjs", "*.json", "index.d.ts", "bin", "core.wasm", "core_jsffi.js", "README.md", "LICENSE", "starter.lawspec", "examples"],
12
12
  "scripts": { "test": "node --test test/*.test.mjs" }
13
13
  }