lawspec 0.3.0 → 0.5.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.3 compiles reusable laws into native property tests, executable examples,
5
+ LawSpec 0.5 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.3.0
14
+ npm install --save-dev lawspec@0.5.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.3.0 -- lawspec init --target javascript
29
- npm install --save-dev lawspec@0.3.0
28
+ npm exec --package=lawspec@0.5.0 -- lawspec init --target javascript
29
+ npm install --save-dev lawspec@0.5.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
@@ -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.3's current JVM profile certifies
64
+ through compatibility profiles after testing; v0.5'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.
@@ -113,7 +113,7 @@ uses `test/Spec.hs` with `hspec-discover`.
113
113
  Commands:
114
114
 
115
115
  - `check`: parse, resolve, type-check and expand laws without target dependencies.
116
- - `explain [unit::law]`: display expansion steps and inherited example inputs.
116
+ - `explain [unit::law]`: display expansions, example inputs, and expected results.
117
117
  - `doctor`: inspect selected native environments and print corrective instructions.
118
118
  - `generate`: check environments, validate every output, then write artifacts.
119
119
  - `generate --dry-run`: show proposed file operations without applying them.
@@ -139,8 +139,10 @@ law `round trip` is
139
139
  description is
140
140
  "applying {itoa} and then {atoi} recovers the original integer"
141
141
  end
142
- example `negative` is
142
+ example `negative integers use a minus sign and round-trip unchanged` is
143
143
  x = -42
144
+ expect itoa x = "-42"
145
+ expect atoi (itoa x) = -42
144
146
  end
145
147
  end
146
148
  ```
@@ -157,10 +159,11 @@ for all (x :: Int32) . atoi (itoa (x)) = x
157
159
 
158
160
  Reusable laws can declare typed unary function parameters and `requires Eq a`.
159
161
  Definitions support law application, function application/composition, universal
160
- quantification, integer and text literals, and equality. Function signatures use `Int32`
161
- and `Text`; generic variables are supported in reusable laws. v0.3 generates
162
- quantified `Int32` and `Text` inputs, including mixed and multiple inputs. Both
163
- types can also be intermediate or compared results. Functions are synchronous
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.5 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
164
167
  and unary. Text literals are double-quoted, with escapes such as `\"`, `\\`,
165
168
  `\n`, and `\t`; examples must bind each input to a literal of its declared type.
166
169
  Text values contain Unicode scalar values; surrogate code points are rejected.
@@ -168,7 +171,7 @@ Text values contain Unicode scalar values; surrogate code points are rejected.
168
171
  Examples refer to the expanded input names, including names inherited from the
169
172
  prelude. Bind every input exactly once. Ambiguous names and out-of-range values
170
173
  are errors. Descriptions and rationales use `{function}` references; `{{` and `}}`
171
- produce literal braces. Metadata blocks follow the order shown in `scratch.md`:
174
+ produce literal braces. Law blocks use this order:
172
175
  definition, optional description, optional rationale, examples, optional references.
173
176
  `--` starts a line comment. Names that cannot be emitted portably are diagnosed.
174
177
 
@@ -176,6 +179,114 @@ Additional primitives, external law packages, cross-unit imports beyond the
176
179
  prelude, async functions, direct existing-symbol binding and browser hosting are
177
180
  outside this release.
178
181
 
