lawspec 0.3.0 → 0.4.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.4 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.4.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.4.0 -- lawspec init --target javascript
29
+ npm install --save-dev lawspec@0.4.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.3.0, JUnit Jupiter 5.14.x | `mvn test` |
55
+ | `java` | Maven, JDK 25, release 25 | JetCheck 0.4.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.3's current JVM profile certifies
64
+ through compatibility profiles after testing; v0.4'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
  ```
@@ -158,7 +160,7 @@ for all (x :: Int32) . atoi (itoa (x)) = x
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
162
  quantification, integer and text literals, and equality. Function signatures use `Int32`
161
- and `Text`; generic variables are supported in reusable laws. v0.3 generates
163
+ and `Text`; generic variables are supported in reusable laws. v0.4 generates
162
164
  quantified `Int32` and `Text` inputs, including mixed and multiple inputs. Both
163
165
  types can also be intermediate or compared results. Functions are synchronous
164
166
  and unary. Text literals are double-quoted, with escapes such as `\"`, `\\`,
@@ -168,7 +170,7 @@ Text values contain Unicode scalar values; surrogate code points are rejected.
168
170
  Examples refer to the expanded input names, including names inherited from the
169
171
  prelude. Bind every input exactly once. Ambiguous names and out-of-range values
170
172
  are errors. Descriptions and rationales use `{function}` references; `{{` and `}}`
171
- produce literal braces. Metadata blocks follow the order shown in `scratch.md`:
173
+ produce literal braces. Law blocks use this order:
172
174
  definition, optional description, optional rationale, examples, optional references.
173
175
  `--` starts a line comment. Names that cannot be emitted portably are diagnosed.
174
176
 
@@ -176,6 +178,40 @@ Additional primitives, external law packages, cross-unit imports beyond the
176
178
  prelude, async functions, direct existing-symbol binding and browser hosting are
177
179
  outside this release.
178
180
 
181
+ ## Expected results and migration to 0.4
182
+
183
+ Every `example` must bind all quantified inputs and then include one or more
184
+ `expect <expression> = <literal>` assertions. The expected literal must have the
185
+ same `Int32` or `Text` type as the expression. Expressions can reference the
186
+ example's inputs and the unit's functions, including composed function calls.
187
+ Input names shadow function names within expectations, following lexical scope.
188
+
189
+ An example passes only when **all its expected results and its enclosing law**
190
+ pass. Expected results are authored specifications, never inferred by executing
191
+ your adapter. They apply to that example's inputs; randomized and boundary tests
192
+ continue to check the general law. Generated assertions show compared values and
193
+ identify the example, input bindings and expression. A failing assertion stops
194
+ that individual test; other tests remain independent.
195
+
196
+ This is an intentional syntax break from 0.3: input-only examples are rejected,
197
+ and `expect` is now a reserved keyword. Laws can still omit examples altogether.
198
+ For an existing input-only example, retain its bindings and add the intended
199
+ result before `end`:
200
+
201
+ ```lawspec
202
+ example `zero renders as 0 and round-trips unchanged` is
203
+ x = 0
204
+ expect itoa x = "0"
205
+ expect atoi (itoa x) = 0
206
+ end
207
+ ```
208
+
209
+ Update every example before running `check` or `generate`. Use
210
+ `npx lawspec explain` to inspect the law expansion, example inputs and expected
211
+ results; this command displays the specification without executing adapters.
212
+ Existing implementation adapters remain user-owned and are never overwritten.
213
+ The historical `scratch.md` is a design draft, not the current syntax reference.
214
+
179
215
  ## Comparing alternative implementations
180
216
 
181
217
  `equivalent` compares two functions with the same input and output types. Its
@@ -192,8 +228,10 @@ law `decimal renderers agree` is
192
228
  definition is
193
229
  `equivalent` render referenceRender
194
230
  end
195
- example `negative integer` is
231
+ example `both renderers produce a negative decimal string` is
196
232
  x = -42
233
+ expect render x = "-42"
234
+ expect referenceRender x = "-42"
197
235
  end
198
236
  end
