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 +72 -17
- package/bin/lawspec.mjs +2 -2
- package/build.json +6 -6
- package/core.wasm +0 -0
- package/examples/specs/atoi_codec.lawspec +31 -0
- package/examples/specs/equivalent.lawspec +39 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,34 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
**State the law once. Check it everywhere.**
|
|
4
4
|
|
|
5
|
-
LawSpec 0.
|
|
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
|
-
|
|
11
|
+
Install [LawSpec from npm](https://www.npmjs.com/package/lawspec):
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
|
-
npm
|
|
15
|
-
|
|
14
|
+
npm install --save-dev lawspec@0.2.1
|
|
15
|
+
npx lawspec --version
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
|
26
|
-
|
|
27
|
-
lawspec
|
|
28
|
-
|
|
29
|
-
lawspec
|
|
30
|
-
lawspec
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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": "
|
|
20
|
+
"package.yaml": "2cc5254bf0c1de94adbb98ee193dd45f5d8e5efced9ce255508112ccbca96793",
|
|
21
21
|
"src/LawSpec/Api.hs": "00b1e4bc81231ebd387f10652463904084ebf9c3c4486fd2c2217a6d4dbfc157",
|
|
22
|
-
"src/LawSpec/Compile.hs": "
|
|
23
|
-
"src/LawSpec/Emit.hs": "
|
|
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": "
|
|
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": "
|
|
34
|
-
"npm/core.wasm": "
|
|
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
|
|
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
|
}
|