182
+ ## Predicates and conditional laws (0.5)
183
+
184
+ A predicate is a unary function returning `Bool`. Use `true` and `false` in
185
+ expressions, example bindings, and expected results. A Boolean expression can
186
+ stand alone as a law's definition: it must evaluate to `true`.
187
+
188
+ `condition implies consequence` checks the consequence only when the condition
189
+ is true. Conditions must have type `Bool`; nested implications short-circuit in
190
+ source order. The consequence can be an equality, another implication, a Boolean
191
+ predicate, or a reusable law application. Quantify any inputs before using them.
192
+
193
+ ```lawspec
194
+ unit example.parse_port
195
+
196
+ validPort :: Int32 -> Bool
197
+ render :: Int32 -> Text
198
+ parse :: Text -> Int32
199
+
200
+ law `valid ports round trip` is
201
+ definition is
202
+ `for all` (x :: Int32) .
203
+ validPort x implies
204
+ parse (render x) = x
205
+ end
206
+
207
+ example `ordinary port` is
208
+ x = 443
209
+ expect validPort x = true
210
+ expect render x = "443"
211
+ expect parse (render x) = 443
212
+ end
213
+
214
+ example `zero is rejected; the round trip is skipped` is
215
+ x = 0
216
+ expect validPort x = false
217
+ end
218
+ end
219
+ ```
220
+
221
+ The [complete port example](https://github.com/brain-fuel/lawspec/blob/v0.5.0/examples/specs/parse_port.lawspec)
222
+ defines valid ports as 1–65535, and covers both endpoints, ordinary ports, zero,
223
+ negative values, and 65536. All explicit `expect` assertions run regardless of
224
+ the law's condition. A false condition skips only the consequence: invalid ports
225
+ never reach `render` or `parse` through the law. Predicate errors still fail the
226
+ test; they are not treated as false.
227
+
228
+ Implication is logical implication, not generator filtering or an assumption.
229
+ Randomized tests still sample the full input domain and count a false condition
230
+ as satisfying the law. A narrow predicate may therefore exercise few or no
231
+ consequences during a random run. Explicit valid examples ensure the important
232
+ cases run, and expectations of both `true` and `false` catch always-false and
233
+ always-true predicate implementations.
234
+
235
+ The prelude includes `satisfies predicate` (the predicate holds for every input)
236
+ and `left inverse when predicate parse render` (the guarded round trip above).
237
+ These reusable laws preserve the condition and its lexical bindings when expanded.
238
+ `equivalent` can also compare two predicates, since `Bool` supports equality.
239
+ The [Boolean flags example](https://github.com/brain-fuel/lawspec/blob/v0.5.0/examples/specs/boolean_flags.lawspec)
240
+ checks that flipping twice restores both `false` and `true`; all targets generate
241
+ Boolean property inputs and explicit tests for both Boolean boundary values. Java caps Boolean-only JetCheck runs at the number of
242
+ possible input combinations (up to 100), avoiding generator exhaustion.
243
+
244
+ In 0.5, `implies`, `true`, and `false` become reserved words. Existing 0.4 specs
245
+ that use those words as identifiers need renaming. The API adds `BoolLit`,
246
+ `Holds`, and `Implies` AST variants, Boolean literal values, and an ordered
247
+ `guards` array on expanded laws. `lawspec explain` prints the conditions.
248
+
249
+ Kotlin adapters now group functions in an `object` named after the unit (for
250
+ example, `object ParsePort` in package `example`). This allows both the port and
251
+ alternatives units to define `render(Int)`. When upgrading a Kotlin project,
252
+ move existing top-level adapter functions into the indicated object; generation
253
+ preserves your adapter and reports the required stub shape. Generated tests call
254
+ `ParsePort.validPort(...)`, `ParsePort.render(...)`, and `ParsePort.parse(...)`.
255
+
256
+ ## Expected results and migration from 0.3
257
+
258
+ Every `example` must bind all quantified inputs and then include one or more
259
+ `expect <expression> = <literal>` assertions. The expected literal must have the
260
+ same `Int32`, `Text`, or `Bool` type as the expression. Expressions can reference the
261
+ example's inputs and the unit's functions, including composed function calls.
262
+ Input names shadow function names within expectations, following lexical scope.
263
+
264
+ An example passes only when **all its expected results and its enclosing law**
265
+ pass. Expected results are authored specifications, never inferred by executing
266
+ your adapter. They apply to that example's inputs; randomized and boundary tests
267
+ continue to check the general law. Generated assertions show compared values and
268
+ identify the example, input bindings and expression. A failing assertion stops
269
+ that individual test; other tests remain independent.
270
+
271
+ This is an intentional syntax break from 0.3: input-only examples are rejected,
272
+ and `expect` is now a reserved keyword. Laws can still omit examples altogether.
273
+ For an existing input-only example, retain its bindings and add the intended
274
+ result before `end`:
275
+
276
+ ```lawspec
277
+ example `zero renders as 0 and round-trips unchanged` is
278
+ x = 0
279
+ expect itoa x = "0"
280
+ expect atoi (itoa x) = 0
281
+ end
282
+ ```
283
+
284
+ Update every example before running `check` or `generate`. Use
285
+ `npx lawspec explain` to inspect the law expansion, example inputs and expected
286
+ results; this command displays the specification without executing adapters.
287
+ Existing implementation adapters remain user-owned and are never overwritten.
288
+ The historical `scratch.md` is a design draft, not the current syntax reference.
289
+
179
290
  ## Comparing alternative implementations
180
291
 
181
292
  `equivalent` compares two functions with the same input and output types. Its
@@ -192,8 +303,10 @@ law `decimal renderers agree` is
192
303
  definition is
193
304
  `equivalent` render referenceRender
194
305
  end
195
- example `negative integer` is
306
+ example `both renderers produce a negative decimal string` is
196
307
  x = -42
308
+ expect render x = "-42"
309
+ expect referenceRender x = "-42"
197
310
  end
198
311
  end
199
312
  ```
@@ -202,7 +315,7 @@ This expands to `for all (x :: Int32) . render (x) = referenceRender (x)`.
202
315
  The example inherits the input name `x` from the prelude. Both functions are
203
316
  user-owned adapter functions; either may delegate to your existing code.
204
317
 
205
- [The complete example](https://github.com/brain-fuel/lawspec/blob/v0.3.0/examples/specs/equivalent.lawspec) compares decimal
318
+ [The complete example](https://github.com/brain-fuel/lawspec/blob/v0.5.0/examples/specs/equivalent.lawspec) compares decimal
206
319
  renderers and two implementations that clamp negative integers to zero. For
207
320
  JavaScript, their adapters can be:
208
321
 
@@ -215,9 +328,9 @@ export const referenceClamp = x => x < 0 ? 0 : x;
215
328
 
216
329
  The same specification generates native tests for all seven targets. The
217
330
  integration suite checks both examples with matching implementations, then
218
- breaks each alternative separately to verify detection. Agreement does not
219
- establish that either implementation meets an independent specification; two
220
- implementations can share the same bug. Quantified inputs can be `Int32` or `Text`.
331
+ breaks each alternative separately to verify detection. The general equivalence law alone does not establish independent correctness;
332
+ two implementations can share the same bug. Explicit expectations additionally
333
+ check the specified outputs at the supplied example inputs. Quantified inputs can be `Int32`, `Text`, or `Bool`.
221
334
 
222
335
  ## Text properties and idempotence
223
336
 
@@ -231,13 +344,15 @@ law `normalizers agree` is
231
344
  definition is
232
345
  `equivalent` normalize referenceNormalize
233
346
  end
234
- example `ordinary text` is
347
+ example `spaces become hyphens; punctuation is preserved` is
235
348
  x = "Hello, World!"
349
+ expect normalize x = "Hello,-World!"
350
+ expect referenceNormalize x = "Hello,-World!"
236
351
  end
237
352
  end
238
353
  ```
239
354
 
240
- The [slug example](https://github.com/brain-fuel/lawspec/blob/v0.3.0/examples/specs/slug.lawspec)
355
+ The [slug example](https://github.com/brain-fuel/lawspec/blob/v0.5.0/examples/specs/slug.lawspec)
241
356
  compares two implementations of ASCII-space replacement. It includes empty,
242
357
  Unicode and escaped text. Each target uses its native string generator:
243
358
  JetCheck `Generator.stringsOf(Generator.asciiPrintableChars())`, Hypothesis `st.text()`, fast-check `fc.string()`,
@@ -257,10 +372,14 @@ law `canonicalization reaches a fixed point` is
257
372
  definition is
258
373
  `idempotent` canonicalize
259
374
  end
375
+ example `all trailing slashes are removed in one pass` is
376
+ x = "https://example.com/path///"
377
+ expect canonicalize x = "https://example.com/path"
378
+ end
260
379
  end
261
380
  ```
262
381
 
263
- The [canonical URL example](https://github.com/brain-fuel/lawspec/blob/v0.3.0/examples/specs/canonical_url.lawspec)
382
+ The [canonical URL example](https://github.com/brain-fuel/lawspec/blob/v0.5.0/examples/specs/canonical_url.lawspec)
264
383
  uses removal of **all trailing slashes** as a small fixed-point demonstration,
265
384
  not a complete URL canonicalization algorithm. For JavaScript:
266
385
 
@@ -269,9 +388,10 @@ export const canonicalize = value => value.replace(/\/+$/, "");
269
388
  ```
270
389
 
271
390
  Removing just one trailing slash fails the supplied repeated-slash example.
272
- The [mixed-input example](https://github.com/brain-fuel/lawspec/blob/v0.3.0/examples/specs/mixed_inputs.lawspec)
391
+ The [mixed-input example](https://github.com/brain-fuel/lawspec/blob/v0.5.0/examples/specs/mixed_inputs.lawspec)
273
392
  shows `Text` and `Int32` in the same quantified property and executable example.
274
- The JavaScript API represents example values as `number | string`.
393
+ The JavaScript API represents input bindings and expected values as `number | string | boolean`.
394
+ Each example includes `expectations: { actual: Expr; expected: number | string | boolean }[]`.
275
395
 
276
396
  ## Generate all example artifacts
277
397
 
@@ -333,7 +453,7 @@ by the JS shim.
333
453
  ## Build and verify
334
454
 
335
455
  For contributors working from a repository checkout, build a local archive with
336
- `npm pack ./npm` and install it with `npm install --save-dev ./lawspec-0.3.0.tgz`.
456
+ `npm pack ./npm` and install it with `npm install --save-dev ./lawspec-0.5.0.tgz`.
337
457
  The package payload lives in `npm/`.
338
458
 
339
459
  ```sh
package/bin/lawspec.mjs CHANGED
@@ -151,16 +151,47 @@ async function init() {
151
151
  `Configured ${language}. ${hasBuild ? "Existing build files preserved." : "Created missing project build files."}\n${setup[language]}\nNext: lawspec doctor, then lawspec generate.`,
152
152
  );
153
153
  }
154
+ function showExpression(expr) {
155
+ const value = expr.contents;
156
+ if (expr.tag === "Var") return value;
157
+ if (
158
+ expr.tag === "Number" ||
159
+ expr.tag === "StringLit" ||
160
+ expr.tag === "BoolLit"
161
+ )
162
+ return JSON.stringify(value);
163
+ if (expr.tag === "Apply")
164
+ return `${showExpression(value[0])} (${showExpression(value[1])})`;
165
+ return `(${showExpression(value[0])} . ${showExpression(value[1])})`;
166
+ }
167
+ function explainExamples(law) {
168
+ return law.original.examples
169
+ .map(
170
+ (ex) =>
171
+ `\nexample ${JSON.stringify(ex.exampleName)}\n` +
172
+ ex.bindings
173
+ .map(([n, v]) => ` ${n} = ${JSON.stringify(v)}`)
174
+ .join("\n") +
175
+ "\n" +
176
+ ex.expectations
177
+ .map(
178
+ (e) =>
179
+ ` expect ${showExpression(e.actual)} = ${JSON.stringify(e.expected)}`,
180
+ )
181
+ .join("\n"),
182
+ )
183
+ .join("\n");
184
+ }
154
185
  async function main() {
155
186
  if (!verb || ["help", "--help", "-h"].includes(verb)) {
156
187
  output(
157
- "LawSpec 0.3.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.5.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: " +
158
189
  targets.join(", "),
159
190
  );
160
191
  return;
161
192
  }
162
193
  if (verb === "--version") {
163
- output("0.3.0");
194
+ output("0.5.0");
164
195
  return;
165
196
  }
166
197
  if (positional.length > (verb === "explain" ? 1 : 0))
@@ -180,7 +211,7 @@ async function main() {
180
211
  : result
181
212
  .map(
182
213
  (r) =>
183
- `${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") : ""}`,
184
215
  )
185
216
  .join("\n") +
186
217
  "\nInspection artifacts only; native toolchains and dependencies are not checked. Stubs must be implemented before running tests.",
@@ -250,7 +281,7 @@ async function main() {
250
281
  : indices
251
282
  .map(
252
283
  ({ e, i }) =>
253
- `${e.owner}::${e.name}\n${e.trace.join("\n=> ")}\n=> ${result.expansions[i]}`,
284
+ `${e.owner}::${e.name}\n${e.trace.join("\n=> ")}\n=> ${result.expansions[i]}${explainExamples(e)}`,
254
285
  )
255
286
  .join("\n\n"),
256
287
  );
package/build.json CHANGED
@@ -17,23 +17,23 @@
17
17
  "wasm/lawspec-wasm.cabal"
18
18
  ],
19
19
  "digests": {
20
- "package.yaml": "a04aefef41a1245f28d4f1d63ea38a5266b1f5302242c9fc3a840252ea7ce3ec",
20
+ "package.yaml": "deae9eb35385fd0e08aa44432ae718e7ba6bad8e8320c728018ee9d224733c8c",
21
21
  "src/LawSpec/Api.hs": "00b1e4bc81231ebd387f10652463904084ebf9c3c4486fd2c2217a6d4dbfc157",
22
- "src/LawSpec/Compile.hs": "cb32f1ff2f2ded5e93e25b11658fec6a2ffb1d24e0caaa2ffec94b510f4bfe40",
23
- "src/LawSpec/Emit.hs": "c72ff20088dcaf74576710e144e79d35f9441f37fc933603033584fdff309fbe",
24
- "src/LawSpec/Gen.hs": "b3d924fddc7e8417665635f049a3f641ea15c2f6f997571287db17a70b1f37b6",
25
- "src/LawSpec/Model.hs": "5d550a5b75cd6b00f1b01d276100527a066c35bb69e95adb6b549b6603bab196",
26
- "src/LawSpec/Parser.hs": "0a70a57b6535cfa33121e9ada595f518ced4f7b35bf09bc4732a1362c05a253f",
27
- "src/LawSpec/Prelude.hs": "0b60dd85bdf0642077dfb497043c63606d507e71e12d3ed8bfe2bf773214c66b",
22
+ "src/LawSpec/Compile.hs": "fd696c774025cb0ec6eb29f71661ab71ac603314865ef068778ae9df1355b6cb",
23
+ "src/LawSpec/Emit.hs": "7f53109a18d2450313c9dc9f9ff0fe31d80f533858baf9b2c30e36b8849b416c",
24
+ "src/LawSpec/Gen.hs": "c4b5d4f003c52fb31983794187890ef0dad09c86a451d8e1145974605a5ca0e8",
25
+ "src/LawSpec/Model.hs": "148f133296be537d4e076c2716219fdafa6d2a44b26c9de4b033f87694870911",
26
+ "src/LawSpec/Parser.hs": "12b652dbbf174a7091609385c4564bd47b99e45329cfc1bcdaa3936901b2f966",
27
+ "src/LawSpec/Prelude.hs": "98cc20e242d2f70510fa09e59d0f3e387d1ebe438b4d3435ba2f32a1192cfb4e",
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": "9f61a4c9e5c7715d1de2c42e518f3fe9cf088a3677182bda4c3f4601aa930ba9",
34
- "npm/core.wasm": "674f19955c07f0bd987df27463cb0ba35c5e9756a8d22309aa21bff7488e7ed0",
33
+ "wasm/lawspec-wasm.cabal": "4c96fa2ec9c6cb61ca16f705c23cdae8723054bb252d27d8ef58043f8e31b868",
34
+ "npm/core.wasm": "581d42431681aa93b64bcd775d403612974f5cb9c7350be4b07d3ed49575c18e",
35
35
  "npm/core_jsffi.js": "88d136efe92f7cff5758c8fec8d9b6bbc9707fe37741cebeeb415fe34ce3d72b",
36
36
  "npm/api.mjs": "d6df654600172131ac66a55b86876fc29cde2a76a0a63bbefff3183488a4837b",
37
- "npm/index.d.ts": "94c4842654d280796aeedcf7b438ffc63e96d0ff639203f93d04610f1de07c53"
37
+ "npm/index.d.ts": "9687bedaa439c0888aa54cb94279f4fbda536122f0e55f6ab4ec3ea82ddb4283"
38
38
  }
39
39
  }
package/core.wasm CHANGED
Binary file
@@ -16,12 +16,16 @@ law `itoa and then atoi yields a` is
16
16
  "representing an Int32 as Text must not change its value"
17
17
  end
18
18
 
19
- example `negative integer` is
19
+ example `negative integers use a minus sign and round-trip unchanged` is
20
20
  x = -42
21
+ expect itoa x = "-42"
22
+ expect atoi (itoa x) = -42
21
23
  end
22
24
 
23
- example `zero` is
25
+ example `zero renders as 0 and round-trips unchanged` is
24
26
  x = 0
27
+ expect itoa x = "0"
28
+ expect atoi (itoa x) = 0
25
29
  end
26
30
 
27
31
  references are
@@ -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
@@ -9,13 +9,16 @@ law `canonicalization reaches a fixed point` is
9
9
  description is
10
10
  "{canonicalize} removes trailing slashes until the result is stable"
11
11
  end
12
- example `trailing slashes` is
12
+ example `all trailing slashes are removed in one pass` is
13
13
  x = "https://example.com/path///"
14
+ expect canonicalize x = "https://example.com/path"
14
15
  end
15
- example `already canonical` is
16
+ example `a URL without trailing slashes stays unchanged` is
16
17
  x = "https://example.com/path"
18
+ expect canonicalize x = "https://example.com/path"
17
19
  end
18
- example `empty` is
20
+ example `empty input remains empty` is
19
21
  x = ""
22
+ expect canonicalize x = ""
20
23
  end
21
24
  end
@@ -15,11 +15,15 @@ law `decimal renderers agree` is
15
15
  rationale is
16
16
  "changing the formatting implementation must preserve its result"
17
17
  end
18
- example `negative integer` is
18
+ example `both renderers produce "-42"` is
19
19
  x = -42
20
+ expect render x = "-42"
21
+ expect referenceRender x = "-42"
20
22
  end
21
- example `zero` is
23
+ example `both renderers produce "0"` is
22
24
  x = 0
25
+ expect render x = "0"
26
+ expect referenceRender x = "0"
23
27
  end
24
28
  end
25
29
 
@@ -30,10 +34,14 @@ law `nonnegative clamps agree` is
30
34
  description is
31
35
  "{clamp} agrees with {referenceClamp} when clamping negative inputs to zero"
32
36
  end
33
- example `negative boundary` is
37
+ example `both clamps return 0 for -2147483648` is
34
38
  x = -2147483648
39
+ expect clamp x = 0
40
+ expect referenceClamp x = 0
35
41
  end
36
- example `positive boundary` is
42
+ example `both clamps return 2147483647 for 2147483647` is
37
43
  x = 2147483647
44
+ expect clamp x = 2147483647
45
+ expect referenceClamp x = 2147483647
38
46
  end
39
47
  end
@@ -3,25 +3,29 @@ unit example.mixed.inputs
3
3
  normalize :: Text -> Text
4
4
  identity :: Int32 -> Int32
5
5
 
6
- law `text and integer inputs remain independent` is
6
+ law `text normalization reaches a fixed point with an unused integer input` is
7
7
  definition is
8
8
  `for all` (text :: Text) (number :: Int32) .
9
9
  normalize text = normalize (normalize text)
10
10
  end
11
- example `mixed inputs` is
11
+ example `text normalization and integer identity have explicit results` is
12
12
  text = "Hello, World!"
13
13
  number = -42
14
+ expect normalize text = "Hello,-World!"
15
+ expect identity number = -42
14
16
  end
15
17
  end
16
18
 
17
- law `integer behavior with a text context` is
19
+ law `integer identity with an unused text input` is
18
20
  definition is
19
21
  `for all` (text :: Text) (number :: Int32) .
20
22
  identity number = number
21
23
  end
22
- example `Unicode context` is
24
+ example `text normalization and integer identity have explicit results` is
23
25
  text = "λ 😀"
24
26
  number = 2147483647
27
+ expect normalize text = "λ-😀"
28
+ expect identity number = 2147483647
25
29
  end
26
30
  end
27
31
 
@@ -30,4 +34,8 @@ law `normalizing an empty literal` is
30
34
  `for all` (text :: Text) .
31
35
  normalize "" = ""
32
36
  end
37
+ example `empty literal stays empty; quantified text is unused` is
38
+ text = "unused context"
39
+ expect normalize "" = ""
40
+ end
33
41
  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
@@ -10,16 +10,24 @@ law `normalizers agree` is
10
10
  description is
11
11
  "{normalize} and {referenceNormalize} replace ASCII spaces with hyphens"
12
12
  end
13
- example `ordinary text` is
13
+ example `spaces become hyphens; punctuation is preserved` is
14
14
  x = "Hello, World!"
15
+ expect normalize x = "Hello,-World!"
16
+ expect referenceNormalize x = "Hello,-World!"
15
17
  end
16
- example `empty` is
18
+ example `empty text remains empty` is
17
19
  x = ""
20
+ expect normalize x = ""
21
+ expect referenceNormalize x = ""
18
22
  end
19
- example `Unicode` is
23
+ example `Unicode is preserved while spaces become hyphens` is
20
24
  x = "café 日本語 😀"
25
+ expect normalize x = "café-日本語-😀"
26
+ expect referenceNormalize x = "café-日本語-😀"
21
27
  end
22
- example `escaped text` is
28
+ example `quotes, backslashes and controls survive space replacement` is
23
29
  x = "quote: \" slash: \\ newline: \n tab: \t dollar: $"
30
+ expect normalize x = "quote:-\"-slash:-\\-newline:-\n-tab:-\t-dollar:-$"
31
+ expect referenceNormalize x = "quote:-\"-slash:-\\-newline:-\n-tab:-\t-dollar:-$"
24
32
  end
25
33
  end
package/index.d.ts CHANGED
@@ -5,11 +5,12 @@ 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 Example { exampleName: string; bindings: [string, number | string][] }
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: '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[] }
11
12
  export interface Law { lawName: string; parameters: [string, Type][]; requirements: Type[]; definition: Definition; description: string; rationale: string; examples: Example[]; references: string[]; location: Location }
12
- export interface Expanded { owner: string; name: string; inputs: {inputName: string; inputId: string; inputType: Type}[]; left: Expr; right: Expr; trace: string[]; original: Law }
13
+ export interface Expanded { owner: string; name: string; inputs: {inputName: string; inputId: string; inputType: Type}[]; left: Expr; right: Expr; guards: Expr[]; trace: string[]; original: Law }
13
14
  export interface CheckRequest { sources: Source[] }
14
15
  export interface GenerationRequest extends CheckRequest { target: Target; sourceDir?: string; testDir?: string }
15
16
  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.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "State the law once. Check it everywhere.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/starter.lawspec CHANGED
@@ -16,12 +16,16 @@ law `itoa and then atoi yields a` is
16
16
  "representing an Int32 as Text must not change its value"
17
17
  end
18
18
 
19
- example `negative integer` is
19
+ example `negative integers use a minus sign and round-trip unchanged` is
20
20
  x = -42
21
+ expect itoa x = "-42"
22
+ expect atoi (itoa x) = -42
21
23
  end
22
24
 
23
- example `zero` is
25
+ example `zero renders as 0 and round-trips unchanged` is
24
26
  x = 0
27
+ expect itoa x = "0"
28
+ expect atoi (itoa x) = 0
25
29
  end
26
30
 
27
31
  references are