199
237
  ```
@@ -202,7 +240,7 @@ This expands to `for all (x :: Int32) . render (x) = referenceRender (x)`.
202
240
  The example inherits the input name `x` from the prelude. Both functions are
203
241
  user-owned adapter functions; either may delegate to your existing code.
204
242
 
205
- [The complete example](https://github.com/brain-fuel/lawspec/blob/v0.3.0/examples/specs/equivalent.lawspec) compares decimal
243
+ [The complete example](https://github.com/brain-fuel/lawspec/blob/v0.4.0/examples/specs/equivalent.lawspec) compares decimal
206
244
  renderers and two implementations that clamp negative integers to zero. For
207
245
  JavaScript, their adapters can be:
208
246
 
@@ -215,9 +253,9 @@ export const referenceClamp = x => x < 0 ? 0 : x;
215
253
 
216
254
  The same specification generates native tests for all seven targets. The
217
255
  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`.
256
+ breaks each alternative separately to verify detection. The general equivalence law alone does not establish independent correctness;
257
+ 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`.
221
259
 
222
260
  ## Text properties and idempotence
223
261
 
@@ -231,13 +269,15 @@ law `normalizers agree` is
231
269
  definition is
232
270
  `equivalent` normalize referenceNormalize
233
271
  end
234
- example `ordinary text` is
272
+ example `spaces become hyphens; punctuation is preserved` is
235
273
  x = "Hello, World!"
274
+ expect normalize x = "Hello,-World!"
275
+ expect referenceNormalize x = "Hello,-World!"
236
276
  end
237
277
  end
238
278
  ```
239
279
 
