lawspec 0.9.0 → 0.11.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.
Files changed (46) hide show
  1. package/API-MIGRATION.md +78 -2
  2. package/HASKELL.md +1 -0
  3. package/KOTLIN.md +5 -0
  4. package/LANGUAGE.md +70 -11
  5. package/NATIVE-BINDINGS.md +1049 -0
  6. package/PRIMITIVES.md +1 -1
  7. package/README.md +86 -23
  8. package/REFINEMENTS.md +6 -1
  9. package/RELEASE-0.10.md +60 -0
  10. package/RELEASE-0.11.md +62 -0
  11. package/api.mjs +23 -3
  12. package/bin/lawspec.mjs +17 -4
  13. package/build.json +60 -38
  14. package/core.wasm +0 -0
  15. package/examples/native-payments/go/example/payments/domain.go +38 -0
  16. package/examples/native-payments/go/example/payments/native_generators_test.go +13 -0
  17. package/examples/native-payments/go/lawspec.json +136 -0
  18. package/examples/native-payments/haskell/lawspec.json +149 -0
  19. package/examples/native-payments/haskell/src/PaymentsDomain.hs +21 -0
  20. package/examples/native-payments/haskell/test/PaymentGenerators.hs +12 -0
  21. package/examples/native-payments/java/lawspec.json +165 -0
  22. package/examples/native-payments/java/src/main/java/domain/PaymentsDomain.java +32 -0
  23. package/examples/native-payments/java/src/test/java/domain/PaymentGenerators.java +16 -0
  24. package/examples/native-payments/javascript/lawspec.json +149 -0
  25. package/examples/native-payments/javascript/src/payments_domain.mjs +44 -0
  26. package/examples/native-payments/javascript/test/lawspec_generators.mjs +6 -0
  27. package/examples/native-payments/kotlin/lawspec.json +165 -0
  28. package/examples/native-payments/kotlin/src/main/kotlin/domain/PaymentsDomain.kt +17 -0
  29. package/examples/native-payments/kotlin/src/test/kotlin/domain/PaymentGenerators.kt +12 -0
  30. package/examples/native-payments/python/lawspec.json +149 -0
  31. package/examples/native-payments/python/src/payments_domain.py +60 -0
  32. package/examples/native-payments/python/tests/lawspec_generators.py +15 -0
  33. package/examples/native-payments/rust/lawspec.json +167 -0
  34. package/examples/native-payments/rust/src/domain.rs +37 -0
  35. package/examples/native-payments/rust/src/lib.rs +2 -0
  36. package/examples/native-payments/rust/tests/support/lawspec_generators.rs +11 -0
  37. package/examples/native-payments/typescript/lawspec.json +149 -0
  38. package/examples/native-payments/typescript/src/payments_domain.ts +42 -0
  39. package/examples/native-payments/typescript/test/lawspec_generators.ts +8 -0
  40. package/examples/specs/indexed_families.lawspec +59 -0
  41. package/examples/specs/payments.lawspec +76 -0
  42. package/examples-command.mjs +5 -0
  43. package/files.mjs +6 -6
  44. package/index.d.ts +53 -2
  45. package/native-examples.mjs +81 -0
  46. package/package.json +2 -2
