lawspec 0.2.1 → 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 +143 -20
- package/bin/lawspec.mjs +55 -4
- package/build.json +10 -10
- package/core.wasm +0 -0
- package/examples/specs/atoi_codec.lawspec +6 -2
- package/examples/specs/canonical_url.lawspec +24 -0
- package/examples/specs/equivalent.lawspec +12 -4
- package/examples/specs/mixed_inputs.lawspec +41 -0
- package/examples/specs/slug.lawspec +33 -0
- package/examples-command.mjs +51 -0
- package/index.d.ts +3 -2
- 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.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.
|
|
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.
|
|
29
|
-
npm install --save-dev lawspec@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.
|
|
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.
|
|
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
|
|
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,14 +139,16 @@ 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
|
```
|
|
147
149
|
|
|
148
150
|
The implicit prelude defines `left inverse`, `round trip identity is preserved`,
|
|
149
|
-
and `
|
|
151
|
+
`equivalent`, and `idempotent`.
|
|
150
152
|
A law may reference a local reusable law or a prelude law. The compiler performs
|
|
151
153
|
capture-avoiding expansion and specializes types; it does not recognize codec
|
|
152
154
|
function names specially. `explain` shows the final property:
|
|
@@ -157,15 +159,18 @@ 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 literals and equality. Function signatures use `Int32`
|
|
161
|
-
and `Text`; generic variables are supported in reusable laws. v0.
|
|
162
|
-
quantified `Int32` inputs, including multiple inputs.
|
|
163
|
-
or compared
|
|
162
|
+
quantification, integer and text literals, and equality. Function signatures use `Int32`
|
|
163
|
+
and `Text`; generic variables are supported in reusable laws. v0.4 generates
|
|
164
|
+
quantified `Int32` and `Text` inputs, including mixed and multiple inputs. Both
|
|
165
|
+
types can also be intermediate or compared results. Functions are synchronous
|
|
166
|
+
and unary. Text literals are double-quoted, with escapes such as `\"`, `\\`,
|
|
167
|
+
`\n`, and `\t`; examples must bind each input to a literal of its declared type.
|
|
168
|
+
Text values contain Unicode scalar values; surrogate code points are rejected.
|
|
164
169
|
|
|
165
170
|
Examples refer to the expanded input names, including names inherited from the
|
|
166
171
|
prelude. Bind every input exactly once. Ambiguous names and out-of-range values
|
|
167
172
|
are errors. Descriptions and rationales use `{function}` references; `{{` and `}}`
|
|
168
|
-
produce literal braces.
|
|
173
|
+
produce literal braces. Law blocks use this order:
|
|
169
174
|
definition, optional description, optional rationale, examples, optional references.
|
|
170
175
|
`--` starts a line comment. Names that cannot be emitted portably are diagnosed.
|
|
171
176
|
|
|
@@ -173,6 +178,40 @@ Additional primitives, external law packages, cross-unit imports beyond the
|
|
|
173
178
|
prelude, async functions, direct existing-symbol binding and browser hosting are
|
|
174
179
|
outside this release.
|
|
175
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
|
+
|
|
176
215
|
## Comparing alternative implementations
|
|
177
216
|
|
|
178
217
|
`equivalent` compares two functions with the same input and output types. Its
|
|
@@ -189,8 +228,10 @@ law `decimal renderers agree` is
|
|
|
189
228
|
definition is
|
|
190
229
|
`equivalent` render referenceRender
|
|
191
230
|
end
|
|
192
|
-
example `negative
|
|
231
|
+
example `both renderers produce a negative decimal string` is
|
|
193
232
|
x = -42
|
|
233
|
+
expect render x = "-42"
|
|
234
|
+
expect referenceRender x = "-42"
|
|
194
235
|
end
|
|
195
236
|
end
|
|
196
237
|
```
|
|
@@ -199,7 +240,7 @@ This expands to `for all (x :: Int32) . render (x) = referenceRender (x)`.
|
|
|
199
240
|
The example inherits the input name `x` from the prelude. Both functions are
|
|
200
241
|
user-owned adapter functions; either may delegate to your existing code.
|
|
201
242
|
|
|
202
|
-
[The complete example](https://github.com/brain-fuel/lawspec/blob/v0.
|
|
243
|
+
[The complete example](https://github.com/brain-fuel/lawspec/blob/v0.4.0/examples/specs/equivalent.lawspec) compares decimal
|
|
203
244
|
renderers and two implementations that clamp negative integers to zero. For
|
|
204
245
|
JavaScript, their adapters can be:
|
|
205
246
|
|
|
@@ -212,9 +253,91 @@ export const referenceClamp = x => x < 0 ? 0 : x;
|
|
|
212
253
|
|
|
213
254
|
The same specification generates native tests for all seven targets. The
|
|
214
255
|
integration suite checks both examples with matching implementations, then
|
|
215
|
-
breaks each alternative separately to verify detection.
|
|
216
|
-
|
|
217
|
-
|
|
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`.
|
|
259
|
+
|
|
260
|
+
## Text properties and idempotence
|
|
261
|
+
|
|
262
|
+
```lawspec
|
|
263
|
+
unit example.slug
|
|
264
|
+
|
|
265
|
+
normalize :: Text -> Text
|
|
266
|
+
referenceNormalize :: Text -> Text
|
|
267
|
+
|
|
268
|
+
law `normalizers agree` is
|
|
269
|
+
definition is
|
|
270
|
+
`equivalent` normalize referenceNormalize
|
|
271
|
+
end
|
|
272
|
+
example `spaces become hyphens; punctuation is preserved` is
|
|
273
|
+
x = "Hello, World!"
|
|
274
|
+
expect normalize x = "Hello,-World!"
|
|
275
|
+
expect referenceNormalize x = "Hello,-World!"
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
The [slug example](https://github.com/brain-fuel/lawspec/blob/v0.4.0/examples/specs/slug.lawspec)
|
|
281
|
+
compares two implementations of ASCII-space replacement. It includes empty,
|
|
282
|
+
Unicode and escaped text. Each target uses its native string generator:
|
|
283
|
+
JetCheck `Generator.stringsOf(Generator.asciiPrintableChars())`, Hypothesis `st.text()`, fast-check `fc.string()`,
|
|
284
|
+
Rapid `rapid.String()`, Hedgehog `Gen.text`, or Kotest `Arb.string()`.
|
|
285
|
+
Generator distributions differ between libraries; the generated tests also
|
|
286
|
+
exercise deterministic empty, whitespace, Unicode, combining-mark and escaped
|
|
287
|
+
control-character cases. Hedgehog's generated text length range is 0–100.
|
|
288
|
+
|
|
289
|
+
The prelude's `idempotent` law requires `f (f x) = f x`:
|
|
290
|
+
|
|
291
|
+
```lawspec
|
|
292
|
+
unit example.canonical_url
|
|
293
|
+
|
|
294
|
+
canonicalize :: Text -> Text
|
|
295
|
+
|
|
296
|
+
law `canonicalization reaches a fixed point` is
|
|
297
|
+
definition is
|
|
298
|
+
`idempotent` canonicalize
|
|
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
|
|
304
|
+
end
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
The [canonical URL example](https://github.com/brain-fuel/lawspec/blob/v0.4.0/examples/specs/canonical_url.lawspec)
|
|
308
|
+
uses removal of **all trailing slashes** as a small fixed-point demonstration,
|
|
309
|
+
not a complete URL canonicalization algorithm. For JavaScript:
|
|
310
|
+
|
|
311
|
+
```javascript
|
|
312
|
+
export const canonicalize = value => value.replace(/\/+$/, "");
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Removing just one trailing slash fails the supplied repeated-slash example.
|
|
316
|
+
The [mixed-input example](https://github.com/brain-fuel/lawspec/blob/v0.4.0/examples/specs/mixed_inputs.lawspec)
|
|
317
|
+
shows `Text` and `Int32` in the same quantified property and executable example.
|
|
318
|
+
The JavaScript API represents input bindings and expected values as `number | string`.
|
|
319
|
+
Each example includes `expectations: { actual: Expr; expected: number | string }[]`.
|
|
320
|
+
|
|
321
|
+
## Generate all example artifacts
|
|
322
|
+
|
|
323
|
+
```sh
|
|
324
|
+
npx lawspec examples
|
|
325
|
+
# Or select a target and a relative output directory:
|
|
326
|
+
npx lawspec examples --target java --output example_artifacts
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
This command works without a project configuration or native build tools. It
|
|
330
|
+
compiles every bundled example and writes its tests and user-owned stubs to
|
|
331
|
+
`example_artifacts/<language>/`, using each target's normal source/test layout.
|
|
332
|
+
By default it exports all seven languages; `--json` returns the file inventory.
|
|
333
|
+
From a checkout, `make examples` runs the same command.
|
|
334
|
+
|
|
335
|
+
These are inspection artifacts, not initialized projects: no build files are
|
|
336
|
+
created and dependency compatibility is not checked. To run them, configure the
|
|
337
|
+
corresponding native project and implement the stubs. Regeneration preserves
|
|
338
|
+
user-owned stubs and refuses to overwrite edited generated tests. Each target
|
|
339
|
+
has its own ownership manifest. Output paths must be relative and cannot use
|
|
340
|
+
parent traversal or symlinks. The default directory is ignored by Git.
|
|
218
341
|
|
|
219
342
|
## Ownership
|
|
220
343
|
|
|
@@ -255,7 +378,7 @@ by the JS shim.
|
|
|
255
378
|
## Build and verify
|
|
256
379
|
|
|
257
380
|
For contributors working from a repository checkout, build a local archive with
|
|
258
|
-
`npm pack ./npm` and install it with `npm install --save-dev ./lawspec-0.
|
|
381
|
+
`npm pack ./npm` and install it with `npm install --save-dev ./lawspec-0.4.0.tgz`.
|
|
259
382
|
The package payload lives in `npm/`.
|
|
260
383
|
|
|
261
384
|
```sh
|
package/bin/lawspec.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { readFile, mkdir, readdir, stat } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createCompiler } from "../api.mjs";
|
|
5
5
|
import { targets, templates, commands, setup } from "../templates.mjs";
|
|
6
|
+
import { generateExamples } from "../examples-command.mjs";
|
|
6
7
|
import { doctor } from "../doctor.mjs";
|
|
7
8
|
import {
|
|
8
9
|
readOptional,
|
|
@@ -17,7 +18,7 @@ const options = {};
|
|
|
17
18
|
const positional = [];
|
|
18
19
|
for (let i = 0; i < args.length; i++) {
|
|
19
20
|
const arg = args[i];
|
|
20
|
-
if (["--target", "--project", "--config"].includes(arg)) {
|
|
21
|
+
if (["--target", "--project", "--config", "--output"].includes(arg)) {
|
|
21
22
|
if (!args[i + 1] || args[i + 1].startsWith("--"))
|
|
22
23
|
throw new Error(`Missing value for ${arg}`);
|
|
23
24
|
options[arg.slice(2)] = args[++i];
|
|
@@ -150,20 +151,70 @@ async function init() {
|
|
|
150
151
|
`Configured ${language}. ${hasBuild ? "Existing build files preserved." : "Created missing project build files."}\n${setup[language]}\nNext: lawspec doctor, then lawspec generate.`,
|
|
151
152
|
);
|
|
152
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
|
+
}
|
|
153
181
|
async function main() {
|
|
154
182
|
if (!verb || ["help", "--help", "-h"].includes(verb)) {
|
|
155
183
|
output(
|
|
156
|
-
"LawSpec 0.
|
|
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: " +
|
|
157
185
|
targets.join(", "),
|
|
158
186
|
);
|
|
159
187
|
return;
|
|
160
188
|
}
|
|
161
189
|
if (verb === "--version") {
|
|
162
|
-
output("0.
|
|
190
|
+
output("0.4.0");
|
|
163
191
|
return;
|
|
164
192
|
}
|
|
165
193
|
if (positional.length > (verb === "explain" ? 1 : 0))
|
|
166
194
|
throw new Error(`Unexpected argument: ${positional.join(" ")}`);
|
|
195
|
+
if (verb === "examples") {
|
|
196
|
+
if (
|
|
197
|
+
options.config ||
|
|
198
|
+
options.project ||
|
|
199
|
+
options["dry-run"] ||
|
|
200
|
+
options.check
|
|
201
|
+
)
|
|
202
|
+
throw new Error("examples supports --target, --output and --json only");
|
|
203
|
+
const result = await generateExamples(options);
|
|
204
|
+
output(
|
|
205
|
+
options.json
|
|
206
|
+
? result
|
|
207
|
+
: result
|
|
208
|
+
.map(
|
|
209
|
+
(r) =>
|
|
210
|
+
`${r.target}: ${r.files.length} artifacts in ${r.directory}; ${r.preservedAdapters.length} user adapters preserved.`,
|
|
211
|
+
)
|
|
212
|
+
.join("\n") +
|
|
213
|
+
"\nInspection artifacts only; native toolchains and dependencies are not checked. Stubs must be implemented before running tests.",
|
|
214
|
+
);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (options.output) throw new Error("--output is only supported by examples");
|
|
167
218
|
if (verb === "init") return init();
|
|
168
219
|
if (!["check", "doctor", "explain", "generate"].includes(verb))
|
|
169
220
|
throw new Error(`Unknown command: ${verb}`);
|
|
@@ -226,7 +277,7 @@ async function main() {
|
|
|
226
277
|
: indices
|
|
227
278
|
.map(
|
|
228
279
|
({ e, i }) =>
|
|
229
|
-
`${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)}`,
|
|
230
281
|
)
|
|
231
282
|
.join("\n\n"),
|
|
232
283
|
);
|
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": "2c1176c912901bd218743a88df1b67fb1ee8d58ed73fcbe7e2f23827f10a3a0f",
|
|
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": "336eca415ed6c8594612ad5acfc7ff6f29ef387cf3cb374b60b2407f811c31c7",
|
|
23
|
+
"src/LawSpec/Emit.hs": "2e23ca38aa5c4c65305283705633209843dd6ba116c43ba8a42ee8731040f7c3",
|
|
24
|
+
"src/LawSpec/Gen.hs": "305d7710dbc69b25d95fa6c0ac9a59622481f6b89682c1e5c9048def3962315a",
|
|
25
|
+
"src/LawSpec/Model.hs": "d1eda2b420edbc327b1adbf437c6f39a9e576ef0ab9b82f468c28202a433eb96",
|
|
26
|
+
"src/LawSpec/Parser.hs": "7fef1876c52d2204f852704150288910bb0c12bcde1d3159219ad25526a879b6",
|
|
27
|
+
"src/LawSpec/Prelude.hs": "0b60dd85bdf0642077dfb497043c63606d507e71e12d3ed8bfe2bf773214c66b",
|
|
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": "098f1d0fc52575907312c95095846d1d4f6e7ace7cfbe0419a305b9fe8a75a92",
|
|
34
|
+
"npm/core.wasm": "0a4ce15cefcd866cd7dd584c32d683c7f8a46823104afcceba1298570883a88a",
|
|
35
35
|
"npm/core_jsffi.js": "88d136efe92f7cff5758c8fec8d9b6bbc9707fe37741cebeeb415fe34ce3d72b",
|
|
36
36
|
"npm/api.mjs": "d6df654600172131ac66a55b86876fc29cde2a76a0a63bbefff3183488a4837b",
|
|
37
|
-
"npm/index.d.ts": "
|
|
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
|
|
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,24 @@
|
|
|
1
|
+
unit example.canonical_url
|
|
2
|
+
|
|
3
|
+
canonicalize :: Text -> Text
|
|
4
|
+
|
|
5
|
+
law `canonicalization reaches a fixed point` is
|
|
6
|
+
definition is
|
|
7
|
+
`idempotent` canonicalize
|
|
8
|
+
end
|
|
9
|
+
description is
|
|
10
|
+
"{canonicalize} removes trailing slashes until the result is stable"
|
|
11
|
+
end
|
|
12
|
+
example `all trailing slashes are removed in one pass` is
|
|
13
|
+
x = "https://example.com/path///"
|
|
14
|
+
expect canonicalize x = "https://example.com/path"
|
|
15
|
+
end
|
|
16
|
+
example `a URL without trailing slashes stays unchanged` is
|
|
17
|
+
x = "https://example.com/path"
|
|
18
|
+
expect canonicalize x = "https://example.com/path"
|
|
19
|
+
end
|
|
20
|
+
example `empty input remains empty` is
|
|
21
|
+
x = ""
|
|
22
|
+
expect canonicalize x = ""
|
|
23
|
+
end
|
|
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
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
unit example.mixed.inputs
|
|
2
|
+
|
|
3
|
+
normalize :: Text -> Text
|
|
4
|
+
identity :: Int32 -> Int32
|
|
5
|
+
|
|
6
|
+
law `text normalization reaches a fixed point with an unused integer input` is
|
|
7
|
+
definition is
|
|
8
|
+
`for all` (text :: Text) (number :: Int32) .
|
|
9
|
+
normalize text = normalize (normalize text)
|
|
10
|
+
end
|
|
11
|
+
example `text normalization and integer identity have explicit results` is
|
|
12
|
+
text = "Hello, World!"
|
|
13
|
+
number = -42
|
|
14
|
+
expect normalize text = "Hello,-World!"
|
|
15
|
+
expect identity number = -42
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
law `integer identity with an unused text input` is
|
|
20
|
+
definition is
|
|
21
|
+
`for all` (text :: Text) (number :: Int32) .
|
|
22
|
+
identity number = number
|
|
23
|
+
end
|
|
24
|
+
example `text normalization and integer identity have explicit results` is
|
|
25
|
+
text = "λ 😀"
|
|
26
|
+
number = 2147483647
|
|
27
|
+
expect normalize text = "λ-😀"
|
|
28
|
+
expect identity number = 2147483647
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
law `normalizing an empty literal` is
|
|
33
|
+
definition is
|
|
34
|
+
`for all` (text :: Text) .
|
|
35
|
+
normalize "" = ""
|
|
36
|
+
end
|
|
37
|
+
example `empty literal stays empty; quantified text is unused` is
|
|
38
|
+
text = "unused context"
|
|
39
|
+
expect normalize "" = ""
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
unit example.slug
|
|
2
|
+
|
|
3
|
+
normalize :: Text -> Text
|
|
4
|
+
referenceNormalize :: Text -> Text
|
|
5
|
+
|
|
6
|
+
law `normalizers agree` is
|
|
7
|
+
definition is
|
|
8
|
+
`equivalent` normalize referenceNormalize
|
|
9
|
+
end
|
|
10
|
+
description is
|
|
11
|
+
"{normalize} and {referenceNormalize} replace ASCII spaces with hyphens"
|
|
12
|
+
end
|
|
13
|
+
example `spaces become hyphens; punctuation is preserved` is
|
|
14
|
+
x = "Hello, World!"
|
|
15
|
+
expect normalize x = "Hello,-World!"
|
|
16
|
+
expect referenceNormalize x = "Hello,-World!"
|
|
17
|
+
end
|
|
18
|
+
example `empty text remains empty` is
|
|
19
|
+
x = ""
|
|
20
|
+
expect normalize x = ""
|
|
21
|
+
expect referenceNormalize x = ""
|
|
22
|
+
end
|
|
23
|
+
example `Unicode is preserved while spaces become hyphens` is
|
|
24
|
+
x = "café 日本語 😀"
|
|
25
|
+
expect normalize x = "café-日本語-😀"
|
|
26
|
+
expect referenceNormalize x = "café-日本語-😀"
|
|
27
|
+
end
|
|
28
|
+
example `quotes, backslashes and controls survive space replacement` is
|
|
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:-$"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { readFile, readdir, mkdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createCompiler } from "./api.mjs";
|
|
4
|
+
import { targets } from "./templates.mjs";
|
|
5
|
+
import { safePath, planWrites, applyWrites } from "./files.mjs";
|
|
6
|
+
|
|
7
|
+
export async function generateExamples(options) {
|
|
8
|
+
const selected = options.target ? [options.target] : targets;
|
|
9
|
+
if (selected.some((t) => !targets.includes(t)))
|
|
10
|
+
throw new Error(`Unknown target: ${options.target}`);
|
|
11
|
+
const directory = new URL("./examples/specs/", import.meta.url);
|
|
12
|
+
const sources = await Promise.all(
|
|
13
|
+
(await readdir(directory))
|
|
14
|
+
.filter((n) => n.endsWith(".lawspec"))
|
|
15
|
+
.sort()
|
|
16
|
+
.map(async (name) => ({
|
|
17
|
+
path: name,
|
|
18
|
+
content: await readFile(new URL(name, directory), "utf8"),
|
|
19
|
+
})),
|
|
20
|
+
);
|
|
21
|
+
const compiler = await createCompiler();
|
|
22
|
+
const artifacts = [];
|
|
23
|
+
for (const target of selected) {
|
|
24
|
+
const result = await compiler.planGeneration({ sources, target });
|
|
25
|
+
if (result.diagnostics.length)
|
|
26
|
+
throw Object.assign(new Error("Bundled examples failed to compile"), {
|
|
27
|
+
diagnostics: result.diagnostics,
|
|
28
|
+
});
|
|
29
|
+
artifacts.push(result.files);
|
|
30
|
+
}
|
|
31
|
+
const root = await safePath(
|
|
32
|
+
process.cwd(),
|
|
33
|
+
options.output || "example_artifacts",
|
|
34
|
+
);
|
|
35
|
+
await mkdir(root, { recursive: true });
|
|
36
|
+
const plans = [];
|
|
37
|
+
for (let i = 0; i < selected.length; i++) {
|
|
38
|
+
const targetRoot = await safePath(root, selected[i]);
|
|
39
|
+
await mkdir(targetRoot, { recursive: true });
|
|
40
|
+
plans.push(await planWrites(targetRoot, artifacts[i]));
|
|
41
|
+
}
|
|
42
|
+
await applyWrites(plans);
|
|
43
|
+
return plans.map((plan, i) => ({
|
|
44
|
+
target: selected[i],
|
|
45
|
+
directory: plan.root,
|
|
46
|
+
files: artifacts[i].map((f) => ({ path: f.path, ownership: f.ownership })),
|
|
47
|
+
changes: plan.changes.length,
|
|
48
|
+
preservedAdapters: plan.preserved,
|
|
49
|
+
adapterUpdates: plan.adapterUpdates,
|
|
50
|
+
}));
|
|
51
|
+
}
|
package/index.d.ts
CHANGED
|
@@ -5,9 +5,10 @@ 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'; contents: string} | {tag: 'Number'; contents: number} | {tag: 'Apply' | 'Compose'; contents: [Expr, Expr]};
|
|
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
|
|
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
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
|