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 +50 -6
- 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,7 +2,7 @@
|
|
|
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
|
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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": "
|
|
20
|
+
"package.yaml": "f8d2b6bbf4308890903787813994500d4843cd08ee5bc1ae3808a49461d51380",
|
|
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": "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": "
|
|
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": "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.
|
|
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
|
}
|