lawspec 0.4.0 → 0.6.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.4 compiles reusable laws into native property tests, executable examples,
5
+ LawSpec 0.6 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
 
@@ -11,7 +11,7 @@ WebAssembly with a Node CLI and an asynchronous, typed JavaScript API.
11
11
  Install [LawSpec from npm](https://www.npmjs.com/package/lawspec):
12
12
 
13
13
  ```sh
14
- npm install --save-dev lawspec@0.4.0
14
+ npm install --save-dev lawspec@0.6.0
15
15
  npx lawspec --version
16
16
  ```
17
17
 
@@ -25,8 +25,8 @@ local dependencies so LawSpec can create its `package.json` and test script:
25
25
  ```sh
26
26
  mkdir lawspec-example
27
27
  cd lawspec-example
28
- npm exec --package=lawspec@0.4.0 -- lawspec init --target javascript
29
- npm install --save-dev lawspec@0.4.0
28
+ npm exec --package=lawspec@0.6.0 -- lawspec init --target javascript
29
+ npm install --save-dev lawspec@0.6.0
30
30
  npx lawspec check
31
31
  npx lawspec explain 'example.atoi_codec::itoa and then atoi yields a'
32
32
  npx lawspec doctor
@@ -52,7 +52,7 @@ properties with the selected framework's shrinking and failure reporting.
52
52
 
53
53
  | Target | Build setup | Test libraries | Test command |
54
54
  | --- | --- | --- | --- |
55
- | `java` | Maven, JDK 25, release 25 | JetCheck 0.4.0, JUnit Jupiter 5.14.x | `mvn test` |
55
+ | `java` | Maven, JDK 25, release 25 | JetCheck 0.3.0, JUnit Jupiter 5.14.x | `mvn test` |
56
56
  | `python` | Python 3.13 or 3.14, pyproject | pytest 8.4.x, Hypothesis 6.135.26+ (6.x) | `python -m pytest` |
57
57
  | `javascript` | Node 22+, npm, ESM | fast-check 4.x, node:test | `npm test` |
58
58
  | `typescript` | Node 22+, npm, TypeScript 5.9.x, ESM | fast-check 4.x, node:test | `npm test` |
@@ -61,7 +61,7 @@ properties with the selected framework's shrinking and failure reporting.
61
61
  | `kotlin` | JDK/JVM 25, Gradle 9.1–9.3, Kotlin 2.3.21 | Kotest 5.9.1 | `gradle test` |
62
62
 
63
63
  Java 25 and Python 3.13 are the minimum baselines. New JVM releases are admitted
64
- through compatibility profiles after testing; v0.4's current JVM profile certifies
64
+ through compatibility profiles after testing; v0.6's current JVM profile certifies
65
65
  25. Python templates declare `requires-python = ">=3.13"` and runtime checks
66
66
  currently recognize 3.13 and 3.14. Kotlin templates pin Gradle's supported build
67
67
  configuration to Kotlin 2.3.21 and target JVM 25.
@@ -159,11 +159,12 @@ for all (x :: Int32) . atoi (itoa (x)) = x
159
159
 
160
160
  Reusable laws can declare typed unary function parameters and `requires Eq a`.
161
161
  Definitions support law application, function application/composition, universal
162
- quantification, integer and text literals, and equality. Function signatures use `Int32`
163
- and `Text`; generic variables are supported in reusable laws. v0.4 generates
164
- quantified `Int32` and `Text` inputs, including mixed and multiple inputs. Both
165
- types can also be intermediate or compared results. Functions are synchronous
166
- and unary. Text literals are double-quoted, with escapes such as `\"`, `\\`,
162
+ quantification, `implies`, Boolean predicates, scalar literals, and equality.
163
+ Function signatures use `Int32`, `Text`, and `Bool`; generic variables are
164
+ supported in reusable laws. v0.6 generates quantified inputs of all three types,
165
+ including mixed and multiple inputs. These types can also be intermediate or
166
+ compared results. Functions are synchronous and support curried signatures with any positive
167
+ number of scalar arguments. Text literals are double-quoted, with escapes such as `\"`, `\\`,
167
168
  `\n`, and `\t`; examples must bind each input to a literal of its declared type.
168
169
  Text values contain Unicode scalar values; surrogate code points are rejected.
169
170
 
@@ -178,11 +179,219 @@ Additional primitives, external law packages, cross-unit imports beyond the
178
179
  prelude, async functions, direct existing-symbol binding and browser hosting are
179
180
  outside this release.
180
181
 
181
- ## Expected results and migration to 0.4
182
+ ## Algebra and currying (0.6)
183
+
184
+ Version 0.6 adds algebra laws, scalar law parameters, curried signatures,
185
+ and conjunctions.
186
+
187
+ ```lawspec
188
+ unit example.addition
189
+
190
+ add :: Int32 -> Int32 -> Int32
191
+
192
+ law `addition commutes` is
193
+ definition is
194
+ `commutative` add
195
+ end
196
+
197
+ example `3 plus 5 and 5 plus 3 both produce 8` is
198
+ x = 3
199
+ y = 5
200
+ expect add x y = 8
201
+ expect add y x = 8
202
+ end
203
+ end
204
+
205
+ law `zero is an identity on both sides` is
206
+ definition is
207
+ `identity` add 0
208
+ end
209
+
210
+ example `zero preserves 3 on either side` is
211
+ x = 3
212
+ expect add 0 x = 3
213
+ expect add x 0 = 3
214
+ end
215
+ end
216
+ ```
217
+
218
+ Arrows associate to the right and application associates to the left:
219
+ `f :: a -> b -> c` takes two arguments, and `f x y` means `(f x) y`.
220
+ A partial application such as `add 1` can be passed to a reusable unary law;
221
+ `sumFour 1 2` can be passed to a binary law. Partial applications also compose.
222
+ The compiler specializes these expressions before emission. Java, Kotlin,
223
+ Python, JavaScript, TypeScript and Go adapters take ordinary positional arguments
224
+ (`add(x, y)`); Haskell adapters use native currying (`add x y`). Argument order
225
+ and types are preserved, including mixtures of `Text`, `Bool` and `Int32`.
226
+
227
+ Law parameters can also be scalar values: `(e :: a)` supplies an identity and
228
+ `(zero :: a)` supplies an absorbing element. Pass literals directly, for example
229
+ `left identity` with arguments `add 0`, or `absorbing element` with `multiply 0`.
230
+ Functions declared by a unit take one or more scalar inputs and return a scalar;
231
+ reusable laws accept these curried functions, their partial applications, and
232
+ scalar parameters. Quantified test inputs remain scalar.
233
+
234
+ The prelude defines the following laws. Every row has an executable example in
235
+ [algebra.lawspec](https://github.com/brain-fuel/lawspec/blob/v0.6.0/examples/specs/algebra.lawspec), including both sides of every
236
+ combined law. `f` and `g` are binary operations, `inverse` is unary, and `e` and
237
+ `zero` are scalar parameters. All these laws require equality of the element type.
238
+
239
+ | Law and arguments | Equations checked for every quantified input |
240
+ | --- | --- |
241
+ | `commutative f` | `f x y = f y x` |
242
+ | `associative f` | `f (f x y) z = f x (f y z)` |
243
+ | `left identity f e` | `f e x = x` |
244
+ | `right identity f e` | `f x e = x` |
245
+ | `identity f e` | Both identity equations |
246
+ | `left absorbing element f zero` | `f zero x = zero` |
247
+ | `right absorbing element f zero` | `f x zero = zero` |
248
+ | `absorbing element f zero` | Both absorbing equations |
249
+ | `left distributive f g` | `f x (g y z) = g (f x y) (f x z)` |
250
+ | `right distributive f g` | `f (g x y) z = g (f x z) (f y z)` |
251
+ | `distributive f g` | Both distributive equations |
252
+ | `idempotent operation f` | `f x x = x` (the existing `idempotent` law is unary) |
253
+ | `left inverse element f inverse e` | `f (inverse x) x = e` |
254
+ | `right inverse element f inverse e` | `f x (inverse x) = e` |
255
+ | `invertible f inverse e` | Both inverse equations |
256
+ | `left division f divideLeft` | `f x (divideLeft x y) = y` and `divideLeft x (f x y) = y` |
257
+ | `right division f divideRight` | `f (divideRight x y) y = x` and `divideRight (f x y) y = x` |
258
+ | `divisible f divideLeft divideRight` | All four division equations |
259
+ | `involution f` | `f (f x) = x` |
260
+
261
+ Here **divisible** means algebraic left/right division. `divideLeft x y` solves
262
+ `f x result = y`; `divideRight x y` solves `f result y = x`. The subtraction
263
+ example deliberately uses a noncommutative operation: with `x = 3` and `y = 5`,
264
+ the left solution is `-2` and the right solution is `8`. Recovery is checked in
265
+ both directions. These are total laws; a partially defined division needs an
266
+ explicit domain predicate and conditional equations.
267
+
268
+ `invertible` checks the supplied inverse operation. Check `identity` and
269
+ `associative` as well when specifying a group. The prelude states contracts;
270
+ it does not supply arithmetic implementations or prove a structure from random
271
+ tests. The numeric examples use Int32 arithmetic modulo 2^32 so their laws hold
272
+ at overflow boundaries on every target. JavaScript uses `Math.imul` for products,
273
+ and Python explicitly wraps results into the signed Int32 range in the test
274
+ adapters.
275
+
276
+ Use `and` to require multiple conclusions in one law. For example:
277
+
278
+ ```lawspec
279
+ unit example.absorption
280
+ multiply :: Int32 -> Int32 -> Int32
281
+
282
+ law `zero absorbs on both sides` is
283
+ definition is
284
+ `for all` (x :: Int32) .
285
+ multiply 0 x = 0 and multiply x 0 = 0
286
+ end
287
+
288
+ example `3 times zero and zero times 3 both produce zero` is
289
+ x = 3
290
+ expect multiply 0 x = 0
291
+ expect multiply x 0 = 0
292
+ end
293
+ end
294
+ ```
295
+
296
+ Quantification and implication extend through the following conjunction:
297
+ `p x implies A and B` guards both conclusions. Write `(p x implies A) and B`
298
+ to guard only the first. A shared guard runs once per check; false guards skip
299
+ their entire consequence. Every conjunct is type-checked and emitted. As with
300
+ existing assertions, the first failure stops that individual test. `and` is now
301
+ a reserved word.
302
+
303
+ [Currying examples](https://github.com/brain-fuel/lawspec/blob/v0.6.0/examples/specs/currying.lawspec) demonstrate a four-argument
304
+ function partially applied twice, a formatter with four heterogeneous arguments,
305
+ and composition after partial application. Each example states its exact outputs.
306
+ Run `node npm/bin/lawspec.mjs examples` after rebuilding to inspect all nine units
307
+ in all seven target languages (126 artifacts).
308
+
309
+ The expanded API's **`assertion` tree is authoritative**: `AssertEqual` contains
310
+ two expressions, `AssertImplies` contains a condition and consequence, and
311
+ `AssertAll` contains every conjunct. Existing `left`, `right`, and `guards`
312
+ fields are compatibility projections of the first conclusion only; consumers
313
+ checking compound laws must traverse `assertion`. Source definitions add `And`.
314
+ `lawspec explain` prints the full conjunction and its conditional scope.
315
+
316
+ ## Predicates and conditional laws (0.5)
317
+
318
+ A predicate is a unary function returning `Bool`. Use `true` and `false` in
319
+ expressions, example bindings, and expected results. A Boolean expression can
320
+ stand alone as a law's definition: it must evaluate to `true`.
321
+
322
+ `condition implies consequence` checks the consequence only when the condition
323
+ is true. Conditions must have type `Bool`; nested implications short-circuit in
324
+ source order. The consequence can be an equality, another implication, a Boolean
325
+ predicate, or a reusable law application. Quantify any inputs before using them.
326
+
327
+ ```lawspec
328
+ unit example.parse_port
329
+
330
+ validPort :: Int32 -> Bool
331
+ render :: Int32 -> Text
332
+ parse :: Text -> Int32
333
+
334
+ law `valid ports round trip` is
335
+ definition is
336
+ `for all` (x :: Int32) .
337
+ validPort x implies
338
+ parse (render x) = x
339
+ end
340
+
341
+ example `ordinary port` is
342
+ x = 443
343
+ expect validPort x = true
344
+ expect render x = "443"
345
+ expect parse (render x) = 443
346
+ end
347
+
348
+ example `zero is rejected; the round trip is skipped` is
349
+ x = 0
350
+ expect validPort x = false
351
+ end
352
+ end
353
+ ```
354
+
355
+ The [complete port example](https://github.com/brain-fuel/lawspec/blob/v0.6.0/examples/specs/parse_port.lawspec)
356
+ defines valid ports as 1–65535, and covers both endpoints, ordinary ports, zero,
357
+ negative values, and 65536. All explicit `expect` assertions run regardless of
358
+ the law's condition. A false condition skips only the consequence: invalid ports
359
+ never reach `render` or `parse` through the law. Predicate errors still fail the
360
+ test; they are not treated as false.
361
+
362
+ Implication is logical implication, not generator filtering or an assumption.
363
+ Randomized tests still sample the full input domain and count a false condition
364
+ as satisfying the law. A narrow predicate may therefore exercise few or no
365
+ consequences during a random run. Explicit valid examples ensure the important
366
+ cases run, and expectations of both `true` and `false` catch always-false and
367
+ always-true predicate implementations.
368
+
369
+ The prelude includes `satisfies predicate` (the predicate holds for every input)
370
+ and `left inverse when predicate parse render` (the guarded round trip above).
371
+ These reusable laws preserve the condition and its lexical bindings when expanded.
372
+ `equivalent` can also compare two predicates, since `Bool` supports equality.
373
+ The [Boolean flags example](https://github.com/brain-fuel/lawspec/blob/v0.6.0/examples/specs/boolean_flags.lawspec)
374
+ checks that flipping twice restores both `false` and `true`; all targets generate
375
+ Boolean property inputs and explicit tests for both Boolean boundary values. Java caps Boolean-only JetCheck runs at the number of
376
+ possible input combinations (up to 100), avoiding generator exhaustion.
377
+
378
+ In 0.5, `implies`, `true`, and `false` become reserved words. Existing 0.4 specs
379
+ that use those words as identifiers need renaming. The API adds `BoolLit`,
380
+ `Holds`, and `Implies` AST variants, Boolean literal values, and an ordered
381
+ `guards` array on expanded laws. `lawspec explain` prints the conditions.
382
+
383
+ Kotlin adapters now group functions in an `object` named after the unit (for
384
+ example, `object ParsePort` in package `example`). This allows both the port and
385
+ alternatives units to define `render(Int)`. When upgrading a Kotlin project,
386
+ move existing top-level adapter functions into the indicated object; generation
387
+ preserves your adapter and reports the required stub shape. Generated tests call
388
+ `ParsePort.validPort(...)`, `ParsePort.render(...)`, and `ParsePort.parse(...)`.
389
+
390
+ ## Expected results and migration from 0.3
182
391
 
183
392
  Every `example` must bind all quantified inputs and then include one or more
184
393
  `expect <expression> = <literal>` assertions. The expected literal must have the
185
- same `Int32` or `Text` type as the expression. Expressions can reference the
394
+ same `Int32`, `Text`, or `Bool` type as the expression. Expressions can reference the
186
395
  example's inputs and the unit's functions, including composed function calls.
187
396
  Input names shadow function names within expectations, following lexical scope.
188
397
 
@@ -240,7 +449,7 @@ This expands to `for all (x :: Int32) . render (x) = referenceRender (x)`.
240
449
  The example inherits the input name `x` from the prelude. Both functions are
241
450
  user-owned adapter functions; either may delegate to your existing code.
242
451
 
243
- [The complete example](https://github.com/brain-fuel/lawspec/blob/v0.4.0/examples/specs/equivalent.lawspec) compares decimal
452
+ [The complete example](https://github.com/brain-fuel/lawspec/blob/v0.6.0/examples/specs/equivalent.lawspec) compares decimal
244
453
  renderers and two implementations that clamp negative integers to zero. For
245
454
  JavaScript, their adapters can be:
246
455
 
@@ -255,7 +464,7 @@ The same specification generates native tests for all seven targets. The
255
464
  integration suite checks both examples with matching implementations, then
256
465
  breaks each alternative separately to verify detection. The general equivalence law alone does not establish independent correctness;
257
466
  two implementations can share the same bug. Explicit expectations additionally
258
- check the specified outputs at the supplied example inputs. Quantified inputs can be `Int32` or `Text`.
467
+ check the specified outputs at the supplied example inputs. Quantified inputs can be `Int32`, `Text`, or `Bool`.
259
468
 
260
469
  ## Text properties and idempotence
261
470
 
@@ -277,7 +486,7 @@ law `normalizers agree` is
277
486
  end
278
487
  ```
279
488
 
280
- The [slug example](https://github.com/brain-fuel/lawspec/blob/v0.4.0/examples/specs/slug.lawspec)
489
+ The [slug example](https://github.com/brain-fuel/lawspec/blob/v0.6.0/examples/specs/slug.lawspec)
281
490
  compares two implementations of ASCII-space replacement. It includes empty,
282
491
  Unicode and escaped text. Each target uses its native string generator:
283
492
  JetCheck `Generator.stringsOf(Generator.asciiPrintableChars())`, Hypothesis `st.text()`, fast-check `fc.string()`,
@@ -304,7 +513,7 @@ law `canonicalization reaches a fixed point` is
304
513
  end
305
514
  ```
306
515
 
307
- The [canonical URL example](https://github.com/brain-fuel/lawspec/blob/v0.4.0/examples/specs/canonical_url.lawspec)
516
+ The [canonical URL example](https://github.com/brain-fuel/lawspec/blob/v0.6.0/examples/specs/canonical_url.lawspec)
308
517
  uses removal of **all trailing slashes** as a small fixed-point demonstration,
309
518
  not a complete URL canonicalization algorithm. For JavaScript:
310
519
 
@@ -313,10 +522,10 @@ export const canonicalize = value => value.replace(/\/+$/, "");
313
522
  ```
314
523
 
315
524
  Removing just one trailing slash fails the supplied repeated-slash example.
316
- The [mixed-input example](https://github.com/brain-fuel/lawspec/blob/v0.4.0/examples/specs/mixed_inputs.lawspec)
525
+ The [mixed-input example](https://github.com/brain-fuel/lawspec/blob/v0.6.0/examples/specs/mixed_inputs.lawspec)
317
526
  shows `Text` and `Int32` in the same quantified property and executable example.
318
- The JavaScript API represents input bindings and expected values as `number | string`.
319
- Each example includes `expectations: { actual: Expr; expected: number | string }[]`.
527
+ The JavaScript API represents input bindings and expected values as `number | string | boolean`.
528
+ Each example includes `expectations: { actual: Expr; expected: number | string | boolean }[]`.
320
529
 
321
530
  ## Generate all example artifacts
322
531
 
@@ -378,7 +587,7 @@ by the JS shim.
378
587
  ## Build and verify
379
588
 
380
589
  For contributors working from a repository checkout, build a local archive with
381
- `npm pack ./npm` and install it with `npm install --save-dev ./lawspec-0.4.0.tgz`.
590
+ `npm pack ./npm` and install it with `npm install --save-dev ./lawspec-0.6.0.tgz`.
382
591
  The package payload lives in `npm/`.
383
592
 
384
593
  ```sh
package/bin/lawspec.mjs CHANGED
@@ -154,7 +154,11 @@ async function init() {
154
154
  function showExpression(expr) {
155
155
  const value = expr.contents;
156
156
  if (expr.tag === "Var") return value;
157
- if (expr.tag === "Number" || expr.tag === "StringLit")
157
+ if (
158
+ expr.tag === "Number" ||
159
+ expr.tag === "StringLit" ||
160
+ expr.tag === "BoolLit"
161
+ )
158
162
  return JSON.stringify(value);
159
163
  if (expr.tag === "Apply")
160
164
  return `${showExpression(value[0])} (${showExpression(value[1])})`;
@@ -181,13 +185,13 @@ function explainExamples(law) {
181
185
  async function main() {
182
186
  if (!verb || ["help", "--help", "-h"].includes(verb)) {
183
187
  output(
184
- "LawSpec 0.4.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>, --json\nGeneration: --dry-run, --check\nTargets: " +
188
+ "LawSpec 0.6.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>, --json\nGeneration: --dry-run, --check\nTargets: " +
185
189
  targets.join(", "),
186
190
  );
187
191
  return;
188
192
  }
189
193
  if (verb === "--version") {
190
- output("0.4.0");
194
+ output("0.6.0");
191
195
  return;
192
196
  }
193
197
  if (positional.length > (verb === "explain" ? 1 : 0))
@@ -207,7 +211,7 @@ async function main() {
207
211
  : result
208
212
  .map(
209
213
  (r) =>
210
- `${r.target}: ${r.files.length} artifacts in ${r.directory}; ${r.preservedAdapters.length} user adapters preserved.`,
214
+ `${r.target}: ${r.files.length} artifacts in ${r.directory}; ${r.preservedAdapters.length} user adapters preserved.${r.adapterUpdates.length ? "\nReview required adapter signatures:\n" + r.adapterUpdates.map((a) => a.path + "\n" + a.requiredAdapter).join("\n") : ""}`,
211
215
  )
212
216
  .join("\n") +
213
217
  "\nInspection artifacts only; native toolchains and dependencies are not checked. Stubs must be implemented before running tests.",
package/build.json CHANGED
@@ -17,23 +17,23 @@
17
17
  "wasm/lawspec-wasm.cabal"
18
18
  ],
19
19
  "digests": {
20
- "package.yaml": "2c1176c912901bd218743a88df1b67fb1ee8d58ed73fcbe7e2f23827f10a3a0f",
20
+ "package.yaml": "25d79d741039fe21e1164d789fdf4f698f135df7e4c96c82560e4f7efe8fbdf9",
21
21
  "src/LawSpec/Api.hs": "00b1e4bc81231ebd387f10652463904084ebf9c3c4486fd2c2217a6d4dbfc157",
22
- "src/LawSpec/Compile.hs": "336eca415ed6c8594612ad5acfc7ff6f29ef387cf3cb374b60b2407f811c31c7",
23
- "src/LawSpec/Emit.hs": "2e23ca38aa5c4c65305283705633209843dd6ba116c43ba8a42ee8731040f7c3",
24
- "src/LawSpec/Gen.hs": "305d7710dbc69b25d95fa6c0ac9a59622481f6b89682c1e5c9048def3962315a",
25
- "src/LawSpec/Model.hs": "d1eda2b420edbc327b1adbf437c6f39a9e576ef0ab9b82f468c28202a433eb96",
26
- "src/LawSpec/Parser.hs": "7fef1876c52d2204f852704150288910bb0c12bcde1d3159219ad25526a879b6",
27
- "src/LawSpec/Prelude.hs": "0b60dd85bdf0642077dfb497043c63606d507e71e12d3ed8bfe2bf773214c66b",
22
+ "src/LawSpec/Compile.hs": "64aa042ce2f0f460d691ea43fa27bf75576a49a6e852a068f62a2cf5a28e81b7",
23
+ "src/LawSpec/Emit.hs": "85ab85d789c4e2c98703e2768075cb1c0bed42b987c45b5060b9a9df56e8d291",
24
+ "src/LawSpec/Gen.hs": "c596e411790a47cb15b2945d391b9909cbec1972bf6e502ed8c069db2c67e991",
25
+ "src/LawSpec/Model.hs": "549f553d1cc5d65c804a5acfac0cc06e63ff10eb80674893e32fcd3c0a71a5b3",
26
+ "src/LawSpec/Parser.hs": "ae7c5ad29871b99441db2dae10548afafb2ae4931dd1a62208ca0f5e3545002c",
27
+ "src/LawSpec/Prelude.hs": "b1e155c12f58e7c346551a0c65c5d5af76d045b3f1625977ca2a5d1c97cb8c46",
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": "098f1d0fc52575907312c95095846d1d4f6e7ace7cfbe0419a305b9fe8a75a92",
34
- "npm/core.wasm": "0a4ce15cefcd866cd7dd584c32d683c7f8a46823104afcceba1298570883a88a",
33
+ "wasm/lawspec-wasm.cabal": "d0d9c395b96de75264d5dc62f7995f02b285aebc696a2cf603a62b4e280f9132",
34
+ "npm/core.wasm": "e90e71610ca9f58a1b1e88d65f593c74d2a5b09347a4b47802bd771c954b7e42",
35
35
  "npm/core_jsffi.js": "88d136efe92f7cff5758c8fec8d9b6bbc9707fe37741cebeeb415fe34ce3d72b",
36
36
  "npm/api.mjs": "d6df654600172131ac66a55b86876fc29cde2a76a0a63bbefff3183488a4837b",
37
- "npm/index.d.ts": "85ad409ae09d243d3b3f21243d0784390d7b0209e2a09fc7c3b733e8dd2e8765"
37
+ "npm/index.d.ts": "628016928dc71dc735d59d15ccde45500d9a4133e3b20c8d3c21cb9c65ccb604"
38
38
  }
39
39
  }
package/core.wasm CHANGED
Binary file
@@ -0,0 +1,254 @@
1
+ unit example.algebra
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
11
+
12
+ law `addition is commutative` is
13
+ definition is
14
+ `commutative` add
15
+ end
16
+
17
+ example `3 plus 5 and 5 plus 3 both produce 8` is
18
+ x = 3
19
+ y = 5
20
+ expect add x y = 8
21
+ expect add y x = 8
22
+ end
23
+ end
24
+
25
+ law `addition is associative` is
26
+ definition is
27
+ `associative` add
28
+ end
29
+
30
+ example `both groupings of 3 plus 5 plus 7 produce 15` is
31
+ x = 3
32
+ y = 5
33
+ z = 7
34
+ expect add (add x y) z = 15
35
+ expect add x (add y z) = 15
36
+ end
37
+ end
38
+
39
+ law `zero is left identity for addition` is
40
+ definition is
41
+ `left identity` add 0
42
+ end
43
+
44
+ example `adding zero on left preserves 3` is
45
+ x = 3
46
+ expect add 0 x = 3
47
+ end
48
+ end
49
+
50
+ law `zero is right identity for addition` is
51
+ definition is
52
+ `right identity` add 0
53
+ end
54
+
55
+ example `adding zero on right preserves 3` is
56
+ x = 3
57
+ expect add x 0 = 3
58
+ end
59
+ end
60
+
61
+ law `zero is two-sided identity for addition` is
62
+ definition is
63
+ `identity` add 0
64
+ end
65
+
66
+ example `adding zero on both sides preserves 3` is
67
+ x = 3
68
+ expect add 0 x = 3
69
+ expect add x 0 = 3
70
+ end
71
+ end
72
+
73
+ law `zero absorbs multiplication left` is
74
+ definition is
75
+ `left absorbing element` multiply 0
76
+ end
77
+
78
+ example `zero on left of multiplication produces zero` is
79
+ x = 3
80
+ expect multiply 0 x = 0
81
+ end
82
+ end
83
+
84
+ law `zero absorbs multiplication right` is
85
+ definition is
86
+ `right absorbing element` multiply 0
87
+ end
88
+
89
+ example `zero on right of multiplication produces zero` is
90
+ x = 3
91
+ expect multiply x 0 = 0
92
+ end
93
+ end
94
+
95
+ law `zero absorbs multiplication on both sides` is
96
+ definition is
97
+ `absorbing element` multiply 0
98
+ end
99
+
100
+ example `zero on both sides of multiplication produces zero` is
101
+ x = 3
102
+ expect multiply 0 x = 0
103
+ expect multiply x 0 = 0
104
+ end
105
+ end
106
+
107
+ law `multiplication distributes over addition left` is
108
+ definition is
109
+ `left distributive` multiply add
110
+ end
111
+
112
+ example `left distribution produces 36` is
113
+ x = 3
114
+ y = 5
115
+ z = 7
116
+ expect multiply x (add y z) = 36
117
+ expect add (multiply x y) (multiply x z) = 36
118
+ end
119
+ end
120
+
121
+ law `multiplication distributes over addition right` is
122
+ definition is
123
+ `right distributive` multiply add
124
+ end
125
+
126
+ example `right distribution produces 56` is
127
+ x = 3
128
+ y = 5
129
+ z = 7
130
+ expect multiply (add x y) z = 56
131
+ expect add (multiply x z) (multiply y z) = 56
132
+ end
133
+ end
134
+
135
+ law `multiplication distributes over addition on both sides` is
136
+ definition is
137
+ `distributive` multiply add
138
+ end
139
+
140
+ example `left distribution produces 36; right distribution produces 56` is
141
+ x = 3
142
+ y = 5
143
+ z = 7
144
+ expect multiply x (add y z) = 36
145
+ expect add (multiply x y) (multiply x z) = 36
146
+ expect multiply (add x y) z = 56
147
+ expect add (multiply x z) (multiply y z) = 56
148
+ end
149
+ end
150
+
151
+ law `maximum is an idempotent operation` is
152
+ definition is
153
+ `idempotent operation` maximumValue
154
+ end
155
+
156
+ example `the maximum of 3 and itself is 3` is
157
+ x = 3
158
+ expect maximumValue x x = 3
159
+ end
160
+ end
161
+
162
+ law `negation supplies left additive inverses` is
163
+ definition is
164
+ `left inverse element` add negateValue 0
165
+ end
166
+
167
+ example `3 has inverse -3; the left inverse equation produces zero` is
168
+ x = 3
169
+ expect negateValue x = -3
170
+ expect add (negateValue x) x = 0
171
+ end
172
+ end
173
+
174
+ law `negation supplies right additive inverses` is
175
+ definition is
176
+ `right inverse element` add negateValue 0
177
+ end
178
+
179
+ example `3 has inverse -3; the right inverse equation produces zero` is
180
+ x = 3
181
+ expect negateValue x = -3
182
+ expect add x (negateValue x) = 0
183
+ end
184
+ end
185
+
186
+ law `negation supplies both additive inverses` is
187
+ definition is
188
+ `invertible` add negateValue 0
189
+ end
190
+
191
+ example `3 has inverse -3; the left and right inverse equation produces zero` is
192
+ x = 3
193
+ expect negateValue x = -3
194
+ expect add (negateValue x) x = 0
195
+ expect add x (negateValue x) = 0
196
+ end
197
+ end
198
+
199
+ law `subtraction has left division` is
200
+ definition is
201
+ `left division` subtractValue divideLeft
202
+ end
203
+
204
+ example `left solution is -2 since 3 - (-2) = 5; reversing the operation recovers 5` is
205
+ x = 3
206
+ y = 5
207
+ expect divideLeft x y = -2
208
+ expect subtractValue x (divideLeft x y) = 5
209
+ expect divideLeft x (subtractValue x y) = 5
210
+ end
211
+ end
212
+
213
+ law `subtraction has right division` is
214
+ definition is
215
+ `right division` subtractValue divideRight
216
+ end
217
+
218
+ example `right solution is 8 since 8 - 5 = 3; reversing the operation recovers 3` is
219
+ x = 3
220
+ y = 5
221
+ expect divideRight x y = 8
222
+ expect subtractValue (divideRight x y) y = 3
223
+ expect divideRight (subtractValue x y) y = 3
224
+ end
225
+ end
226
+
227
+ law `subtraction has divisible` is
228
+ definition is
229
+ `divisible` subtractValue divideLeft divideRight
230
+ end
231
+
232
+ example `left solution is -2; right solution is 8; both recovery directions hold` is
233
+ x = 3
234
+ y = 5
235
+ expect divideLeft x y = -2
236
+ expect subtractValue x (divideLeft x y) = 5
237
+ expect divideLeft x (subtractValue x y) = 5
238
+ expect divideRight x y = 8
239
+ expect subtractValue (divideRight x y) y = 3
240
+ expect divideRight (subtractValue x y) y = 3
241
+ end
242
+ end
243
+
244
+ law `negation is an involution` is
245
+ definition is
246
+ `involution` negateValue
247
+ end
248
+
249
+ example `negating -3 twice restores -3` is
250
+ x = -3
251
+ expect negateValue x = 3
252
+ expect negateValue (negateValue x) = -3
253
+ end
254
+ end
@@ -0,0 +1,21 @@
1
+ unit example.boolean_flags
2
+
3
+ flipFlag :: Bool -> Bool
4
+
5
+ law `flipping twice restores either flag` is
6
+ definition is
7
+ `left inverse` flipFlag flipFlag
8
+ end
9
+
10
+ example `disabled becomes enabled and then disabled again` is
11
+ x = false
12
+ expect flipFlag x = true
13
+ expect flipFlag (flipFlag x) = false
14
+ end
15
+
16
+ example `enabled becomes disabled and then enabled again` is
17
+ x = true
18
+ expect flipFlag x = false
19
+ expect flipFlag (flipFlag x) = true
20
+ end
21
+ end
@@ -0,0 +1,60 @@
1
+ unit example.currying
2
+
3
+ sumFour :: Int32 -> Int32 -> Int32 -> Int32 -> Int32
4
+ format :: Text -> Bool -> Int32 -> Text -> Text
5
+ referenceFormat :: Text -> Bool -> Int32 -> Text -> Text
6
+ trim :: Text -> Text
7
+
8
+ law `a four-argument function partially applied twice is commutative` is
9
+ definition is
10
+ `commutative` (sumFour 1 2)
11
+ end
12
+
13
+ example `the fixed 1 and 2 plus 3 and 4 sum to 10 in either order` is
14
+ x = 3
15
+ y = 4
16
+ expect sumFour 1 2 x y = 10
17
+ expect sumFour 1 2 y x = 10
18
+ end
19
+ end
20
+
21
+ law `staged formatter`
22
+ (render :: Int32 -> Text -> Text)
23
+ (reference :: Int32 -> Text -> Text)
24
+ is
25
+ definition is
26
+ `for all` (port :: Int32) (suffix :: Text) .
27
+ render port suffix = reference port suffix
28
+ end
29
+ end
30
+
31
+ law `partial application preserves heterogeneous argument order` is
32
+ definition is
33
+ `staged formatter` (format "port:" true) (referenceFormat "port:" true)
34
+ end
35
+
36
+ example `port 443 with a tcp suffix formats as port:443/tcp` is
37
+ port = 443
38
+ suffix = "/tcp"
39
+ expect format "port:" true port suffix = "port:443/tcp"
40
+ expect referenceFormat "port:" true port suffix = "port:443/tcp"
41
+ end
42
+
43
+ example `a disabled numeric field keeps the prefix and suffix` is
44
+ port = 443
45
+ suffix = "/tcp"
46
+ expect format "port:" false port suffix = "port:/tcp"
47
+ expect referenceFormat "port:" false port suffix = "port:/tcp"
48
+ end
49
+ end
50
+
51
+ law `composition accepts a partially applied function` is
52
+ definition is
53
+ `equivalent` ((format "port:" true 443) . trim) ((referenceFormat "port:" true 443) . trim)
54
+ end
55
+
56
+ example `trim runs before the remaining formatter argument` is
57
+ x = " /tcp "
58
+ expect ((format "port:" true 443) . trim) x = "port:443/tcp"
59
+ end
60
+ end
@@ -0,0 +1,66 @@
1
+ unit example.parse_port
2
+
3
+ validPort :: Int32 -> Bool
4
+ render :: Int32 -> Text
5
+ parse :: Text -> Int32
6
+
7
+ law `valid ports round trip` is
8
+ definition is
9
+ `for all` (x :: Int32) .
10
+ validPort x implies
11
+ parse (render x) = x
12
+ end
13
+
14
+ description is
15
+ "For ports in 1 through 65535, {parse} recovers the value written by {render}"
16
+ end
17
+
18
+ example `ordinary port` is
19
+ x = 443
20
+ expect validPort x = true
21
+ expect render x = "443"
22
+ expect parse (render x) = 443
23
+ end
24
+
25
+ example `lowest port is accepted and round trips` is
26
+ x = 1
27
+ expect validPort x = true
28
+ expect render x = "1"
29
+ expect parse (render x) = 1
30
+ end
31
+
32
+ example `highest port is accepted and round trips` is
33
+ x = 65535
34
+ expect validPort x = true
35
+ expect render x = "65535"
36
+ expect parse (render x) = 65535
37
+ end
38
+
39
+ example `zero is rejected; rendering and parsing are skipped` is
40
+ x = 0
41
+ expect validPort x = false
42
+ end
43
+
44
+ example `negative port is rejected; rendering and parsing are skipped` is
45
+ x = -1
46
+ expect validPort x = false
47
+ end
48
+
49
+ example `65536 is rejected; rendering and parsing are skipped` is
50
+ x = 65536
51
+ expect validPort x = false
52
+ end
53
+ end
54
+
55
+ law `the same port contract via the prelude` is
56
+ definition is
57
+ `left inverse when` validPort parse render
58
+ end
59
+
60
+ example `port 8080 satisfies the inherited condition and round trips` is
61
+ x = 8080
62
+ expect validPort x = true
63
+ expect render x = "8080"
64
+ expect parse (render x) = 8080
65
+ end
66
+ end
package/index.d.ts CHANGED
@@ -5,12 +5,13 @@ export interface Location { file: string; line: number; column: number }
5
5
  export interface Diagnostic { code: string; message: string; at: Location | null }
6
6
  export interface Artifact { path: string; content: string; ownership: 'user' | 'generated' }
7
7
  export type Type = {tag: 'Named' | 'Variable'; contents: string} | {tag: 'Arrow'; contents: [Type, Type]};
8
- export type Expr = {tag: 'Var' | 'StringLit'; contents: string} | {tag: 'Number'; contents: number} | {tag: 'Apply' | 'Compose'; contents: [Expr, Expr]};
9
- export type Definition = {tag: 'Forall'; contents: [[string, Type][], Definition]} | {tag: 'Equal'; contents: [Expr, Expr]} | {tag: 'Invoke'; contents: [string, Expr[]]};
10
- export interface Expectation { actual: Expr; expected: number | string }
11
- export interface Example { exampleName: string; bindings: [string, number | string][]; expectations: Expectation[] }
8
+ export type Expr = {tag: 'Var' | 'StringLit'; contents: string} | {tag: 'Number'; contents: number} | {tag: 'BoolLit'; contents: boolean} | {tag: 'Apply' | 'Compose'; contents: [Expr, Expr]};
9
+ 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[]]};
10
+ export interface Expectation { actual: Expr; expected: number | string | boolean }
11
+ export interface Example { exampleName: string; bindings: [string, number | string | boolean][]; expectations: Expectation[] }
12
12
  export interface Law { lawName: string; parameters: [string, Type][]; requirements: Type[]; definition: Definition; description: string; rationale: string; examples: Example[]; references: string[]; location: Location }
13
- export interface Expanded { owner: string; name: string; inputs: {inputName: string; inputId: string; inputType: Type}[]; left: Expr; right: Expr; trace: string[]; original: Law }
13
+ export type Assertion = {tag: 'AssertEqual'; contents: [Expr, Expr]} | {tag: 'AssertImplies'; contents: [Expr, Assertion]} | {tag: 'AssertAll'; contents: Assertion[]};
14
+ export interface Expanded { owner: string; name: string; inputs: {inputName: string; inputId: string; inputType: Type}[]; left: Expr; right: Expr; guards: Expr[]; assertion: Assertion; trace: string[]; original: Law }
14
15
  export interface CheckRequest { sources: Source[] }
15
16
  export interface GenerationRequest extends CheckRequest { target: Target; sourceDir?: string; testDir?: string }
16
17
  export interface Result { diagnostics: Diagnostic[]; laws?: Expanded[]; expansions?: string[]; files?: Artifact[] }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lawspec",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "State the law once. Check it everywhere.",
5
5
  "license": "MIT",
6
6
  "type": "module",