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 +143 -23
- package/bin/lawspec.mjs +35 -4
- package/build.json +10 -10
- package/core.wasm +0 -0
- package/examples/specs/atoi_codec.lawspec +6 -2
- package/examples/specs/boolean_flags.lawspec +21 -0
- package/examples/specs/canonical_url.lawspec +6 -3
- package/examples/specs/equivalent.lawspec +12 -4
- package/examples/specs/mixed_inputs.lawspec +12 -4
- package/examples/specs/parse_port.lawspec +66 -0
- package/examples/specs/slug.lawspec +12 -4
- package/index.d.ts +5 -4
- package/package.json +1 -1
- package/starter.lawspec +6 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**State the law once. Check it everywhere.**
|
|
4
4
|
|
|
5
|
-
LawSpec 0.
|
|
5
|
+
LawSpec 0.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.
|
|
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.
|
|
29
|
-
npm install --save-dev lawspec@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.
|
|
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
|
|
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,
|
|
161
|
-
and `
|
|
162
|
-
|
|
163
|
-
types can also be intermediate or
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
219
|
-
|
|
220
|
-
|
|
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 `
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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": "
|
|
20
|
+
"package.yaml": "deae9eb35385fd0e08aa44432ae718e7ba6bad8e8320c728018ee9d224733c8c",
|
|
21
21
|
"src/LawSpec/Api.hs": "00b1e4bc81231ebd387f10652463904084ebf9c3c4486fd2c2217a6d4dbfc157",
|
|
22
|
-
"src/LawSpec/Compile.hs": "
|
|
23
|
-
"src/LawSpec/Emit.hs": "
|
|
24
|
-
"src/LawSpec/Gen.hs": "
|
|
25
|
-
"src/LawSpec/Model.hs": "
|
|
26
|
-
"src/LawSpec/Parser.hs": "
|
|
27
|
-
"src/LawSpec/Prelude.hs": "
|
|
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": "
|
|
34
|
-
"npm/core.wasm": "
|
|
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": "
|
|
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
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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
|
|
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 `
|
|
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
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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
|
|
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
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
|
|
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
|