240
- The [slug example](https://github.com/brain-fuel/lawspec/blob/v0.3.0/examples/specs/slug.lawspec)
280
+ The [slug example](https://github.com/brain-fuel/lawspec/blob/v0.4.0/examples/specs/slug.lawspec)
241
281
  compares two implementations of ASCII-space replacement. It includes empty,
242
282
  Unicode and escaped text. Each target uses its native string generator:
243
283
  JetCheck `Generator.stringsOf(Generator.asciiPrintableChars())`, Hypothesis `st.text()`, fast-check `fc.string()`,
@@ -257,10 +297,14 @@ law `canonicalization reaches a fixed point` is
257
297
  definition is
258
298
  `idempotent` canonicalize
259
299
  end
300
+ example `all trailing slashes are removed in one pass` is
301
+ x = "https://example.com/path///"
302
+ expect canonicalize x = "https://example.com/path"
303
+ end
260
304
  end
261
305
  ```
262
306
 
263
- The [canonical URL example](https://github.com/brain-fuel/lawspec/blob/v0.3.0/examples/specs/canonical_url.lawspec)
307
+ The [canonical URL example](https://github.com/brain-fuel/lawspec/blob/v0.4.0/examples/specs/canonical_url.lawspec)
264
308
  uses removal of **all trailing slashes** as a small fixed-point demonstration,
265
309
  not a complete URL canonicalization algorithm. For JavaScript:
266
310
 
@@ -269,9 +313,10 @@ export const canonicalize = value => value.replace(/\/+$/, "");
269
313
  ```
270
314
 
271
315
  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)
316
+ The [mixed-input example](https://github.com/brain-fuel/lawspec/blob/v0.4.0/examples/specs/mixed_inputs.lawspec)
273
317
  shows `Text` and `Int32` in the same quantified property and executable example.
274
- The JavaScript API represents example values as `number | string`.
318
+ The JavaScript API represents input bindings and expected values as `number | string`.
319
+ Each example includes `expectations: { actual: Expr; expected: number | string }[]`.
275
320
 
276
321
  ## Generate all example artifacts
277
322
 
@@ -333,7 +378,7 @@ by the JS shim.
333
378
  ## Build and verify
334
379
 
335
380
  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`.
381
+ `npm pack ./npm` and install it with `npm install --save-dev ./lawspec-0.4.0.tgz`.
337
382
  The package payload lives in `npm/`.
338
383
 
339
384
  ```sh
package/bin/lawspec.mjs CHANGED
@@ -151,16 +151,43 @@ 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 (expr.tag === "Number" || expr.tag === "StringLit")
158
+ return JSON.stringify(value);
159
+ if (expr.tag === "Apply")
160
+ return `${showExpression(value[0])} (${showExpression(value[1])})`;
161
+ return `(${showExpression(value[0])} . ${showExpression(value[1])})`;
162
+ }
163
+ function explainExamples(law) {
164
+ return law.original.examples
165
+ .map(
166
+ (ex) =>
167
+ `\nexample ${JSON.stringify(ex.exampleName)}\n` +
168
+ ex.bindings
169
+ .map(([n, v]) => ` ${n} = ${JSON.stringify(v)}`)
170
+ .join("\n") +
171
+ "\n" +
172
+ ex.expectations
173
+ .map(
174
+ (e) =>
175
+ ` expect ${showExpression(e.actual)} = ${JSON.stringify(e.expected)}`,
176
+ )
177
+ .join("\n"),
178
+ )
179
+ .join("\n");
180
+ }
154
181
  async function main() {
155
182
  if (!verb || ["help", "--help", "-h"].includes(verb)) {
156
183
  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: " +
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: " +
158
185
  targets.join(", "),
159
186
  );
160
187
  return;
161
188
  }
162
189
  if (verb === "--version") {
163
- output("0.3.0");
190
+ output("0.4.0");
164
191
  return;
165
192
  }
166
193
  if (positional.length > (verb === "explain" ? 1 : 0))
@@ -250,7 +277,7 @@ async function main() {
250
277
  : indices
251
278
  .map(
252
279
  ({ e, i }) =>
253
- `${e.owner}::${e.name}\n${e.trace.join("\n=> ")}\n=> ${result.expansions[i]}`,
280
+ `${e.owner}::${e.name}\n${e.trace.join("\n=> ")}\n=> ${result.expansions[i]}${explainExamples(e)}`,
254
281
  )
255
282
  .join("\n\n"),
256
283
  );
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": "2c1176c912901bd218743a88df1b67fb1ee8d58ed73fcbe7e2f23827f10a3a0f",
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",
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
27
  "src/LawSpec/Prelude.hs": "0b60dd85bdf0642077dfb497043c63606d507e71e12d3ed8bfe2bf773214c66b",
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": "098f1d0fc52575907312c95095846d1d4f6e7ace7cfbe0419a305b9fe8a75a92",
34
+ "npm/core.wasm": "0a4ce15cefcd866cd7dd584c32d683c7f8a46823104afcceba1298570883a88a",
35
35
  "npm/core_jsffi.js": "88d136efe92f7cff5758c8fec8d9b6bbc9707fe37741cebeeb415fe34ce3d72b",
36
36
  "npm/api.mjs": "d6df654600172131ac66a55b86876fc29cde2a76a0a63bbefff3183488a4837b",
37
- "npm/index.d.ts": "94c4842654d280796aeedcf7b438ffc63e96d0ff639203f93d04610f1de07c53"
37
+ "npm/index.d.ts": "85ad409ae09d243d3b3f21243d0784390d7b0209e2a09fc7c3b733e8dd2e8765"
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
@@ -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
@@ -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
@@ -7,7 +7,8 @@ export interface Artifact { path: string; content: string; ownership: 'user' | '
7
7
  export type Type = {tag: 'Named' | 'Variable'; contents: string} | {tag: 'Arrow'; contents: [Type, Type]};
8
8
  export type Expr = {tag: 'Var' | 'StringLit'; contents: string} | {tag: 'Number'; contents: number} | {tag: 'Apply' | 'Compose'; contents: [Expr, Expr]};
9
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][] }
10
+ export interface Expectation { actual: Expr; expected: number | string }
11
+ export interface Example { exampleName: string; bindings: [string, number | string][]; 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
13
  export interface Expanded { owner: string; name: string; inputs: {inputName: string; inputId: string; inputType: Type}[]; left: Expr; right: Expr; trace: string[]; original: Law }
13
14
  export interface CheckRequest { sources: Source[] }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lawspec",
3
- "version": "0.3.0",
3
+ "version": "0.4.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