@@ -0,0 +1,59 @@
1
+ unit example.indexed
2
+
3
+ -- A list indexed by its length. Each constructor states how it changes the
4
+ -- index; nOfVec is the checked measure that recomputes it from a value.
5
+ type Vec (n :: Natural) (a :: Type) is
6
+ | VNil where n = 0
7
+ | VCons head :: a tail :: Vec m a where n = m + 1
8
+ end
9
+
10
+ -- A binary tree indexed by its number of values.
11
+ type Tree (n :: Natural) (a :: Type) is
12
+ | Tip where n = 0
13
+ | Bin left :: Tree l a value :: a right :: Tree r a where n = l + r + 1
14
+ end
15
+
16
+ -- Result indices are checked contracts over the native implementations.
17
+ replicate :: (n :: Natural where n < 64) -> (x :: Int8) -> (r :: Vec n Int8)
18
+ append :: (xs :: Vec n Int8) -> (ys :: Vec m Int8) -> (r :: Vec (n + m) Int8)
19
+ zip :: (xs :: Vec n Int8) -> (ys :: Vec n Bool) -> (r :: Vec n Bool)
20
+ flatten :: (tree :: Tree n Int8) -> (r :: Vec n Int8)
21
+
22
+ law `replicate has the requested length` is
23
+ definition is
24
+ `for all` (n :: Natural where n < 16) (x :: Int8) . nOfVec (replicate n x) = n
25
+ end
26
+ example `three copies` is
27
+ n = 3
28
+ x = 7
29
+ expect nOfVec (replicate n x) = 3
30
+ end
31
+ end
32
+
33
+ law `append adds lengths` is
34
+ definition is
35
+ `for all` (xs :: Vec n Int8) (ys :: Vec m Int8) .
36
+ nOfVec (append xs ys) = n + m
37
+ end
38
+ end
39
+
40
+ -- A fixed index is generated directly rather than filtered.
41
+ law `vectors of three elements` is
42
+ definition is
43
+ `for all` (xs :: Vec 3 Int8) . nOfVec (append xs xs) = 6
44
+ end
45
+ end
46
+
47
+ -- A shared index constrains the second input by the first.
48
+ law `zip preserves a shared length` is
49
+ definition is
50
+ `for all` (xs :: Vec n Int8) (ys :: Vec n Bool) . nOfVec (zip xs ys) = n
51
+ end
52
+ end
53
+
54
+ -- Generation splits the remaining index across both subtrees.
55
+ law `flattening five values` is
56
+ definition is
57
+ `for all` (tree :: Tree 5 Int8) . nOfVec (flatten tree) = 5
58
+ end
59
+ end
@@ -0,0 +1,76 @@
1
+ unit example.payments
2
+
3
+ -- The executable acceptance model for native domain bindings in 0.10.
4
+ -- Applications may call Money "Price", amount "major", and currency "unit";
5
+ -- those representation choices must not alter these laws.
6
+ type Currency is USD | EUR | GBP end
7
+
8
+ type Money is
9
+ Money amount :: Decimal currency :: Currency
10
+ end
11
+
12
+ type Payment is
13
+ Paid value :: Money
14
+ Declined reason :: Text
15
+ end
16
+
17
+ -- These are adapter functions supplied by the application.
18
+ addFee :: Money -> Money
19
+ roundTrip :: Payment -> Payment
20
+ archive :: List (Maybe Payment) -> List (Maybe Payment)
21
+
22
+ -- The model is exact, independent of host decimal rounding settings.
23
+ definition feeModel (money :: Money) :: Money is
24
+ match money with
25
+ | Money amount currency -> Money (amount + 0.2) currency
26
+ end
27
+ end
28
+
29
+ law `fees preserve currency and exact decimal value` is
30
+ definition is
31
+ `for all` (money :: Money) . addFee money = feeModel money
32
+ end
33
+ example `decimal tenths` is
34
+ money = Money 0.1 USD
35
+ expect addFee money = Money 0.3 USD
36
+ end
37
+ example `small amount is not rounded away` is
38
+ money = Money 0.000000000000000000000000000001 EUR
39
+ expect addFee money = Money 0.200000000000000000000000000001 EUR
40
+ end
41
+ example `beyond binary floating precision` is
42
+ money = Money 9007199254740993.1 GBP
43
+ expect addFee money = Money 9007199254740993.3 GBP
44
+ end
45
+ end
46
+
47
+ law `payment variants and payloads survive native conversion` is
48
+ definition is
49
+ `for all` (payment :: Payment) . roundTrip payment = payment
50
+ end
51
+ example `successful payment` is
52
+ payment = Paid (Money 12.34 EUR)
53
+ expect roundTrip payment = Paid (Money 12.34 EUR)
54
+ end
55
+ example `declined payment` is
56
+ payment = Declined "card declined"
57
+ expect roundTrip payment = Declined "card declined"
58
+ end
59
+ end
60
+
61
+ law `archives preserve order duplicates and absence` is
62
+ definition is
63
+ `for all` (payments :: List (Maybe Payment)) . archive payments = payments
64
+ end
65
+ example `empty archive` is
66
+ payments = []
67
+ expect archive payments = []
68
+ end
69
+ example `missing is different from a declined payment` is
70
+ payments = [Nothing, Just (Declined ""), Just (Paid (Money 0.1 USD)),
71
+ Just (Paid (Money 0.1 USD))]
72
+ expect archive payments = [Nothing, Just (Declined ""),
73
+ Just (Paid (Money 0.1 USD)),
74
+ Just (Paid (Money 0.1 USD))]
75
+ end
76
+ end
@@ -3,11 +3,16 @@ import path from "node:path";
3
3
  import { createCompiler } from "./api.mjs";
4
4
  import { targets } from "./templates.mjs";
5
5
  import { safePath, planWrites, applyWrites } from "./files.mjs";
6
+ import { exportNativePayments } from "./native-examples.mjs";
6
7
 
7
8
  export async function generateExamples(options) {
8
9
  const selected = options.target ? [options.target] : targets;
9
10
  if (selected.some((t) => !targets.includes(t)))
10
11
  throw new Error(`Unknown target: ${options.target}`);
12
+ if (options.example !== undefined) {
13
+ if (options.example !== "payments") throw new Error(`Unknown example: ${options.example}`);
14
+ return exportNativePayments(options, selected);
15
+ }
11
16
  const directory = new URL("./examples/specs/", import.meta.url);
12
17
  const sources = await Promise.all(
13
18
  (await readdir(directory))
package/files.mjs CHANGED
@@ -63,8 +63,8 @@ export async function atomicWrite(file, content, exclusive = false) {
63
63
  }
64
64
  }
65
65
  }
