lawspec 0.1.0 → 0.2.1

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,34 +2,41 @@
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
 
9
9
  ## Install and try it
10
10
 
11
- The checkout includes the npm package in `npm/`. Build an installable archive:
11
+ Install [LawSpec from npm](https://www.npmjs.com/package/lawspec):
12
12
 
13
13
  ```sh
14
- npm pack ./npm
15
- npm install --save-dev ./lawspec-0.1.0.tgz
14
+ npm install --save-dev lawspec@0.2.1
15
+ npx lawspec --version
16
16
  ```
17
17
 
18
- No Haskell toolchain is needed to install or run the npm package. Node 22+ and the
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.
18
+ Node 22+ and the selected target's build tools are required. No Haskell toolchain
19
+ is needed to install or run LawSpec. The reference platforms are macOS and Linux.
20
+ Use `npx lawspec` to run the locally installed CLI.
21
21
 
22
- From an empty application directory, use the installed `lawspec` command:
22
+ To try it in a **new, empty directory**, initialize the starter before installing
23
+ local dependencies so LawSpec can create its `package.json` and test script:
23
24
 
24
25
  ```sh
25
- lawspec init --target javascript
26
- npm install
27
- lawspec check
28
- lawspec explain 'example.atoi_codec::itoa and then atoi yields a'
29
- lawspec doctor
30
- lawspec generate
26
+ mkdir lawspec-example
27
+ cd lawspec-example
28
+ npm exec --package=lawspec@0.2.1 -- lawspec init --target javascript
29
+ npm install --save-dev lawspec@0.2.1
30
+ npx lawspec check
31
+ npx lawspec explain 'example.atoi_codec::itoa and then atoi yields a'
32
+ npx lawspec doctor
33
+ npx lawspec generate
31
34
  ```
32
35
 
36
+ In an existing project, install LawSpec first, then run
37
+ `npx lawspec init --target javascript`. Existing build files are preserved;
38
+ apply the printed dependency and test-runner setup instructions before generation.
39
+
33
40
  Implement `src/example/atoi_codec.mjs`, then run `npm test`:
34
41
 
35
42
  ```javascript
@@ -54,7 +61,7 @@ properties with the selected framework's shrinking and failure reporting.
54
61
  | `kotlin` | JDK/JVM 25, Gradle 9.1–9.3, Kotlin 2.3.21 | Kotest 5.9.1 | `gradle test` |
55
62
 
56
63
  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
64
+ through compatibility profiles after testing; v0.2's current JVM profile certifies
58
65
  25. Python templates declare `requires-python = ">=3.13"` and runtime checks
59
66
  currently recognize 3.13 and 3.14. Kotlin templates pin Gradle's supported build
60
67
  configuration to Kotlin 2.3.21 and target JVM 25.
@@ -138,7 +145,8 @@ law `round trip` is
138
145
  end
139
146
  ```
140
147
 
141
- The implicit prelude defines `left inverse` and `round trip identity is preserved`.
148
+ The implicit prelude defines `left inverse`, `round trip identity is preserved`,
149
+ and `equivalent`.
142
150
  A law may reference a local reusable law or a prelude law. The compiler performs
143
151
  capture-avoiding expansion and specializes types; it does not recognize codec
144
152
  function names specially. `explain` shows the final property:
@@ -150,7 +158,7 @@ for all (x :: Int32) . atoi (itoa (x)) = x
150
158
  Reusable laws can declare typed unary function parameters and `requires Eq a`.
151
159
  Definitions support law application, function application/composition, universal
152
160
  quantification, integer literals and equality. Function signatures use `Int32`
153
- and `Text`; generic variables are supported in reusable laws. v0.1 generates
161
+ and `Text`; generic variables are supported in reusable laws. v0.2 generates
154
162
  quantified `Int32` inputs, including multiple inputs. `Text` can be an intermediate
155
163
  or compared result. Functions are synchronous and unary.
156
164
 
@@ -165,6 +173,49 @@ Additional primitives, external law packages, cross-unit imports beyond the
165
173
  prelude, async functions, direct existing-symbol binding and browser hosting are
166
174
  outside this release.
167
175
 
176
+ ## Comparing alternative implementations
177
+
178
+ `equivalent` compares two functions with the same input and output types. Its
179
+ `Eq b` requirement applies to the **result**, so an `Int32 -> Text` comparison
180
+ uses text equality, while `Int32 -> Int32` uses integer equality.
181
+
182
+ ```lawspec
183
+ unit example.formatting
184
+
185
+ render :: Int32 -> Text
186
+ referenceRender :: Int32 -> Text
187
+
188
+ law `decimal renderers agree` is
189
+ definition is
190
+ `equivalent` render referenceRender
191
+ end
192
+ example `negative integer` is
193
+ x = -42
194
+ end
195
+ end
196
+ ```
197
+
198
+ This expands to `for all (x :: Int32) . render (x) = referenceRender (x)`.
199
+ The example inherits the input name `x` from the prelude. Both functions are
200
+ user-owned adapter functions; either may delegate to your existing code.
201
+
202
+ [The complete example](https://github.com/brain-fuel/lawspec/blob/v0.2.1/examples/specs/equivalent.lawspec) compares decimal
203
+ renderers and two implementations that clamp negative integers to zero. For
204
+ JavaScript, their adapters can be:
205
+
206
+ ```javascript
207
+ export const render = x => String(x);
208
+ export const referenceRender = x => x.toString(10);
209
+ export const clamp = x => Math.max(0, x);
210
+ export const referenceClamp = x => x < 0 ? 0 : x;
211
+ ```
212
+
213
+ The same specification generates native tests for all seven targets. The
214
+ integration suite checks both examples with matching implementations, then
215
+ breaks each alternative separately to verify detection. Agreement does not
216
+ establish that either implementation meets an independent specification; two
217
+ implementations can share the same bug. Quantified inputs remain `Int32` in v0.2.
218
+
168
219
  ## Ownership
169
220
 
170
221
  Implementation adapters are created once and belong to you. Implement them or
@@ -203,6 +254,10 @@ by the JS shim.
203
254
 
204
255
  ## Build and verify
205
256
 
257
+ For contributors working from a repository checkout, build a local archive with
258
+ `npm pack ./npm` and install it with `npm install --save-dev ./lawspec-0.2.1.tgz`.
259
+ The package payload lives in `npm/`.
260
+
206
261
  ```sh
207
262
  stack test
208
263
  # With wasm32-wasi-cabal and wasm32-wasi-ghc installed:
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.1\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.1");
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": "2cc5254bf0c1de94adbb98ee193dd45f5d8e5efced9ce255508112ccbca96793",
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": "4e223dfc975c5b66021e7a012a063edd159fe38ca12df7d009aa3de472a2e646",
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": "139de82d2d6063eb82fc86438a82e59d9b9315318009cad199f488bca14cb56d",
34
+ "npm/core.wasm": "943abf2084a7395ef6c5441ef4fd93aa0d57e581a0e71723bf6187afa2c3b48c",
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.1",
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
  }