66
- export async function planWrites(root, artifacts) {
67
- const manifestPath = await safePath(root, ".lawspec/generated.json");
66
+ export async function planWrites(root, artifacts, {manifest = ".lawspec/generated.json"} = {}) {
67
+ const manifestPath = await safePath(root, manifest);
68
68
  const previousText = await readOptional(manifestPath);
69
69
  const previous =
70
70
  previousText === null
@@ -144,18 +144,18 @@ export async function planWrites(root, artifacts) {
144
144
  throw new Error(`Refusing to remove edited generated file: ${relative}`);
145
145
  changes.push({ action: "remove", file, relative, expected: old });
146
146
  }
147
- const manifest =
147
+ const manifestContent =
148
148
  JSON.stringify(
149
149
  { version: 1, files: next, adapters: adapterHashes },
150
150
  null,
151
151
  2,
152
152
  ) + "\n";
153
- if (manifest !== previousText)
153
+ if (manifestContent !== previousText)
154
154
  changes.push({
155
155
  action: previousText === null ? "create" : "update",
156
156
  file: manifestPath,
157
- relative: ".lawspec/generated.json",
158
- content: manifest,
157
+ relative: manifest,
158
+ content: manifestContent,
159
159
  expected: previousText,
160
160
  });
161
161
  return { root, changes, preserved, adapterUpdates };
package/index.d.ts CHANGED
@@ -162,11 +162,62 @@ export interface Refinement {
162
162
  definition: string;
163
163
  }
164
164
 
165
+ export type NativeReference = string[];
166
+
167
+ export interface NativeFieldBinding {
168
+ field: string;
169
+ native: string;
170
+ }
171
+
172
+ export interface NativeConstructorBinding {
173
+ constructor: string;
174
+ native: NativeReference;
175
+ fields?: NativeFieldBinding[];
176
+ style: 'record' | 'variant' | 'unit';
177
+ }
178
+
179
+ export interface NativeCodecBinding {
180
+ toNative: NativeReference;
181
+ fromNative: NativeReference;
182
+ }
183
+
184
+ export interface NativeTypeBinding {
185
+ type: string;
186
+ native: NativeReference;
187
+ constructors?: NativeConstructorBinding[];
188
+ codec?: NativeCodecBinding;
189
+ }
190
+
191
+ export interface NativeGeneratorBinding {
192
+ type: string;
193
+ factory: NativeReference;
194
+ stub?: boolean;
195
+ }
196
+
197
+ export interface NativeFunctionBinding {
198
+ declaration: string;
199
+ native: NativeReference;
200
+ }
201
+
202
+ export interface NativeGoImport {
203
+ alias: string;
204
+ path: string;
205
+ }
206
+
207
+ export interface NativeBindings {
208
+ types?: NativeTypeBinding[];
209
+ generators?: NativeGeneratorBinding[];
210
+ functions?: NativeFunctionBinding[];
211
+ rustCrate?: string;
212
+ goImports?: NativeGoImport[];
213
+ }
214
+
165
215
  export interface CheckRequest {
166
216
  sources: Source[];
167
- schemaVersion?: 3;
217
+ schemaVersion?: 3 | 4;
168
218
  machineBits?: 32 | 64;
169
219
  generation?: Partial<Generation>;
220
+ nativeBindings?: NativeBindings;
170
221
  }
171
222
 
172
223
  export interface GenerationRequest extends CheckRequest {
@@ -177,7 +228,7 @@ export interface GenerationRequest extends CheckRequest {
177
228
  }
178
229
 
179
230
  export interface Result {
180
- schemaVersion: 3;
231
+ schemaVersion: 3 | 4;
181
232
  machineBits?: 32 | 64;
182
233
  generation?: Generation;
183
234
  units?: Unit[];
@@ -0,0 +1,81 @@
1
+ import {readFile, readdir, mkdir} from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {templates, commands, setup} from './templates.mjs';
4
+ import {safePath, planWrites, applyWrites} from './files.mjs';
5
+
6
+ async function projectFiles(directory, prefix = '') {
7
+ const files = {};
8
+ for (const entry of await readdir(directory, {withFileTypes: true})) {
9
+ const name = prefix + entry.name;
10
+ const location = new URL(entry.name + (entry.isDirectory() ? '/' : ''), directory);
11
+ if (entry.isDirectory()) Object.assign(files, await projectFiles(location, name + '/'));
12
+ else if (entry.isFile()) files[name] = await readFile(location, 'utf8');
13
+ else throw new Error(`Unsupported bundled example entry: ${name}`);
14
+ }
15
+ return files;
16
+ }
17
+
18
+ export async function exportNativePayments(options, selected) {
19
+ const root = await safePath(process.cwd(), options.output || 'native_payments');
20
+ await mkdir(root, {recursive: true});
21
+ const law = await readFile(new URL('./examples/specs/payments.lawspec', import.meta.url), 'utf8');
22
+ const plans = [];
23
+ const results = [];
24
+ for (const target of selected) {
25
+ const directory = await safePath(root, target);
26
+ await mkdir(directory, {recursive: true});
27
+ const files = {
28
+ ...templates(target, {minify: options.minify === true}),
29
+ ...await projectFiles(new URL(`./examples/native-payments/${target}/`, import.meta.url)),
30
+ 'laws/payments.lawspec': law,
31
+ };
32
+ const config = JSON.parse(files['lawspec.json']);
33
+ config.machineBits = options.machineBits ?? 64;
34
+ files['lawspec.json'] = JSON.stringify(config, null, options.minify ? undefined : 2) + '\n';
35
+ files['README.md'] = `# Native payments: ${target}
36
+
37
+ This project checks application-owned payment types against the shared LawSpec
38
+ model. Money maps to Price, currency variants have application names, and the
39
+ archive preserves distinct present and absent payments.
40
+
41
+ ## Run
42
+
43
+ Install LawSpec and the native dependencies described below, then run:
44
+
45
+ \`\`\`sh
46
+ lawspec check
47
+ lawspec generate
48
+ ${commands[target]}
49
+ \`\`\`
50
+
51
+ ${setup[target]}
52
+
53
+ JavaScript and TypeScript projects require \`npm install\` before testing.
54
+ The specification includes exact decimal arithmetic and explicit USD/GBP examples.
55
+ The native price generator deliberately samples only EUR values between 1.00 and
56
+ 2.00; examples and deterministic boundaries still check the other cases. Its
57
+ framework's mapping operation preserves the native shrinker.
58
+
59
+ The application types use native exact decimals where available and LawSpec's
60
+ framework-independent numeric runtime otherwise. Run generation before compiling
61
+ the application so that runtime support is present.
62
+
63
+ ## Change the application
64
+
65
+ Edit the domain implementation or generator, then run the native tests again.
66
+ For example, change the fee from 0.2 to 0.3: the exact-decimal law must fail.
67
+ Edit \`lawspec.json\` to change native bindings and rerun \`lawspec generate\`.
68
+
69
+ All exported project files are user-owned. Re-exporting preserves your edits;
70
+ it reports changed bundled versions for review. Compiler-generated files use a
71
+ separate ownership manifest and remain protected against accidental overwrites.
72
+ `;
73
+ const artifacts = Object.entries(files).map(([path, content]) => ({path, content, ownership: 'user'}));
74
+ const plan = await planWrites(directory, artifacts, {manifest: '.lawspec/example.json'});
75
+ plans.push(plan);
76
+ results.push({target, directory, example: 'payments', files: artifacts.map(({path, ownership}) => ({path, ownership})),
77
+ changes: plan.changes.length, preservedAdapters: plan.preserved, adapterUpdates: plan.adapterUpdates});
78
+ }
79
+ await applyWrites(plans);
80
+ return results;
81
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lawspec",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "State the law once. Check it everywhere.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -8,6 +8,6 @@
8
8
  "bin": { "lawspec": "bin/lawspec.mjs" },
9
9
  "exports": { ".": { "types": "./index.d.ts", "import": "./api.mjs" } },
10
10
  "types": "index.d.ts",
11
- "files": ["*.mjs", "*.json", "index.d.ts", "bin", "core.wasm", "core_jsffi.js", "README.md", "PRIMITIVES.md", "REFINEMENTS.md", "RUST.md", "JAVA.md", "PYTHON.md", "GO.md", "HASKELL.md", "KOTLIN.md", "WEB.md", "LANGUAGE.md", "API-MIGRATION.md", "RELEASE-0.9.md", "LICENSE", "starter.lawspec", "examples"],
11
+ "files": ["*.mjs", "*.json", "index.d.ts", "bin", "core.wasm", "core_jsffi.js", "README.md", "NATIVE-BINDINGS.md", "PRIMITIVES.md", "REFINEMENTS.md", "RUST.md", "JAVA.md", "PYTHON.md", "GO.md", "HASKELL.md", "KOTLIN.md", "WEB.md", "LANGUAGE.md", "API-MIGRATION.md", "RELEASE-0.9.md", "RELEASE-0.10.md", "RELEASE-0.11.md", "LICENSE", "starter.lawspec", "examples"],
12
12
  "scripts": { "test": "node --test test/*.test.mjs" }
13
13
  }