bend-schema 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nohzafk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,230 @@
1
+ # bend-schema
2
+
3
+ A schema library whose checker is **proved correct**, not just tested.
4
+
5
+ Define a schema once. Check JSON against it from TypeScript or from
6
+ [Bend](https://github.com/bendlang/bend), and get the first error with
7
+ its path. The checker is written in Bend, and its laws are machine-checked
8
+ for every schema and every value.
9
+
10
+ The npm package and the GitHub repository are `bend-schema`. The BendHub
11
+ package is `bend-schema-lib`.
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ bun add bend-schema
17
+ ```
18
+
19
+ Use it with [Bun](https://bun.sh). The package ships TypeScript source, and
20
+ Node.js refuses to run TypeScript from `node_modules`. You do not need Bend
21
+ installed.
22
+
23
+ The `bend-schema` command emits Bend source, and it runs under Bun whether you
24
+ start it with `bunx` or `npx`: its shebang is `env bun`.
25
+
26
+ ## Quick start
27
+
28
+ ```ts
29
+ import { s, type Infer } from "bend-schema";
30
+
31
+ const Plan = s.object({
32
+ name: s.str().len(1, 64),
33
+ seats: s.nat().in(1, 500),
34
+ tier: s.enum(["free", "pro"]),
35
+ email: s.str().refine((x) => x.includes("@"), "must be an email"),
36
+ note: s.str().optional(),
37
+ }).strict();
38
+ type Plan = Infer<typeof Plan>;
39
+
40
+ const r = Plan.parse(JSON.parse(body));
41
+ if (!r.ok) {
42
+ console.error(r.error.text("plan")); // "plan.seats: must be from 1 to 500"
43
+ }
44
+ ```
45
+
46
+ Every schema has three methods:
47
+
48
+ - `parse(value)` checks the value, then returns it typed.
49
+ - `check(value)` returns the first error, or `null`.
50
+ - `encode(value)` turns a typed value back into JSON-ready data.
51
+
52
+ An error has a `path`, a `message`, and `proved`. `proved: true` means the
53
+ error came from the verified core. `proved: false` means it came from your
54
+ own `.refine()` function, which runs only after the proved check passes.
55
+
56
+ ## Schemas
57
+
58
+ | Builder | Accepts |
59
+ |---|---|
60
+ | `s.nat()`, `.in(lo, hi)` | integer 0 to 2^48-1 |
61
+ | `s.str()`, `.len(lo, hi)` | string |
62
+ | `s.bool()`, `s.true()` | boolean, or only `true` |
63
+ | `s.enum([...])` | one of the given strings |
64
+ | `s.list(x)`, `.len(lo, hi)` | array of `x` |
65
+ | `s.tuple(a, b, ...)` | fixed-length array |
66
+ | `s.object({...})`, `.strict()` | object; `.strict()` refuses unknown keys |
67
+ | `s.tagged(key, {...})` | union chosen by a tag key |
68
+ | `s.oneKey({...})` | object with exactly one of the keys |
69
+ | `.optional()` | the object key may be absent |
70
+ | `.nullable()` | the value may be `null` |
71
+ | `.refine(fn, message)` | a custom check (not proved) |
72
+
73
+ See **[docs/schemas.md](https://github.com/nohzafk/bend-schema/blob/main/docs/schemas.md)**
74
+ for how to write schemas:
75
+ objects, unions, custom rules, error messages and encoding.
76
+
77
+ ## Examples
78
+
79
+ Each example is a small runnable project with tests:
80
+
81
+ - [`examples/api-server`](https://github.com/nohzafk/bend-schema/tree/main/examples/api-server): an HTTP endpoint that
82
+ validates the request body and returns the error path in a 400.
83
+ - [`examples/config-loader`](https://github.com/nohzafk/bend-schema/tree/main/examples/config-loader): reads a JSON config
84
+ file and reports the first mistake in one line.
85
+ - [`examples/event-log`](https://github.com/nohzafk/bend-schema/tree/main/examples/event-log): an append-only JSON-lines log
86
+ that writes and reads with the same schema.
87
+
88
+ ## Ask an AI agent to write a schema
89
+
90
+ New to bend-schema? Paste this into your coding agent to introduce it, then
91
+ work with the agent as usual:
92
+
93
+ ```text
94
+ bend-schema is a TypeScript schema library like zod, whose checker is
95
+ written in Bend and proved correct. Read its README to learn what it is
96
+ and how to install it: https://github.com/nohzafk/bend-schema
97
+
98
+ If we decide to use it, docs/schemas.md in that repo explains how to
99
+ write schemas.
100
+ ```
101
+
102
+ ## Use from Bend
103
+
104
+ Write a schema once in TypeScript, use it in a Bend function whose laws you
105
+ prove, and call that function from TypeScript again. The loop is:
106
+
107
+ ```
108
+ schema.ts ──gen──▶ schema.bend ──import──▶ core.bend ──bend-emit──▶ dist/core.mjs ──import──▶ app.ts
109
+ ```
110
+
111
+ **1. Export your schemas** from a TypeScript module:
112
+
113
+ ```ts
114
+ // schema.ts
115
+ import { s } from "bend-schema";
116
+
117
+ export const Config = s.object({
118
+ name: s.str().len(1, 64),
119
+ seats: s.nat().in(1, 500),
120
+ }).strict();
121
+
122
+ export const schemas = { config: Config };
123
+ ```
124
+
125
+ **2. Generate Bend source.** The command executes `schema.ts`, so that file
126
+ should contain only schemas. It runs under Bun — Node cannot strip the types
127
+ from TypeScript in `node_modules`.
128
+
129
+ ```sh
130
+ bunx bend-schema gen schema.ts schema.bend
131
+ ```
132
+
133
+ `schema.bend` defines `config_schema()` and imports the proved checker from
134
+ this package's `core/core.bend`.
135
+
136
+ **3. Use the schema in your own Bend core.** Import the checker with the same
137
+ path that `schema.bend` uses on its second line:
138
+
139
+ ```bend
140
+ # core.bend
141
+ import Base
142
+ import ./node_modules/bend-schema/core/core.bend as S
143
+ import ./schema.bend as Sch
144
+
145
+ def config_ok(r: S.Raw) -> Bool:
146
+ S.conforms0(Sch.config_schema(), r)
147
+ ```
148
+
149
+ Here you can state and prove laws about your own functions, and build on the
150
+ laws in `core/LAWS.bend`. Import `core/PROOF.bend` alongside them: its proofs
151
+ are what close them, and a file that imports `LAWS.bend` alone does not check.
152
+ The test suite proves it.
153
+
154
+ **4. Turn the core into a typed ES module** with
155
+ [bend-emit](https://github.com/nohzafk/bend-emit):
156
+
157
+ ```sh
158
+ bun add -d github:nohzafk/bend-emit
159
+ bunx bend-emit core.bend dist # writes dist/core.mjs and dist/core.d.mts
160
+ ```
161
+
162
+ **5. Call it from TypeScript.** `toRaw` converts a JSON value into the
163
+ core's `Raw` input:
164
+
165
+ ```ts
166
+ // app.ts
167
+ import { toRaw } from "bend-schema";
168
+ import { config_ok } from "./dist/core.mjs";
169
+
170
+ config_ok(toRaw({ name: "a", seats: 3 })); // true
171
+ config_ok(toRaw({ name: "a", seats: 999 })); // false
172
+ ```
173
+
174
+ Commit `dist/`: at run time your package needs neither Bend nor bend-emit.
175
+
176
+ Bend code can also plug in its own rules through a template parameter. The
177
+ laws hold for every rule, so you only need to prove what your rule means. See
178
+ `core/core.bend` for the full schema type and the rule interface.
179
+
180
+ ## What is proved
181
+
182
+ The laws in `core/LAWS.bend` hold for every schema, every rule and every value:
183
+
184
+ - **Exact:** `check` reports no error if and only if the value conforms.
185
+ - **Accurate:** the reported path leads to a real error of the reported kind.
186
+ - **Round trip:** decoding an encoded value gives back the same value.
187
+ - **Encoding conforms:** what the encoder writes passes the check whenever each
188
+ enum value is one of its names and each bound holds of the value.
189
+ - Each combinator (enum, tuple, variant, tagged union, strict, bounds) has a
190
+ law that states what it accepts.
191
+
192
+ Not proved: the TypeScript builder, the conversion between JS values and the
193
+ core, and `.refine()` predicates. These are covered by tests.
194
+
195
+ Two things the TypeScript layer does not carry, because they are not Bend values
196
+ and the core never sees them: a non-plain object (a `Date`, `Map` or class
197
+ instance) is refused as not being JSON, and an object key named `__proto__` is
198
+ the JavaScript prototype, not a field.
199
+
200
+ ## Limits
201
+
202
+ - **Size:** a value may contain at most 3072 array elements plus object keys
203
+ in total, counted across all nesting levels. A larger value is rejected with
204
+ a `TooLarge` error at the point where it goes over. This keeps the checker
205
+ within the stack.
206
+ - **Width:** an object may have at most 256 keys.
207
+
208
+ ## Development
209
+
210
+ Development uses [Bun](https://bun.sh), Bend, and
211
+ [bend-emit](https://github.com/nohzafk/bend-emit) (a dev dependency), which
212
+ builds `dist-core/`.
213
+
214
+ ```sh
215
+ sh test.sh # full gate: proofs, tests, types
216
+ # rebuild the compiled core
217
+ bunx bend-emit core/core.bend dist-core
218
+ ```
219
+
220
+ | Path | Contents |
221
+ |---|---|
222
+ | `core/` | the Bend checker, its laws and their proofs |
223
+ | `src/` | the TypeScript API, codec and Bend code generator |
224
+ | `dist-core/` | the compiled core (committed) |
225
+ | `examples/` | example projects |
226
+ | `tools/` | build and verification tools |
227
+
228
+ ## License
229
+
230
+ MIT
package/core/LAWS.bend ADDED
@@ -0,0 +1,379 @@
1
+ import Base
2
+ import ./core.bend as C
3
+
4
+ # The laws of bend-schema, for every schema, every value and every rule:
5
+ # proved once, and every schema built from these combinators has them.
6
+ #
7
+ # check_exact check finds nothing exactly when the value conforms: it
8
+ # neither lets a bad value through nor refuses a good one.
9
+ # At an SRule the value must conform to the rule's schema,
10
+ # and the rule must report nothing.
11
+ # check_accurate what check reports is there: following its path, every
12
+ # value passed on the way conforms, and at the end the value
13
+ # is wrong in the way it says. At an SRule, the schema's
14
+ # shape was not the defect and the rule itself reported
15
+ # this error at the path it names.
16
+ # enum_accepts every name in an SEnum's list is accepted.
17
+ # enum_admits a string SEnum accepts is one of its names, and here is
18
+ # where. Together they pin in_names, which check and
19
+ # conforms share.
20
+ # variant_meaning for a chain of SVariant keys ending in SVEnd, a value
21
+ # conforms exactly when it is an object, exactly one of the
22
+ # keys is in it, and the value under that key conforms: a
23
+ # second reading by counting, which pins none_present.
24
+ # too_large_reported
25
+ # a node the codec refused to build is reported where it
26
+ # stands, with TooLarge: the one thing the core can be told
27
+ # about a size.
28
+ # sbool_meaning SBool accepts a boolean and nothing else.
29
+ # sstr_len_meaning
30
+ # SStrLen{lo, hi, s} accepts a string whose length is in the
31
+ # bounds and that also satisfies s -- str_len_in_meaning,
32
+ # restated for the constructor.
33
+ # snat_in_meaning SNatIn{lo, hi} accepts a number in the bounds --
34
+ # nat_in_meaning, restated for the constructor.
35
+ # A lo past hi is an empty range, not an error: nothing is in
36
+ # bounds, and each of these laws holds for every lo and hi.
37
+ # slist_len_meaning
38
+ # SListLen{lo, hi, s} accepts a list whose element count is
39
+ # in the bounds and that also satisfies s -- str_len_in_meaning
40
+ # over elements. A value that is not a list is not in bounds
41
+ # here either (the count is a list's), and it is the inner
42
+ # schema's to refuse, exactly as check reports it.
43
+ # soptional_meaning
44
+ # SOptional{inner} accepts an absent value (RMissing), and
45
+ # every other value exactly as the inner schema accepts it:
46
+ # the constructor adds absence and nothing else.
47
+ #
48
+ # bounds_meaning died with SBounds: the bounded list is billing's rule now,
49
+ # and its meaning is billing's law.
50
+ #
51
+ # Decided by the human with these laws (acl, 2026-09-24): an SVariant object
52
+ # with none of its keys, or with two of them, is an error, and a key's value
53
+ # is checked by its own schema (so {any: false} is refused by STrue).
54
+ #
55
+ # Decided by the human with these laws: a key the schema names must be
56
+ # present, and an optional value is written null; keys the schema does not
57
+ # name are ignored; the first error is depth first, by position. A rule runs
58
+ # only after the shape it refines holds, and its error's path is relative to
59
+ # the value it was given. (With a key repeated in an object, lookup reads the
60
+ # first: a JSON object has no repeated keys, and a value that repeats one is
61
+ # refused before any lookup is trusted -- repeated_key_refused.)
62
+
63
+ law check_exact:
64
+ for ~rule: Nat -> C.Raw -> Maybe<&2, C.Err>
65
+ for +s: C.Schema
66
+ for +r: C.Raw
67
+ for +prev: Maybe<&2, Nat>
68
+ {Maybe.is_none(&2, C.Err, C.check(~rule, s, r, prev)) == C.conforms(~rule, s, r, prev) : Bool}
69
+
70
+ law check_accurate:
71
+ for ~rule: Nat -> C.Raw -> Maybe<&2, C.Err>
72
+ for +s: C.Schema
73
+ for +r: C.Raw
74
+ for +prev: Maybe<&2, Nat>
75
+ for +path: List<&2, C.Step>
76
+ for +why: C.Why
77
+ for h: {C.check(~rule, s, r, prev) == Some{C.Err{path, why}} : Maybe<&2, C.Err>}
78
+ {C.defect(~rule, s, r, prev, path) == Some{why} : Maybe<&2, C.Why>}
79
+
80
+ law enum_accepts:
81
+ for ~rule: Nat -> C.Raw -> Maybe<&2, C.Err>
82
+ for +pre: List<&2, String>
83
+ for +n: String
84
+ for +post: List<&2, String>
85
+ {C.conforms(~rule, C.SEnum{List.append(&2, String, pre, n <> post)}, C.RStr{n}, None{}) == True{} : Bool}
86
+
87
+ def OneOf(ns: List<&2, String>, x: String) -> Type:
88
+ &pre: List<&2, String> -> &n: String -> &post: List<&2, String> ->
89
+ {ns == List.append(&2, String, pre, n <> post) : List<&2, String>} & {String.eq(x, n) == True{} : Bool}
90
+
91
+ law enum_admits:
92
+ for ~rule: Nat -> C.Raw -> Maybe<&2, C.Err>
93
+ for +ns: List<&2, String>
94
+ for +x: String
95
+ for h: {C.conforms(~rule, C.SEnum{ns}, C.RStr{x}, None{}) == True{} : Bool}
96
+ OneOf(ns, x)
97
+
98
+ law variant_meaning:
99
+ for ~rule: Nat -> C.Raw -> Maybe<&2, C.Err>
100
+ for +s: C.Schema
101
+ for +r: C.Raw
102
+ for hc: {C.is_chain(s) == True{} : Bool}
103
+ {C.conforms(~rule, s, r, None{}) == Bool.and(C.is_object(r), Bool.and(Nat.is_eq(C.count_present(s, r), 1n), C.present_conform(~rule, s, r))) : Bool}
104
+
105
+ # ---- a key the schema reads, taken twice ----
106
+ #
107
+ # A key the schema READS must appear at most once in the value: with the key
108
+ # repeated, lookup reads the first one and the second is unreachable, so the
109
+ # value says something the schema cannot mean, and it is refused. Three keys
110
+ # are read that way -- a field key (SField), a variant key (SVariant), and the
111
+ # tag key of a tagged chain (STagged, which is how the chain picks its case).
112
+ # All three are stated, at explicit constructors and nothing else: the value
113
+ # does not conform, and check reports RepeatedKey at that key's own name with
114
+ # no step after it. Nothing else in the gate can catch a key_once that is
115
+ # wrong: check_exact is a consistency law, which a key_once wrong in the same
116
+ # direction on both sides satisfies, and the mutant has key_err stop
117
+ # reporting, which leaves key_once itself untested.
118
+ #
119
+ # The tag key's test sits at the case the tag names, not at the chain, and the
120
+ # two halves of that are the same two as the other keys'. A value whose tag
121
+ # names no case is refused at the chain's end whatever its keys are, so there
122
+ # the key's own test would decide nothing; and a case is the only place it can
123
+ # sit, because the case is checked against the object with the tag key taken
124
+ # out (drop_key), which is the object a case's own schema is about.
125
+ #
126
+ # The other half of the same rule is wf's: a case's own schema may not read
127
+ # the tag key at all (core.bend, no_key). If it did, enc would write that key
128
+ # twice -- the tag, and then the case's own field -- and the host builds the
129
+ # object as a JS object, where the second write overwrites the first, so the
130
+ # value written could not be read back. wf refusing the schema is what keeps
131
+ # encode_conforms true.
132
+ def Refused(s: C.Schema, r: C.Raw, +p: List<&2, C.Step>, +w: C.Why) -> Type:
133
+ {C.conforms(~C.no_rule, s, r, None{}) == False{} : Bool} & {C.check(~C.no_rule, s, r, None{}) == Some{C.Err{p, w}} : Maybe<&2, C.Err>}
134
+
135
+ law repeated_key_refused:
136
+ Refused(C.SField{"a", C.SNat{}, C.SEnd{}}, C.RKey{"a", C.RNum{1n}, C.RKey{"a", C.RNum{2n}, C.REnd{}}}, C.AtField{0n, "a"} <> Nil{}, C.RepeatedKey{"a"}) & Refused(C.SVariant{"a", C.SNat{}, C.SVEnd{}}, C.RKey{"a", C.RNum{1n}, C.RKey{"a", C.RNum{2n}, C.REnd{}}}, C.AtField{0n, "a"} <> Nil{}, C.RepeatedKey{"a"})
137
+
138
+ # The tag key of a tagged chain, taken twice: the value's tag names the case,
139
+ # and the key it was read from is there twice, so the value is refused at that
140
+ # key's own name. The case's schema is SEnd{}, which reads no key and accepts
141
+ # any object, so what is decided here is the tag key's own test and nothing
142
+ # else -- the same test the two keys above are refused by.
143
+ law repeated_tag_refused:
144
+ Refused(C.STagged{"t", "a", C.SEnd{}, C.STagEnd{"t"}}, C.RKey{"t", C.RStr{"a"}, C.RKey{"t", C.RStr{"a"}, C.REnd{}}}, C.AtField{0n, "t"} <> Nil{}, C.RepeatedKey{"t"})
145
+
146
+ # ---- a key the schema does not name ----
147
+ #
148
+ # A key the schema does not name is ignored, so adding one to an object that
149
+ # already conforms leaves the answer alone -- before the keys the schema reads,
150
+ # or after them. Stated at explicit constructors, like repeated_key_refused,
151
+ # and that is the point of it (D3): the general form is FALSE. key_names names
152
+ # the keys of a chain and nothing else -- the SField and the SVariant it walks,
153
+ # not the wrappers a chain may sit in -- so its list says nothing about a
154
+ # wrapper, and a key outside that list can still be one the schema needs. The
155
+ # counterexample, which is why this law is at literals: with
156
+ # s = SOpt{SField{"a", SNat{}, SEnd{}}}, k = "a" and r = REnd{}, the premise
157
+ # Bool.not(in_names(k, key_names(s))) holds, because key_names(SOpt{i}) is Nil{}
158
+ # and Nil{} names no key at all, yet conforms(SOpt{i}, r) is False while
159
+ # conforms(SOpt{i}, RKey{"a", RNum{1}, REnd{}}) is True: the key is not extra
160
+ # at all, it is the one the wrapper's own schema asks for. A variant chain
161
+ # stands beside the record in the law because at literals it costs the same
162
+ # computation: none_present over a concrete chain reduces, and no skip lemma is
163
+ # wanted for it.
164
+ #
165
+ # What is deliberately not here. The reordering half of "non-canonical still
166
+ # decodes" is unproved: no def in the core reorders an object's keys -- lookup,
167
+ # drop_key, has_key and no_extra read them, and none of them moves one -- so a
168
+ # permuted value has nothing here to be an instance of. And a key the schema
169
+ # does not name that appears TWICE is left alone as well (core.bend, "a key the
170
+ # schema does not name"): a key nothing reads is unobservable however often it
171
+ # is written, so it is not refused, unlike the repeated key of
172
+ # repeated_key_refused. Neither is stated as a law: the first has no code to be
173
+ # false about, the second is unobservable by construction.
174
+
175
+ law unnamed_key_ignored:
176
+ {C.conforms(~C.no_rule, C.SField{"a", C.SNat{}, C.SEnd{}}, C.RKey{"z", C.RNum{9n}, C.RKey{"a", C.RNum{1n}, C.REnd{}}}, None{}) == C.conforms(~C.no_rule, C.SField{"a", C.SNat{}, C.SEnd{}}, C.RKey{"a", C.RNum{1n}, C.REnd{}}, None{}) : Bool} &
177
+ {C.conforms(~C.no_rule, C.SField{"a", C.SNat{}, C.SEnd{}}, C.RKey{"a", C.RNum{1n}, C.RKey{"z", C.RNum{9n}, C.REnd{}}}, None{}) == C.conforms(~C.no_rule, C.SField{"a", C.SNat{}, C.SEnd{}}, C.RKey{"a", C.RNum{1n}, C.REnd{}}, None{}) : Bool} &
178
+ {C.conforms(~C.no_rule, C.SVariant{"a", C.SNat{}, C.SVEnd{}}, C.RKey{"z", C.RNum{9n}, C.RKey{"a", C.RNum{1n}, C.REnd{}}}, None{}) == C.conforms(~C.no_rule, C.SVariant{"a", C.SNat{}, C.SVEnd{}}, C.RKey{"a", C.RNum{1n}, C.REnd{}}, None{}) : Bool}
179
+
180
+ # A tuple conforms exactly when the value is a list, as long as the tuple,
181
+ # and each position conforms. Without it, a bug shared by check and conforms
182
+ # (both skipping a position) passes the two laws above.
183
+ law tuple_meaning:
184
+ for ~rule: Nat -> C.Raw -> Maybe<&2, C.Err>
185
+ for +ss: List<&2, C.Schema>
186
+ for +r: C.Raw
187
+ {C.conforms(~rule, C.tuple_of(ss), r, None{}) == Bool.and(C.raw_list(r), Bool.and(Nat.is_eq(C.raw_len(r), List.length(&2, C.Schema, ss)), C.each_pos(~rule, ss, r, 0n))) : Bool}
188
+
189
+ # ---- a strict object ----
190
+ #
191
+ # SStrict{s} is s, and no key but the ones s names. The right side counts
192
+ # the unknown keys (count_unknown), a second walk that shares only in_names
193
+ # with the core, and in_names is pinned by the enum laws.
194
+
195
+ law strict_meaning:
196
+ for ~rule: Nat -> C.Raw -> Maybe<&2, C.Err>
197
+ for +s: C.Schema
198
+ for +r: C.Raw
199
+ {C.conforms(~rule, C.SStrict{s}, r, None{}) == Bool.and(C.conforms(~rule, s, r, None{}), Nat.is_eq(C.count_unknown(C.key_names(s), r), 0n)) : Bool}
200
+
201
+ # ---- a tagged union ----
202
+ #
203
+ # tagged_meaning: at a case, a value whose tag is the case's name conforms
204
+ # exactly when the tag key is read once AND the object without the tag
205
+ # conforms to the case; any other value is the rest's to decide -- and there
206
+ # the value goes to the rest whatever its keys, so a repeated tag key is the
207
+ # case's business and not the chain's. At the chain's end nothing conforms.
208
+ # drop_meaning pins drop_key, which conforms, check and dec share: what it
209
+ # leaves under k is what was under k after the first one (so a single tag
210
+ # key is gone), and every other key reads as before.
211
+
212
+ law tagged_meaning:
213
+ for ~rule: Nat -> C.Raw -> Maybe<&2, C.Err>
214
+ for +k: String
215
+ for +n: String
216
+ for +cs: C.Schema
217
+ for +rest: C.Schema
218
+ for +r: C.Raw
219
+ {C.conforms(~rule, C.STagged{k, n, cs, rest}, r, None{}) == C.pick_bool(C.is_tag(k, n, r), Bool.and(C.key_once(k, r), C.conforms(~rule, cs, C.drop_key(k, r), None{})), C.conforms(~rule, rest, r, None{})) : Bool}
220
+
221
+ law tag_end_refuses:
222
+ for ~rule: Nat -> C.Raw -> Maybe<&2, C.Err>
223
+ for +k: String
224
+ for +r: C.Raw
225
+ {C.conforms(~rule, C.STagEnd{k}, r, None{}) == False{} : Bool}
226
+
227
+ law drop_first:
228
+ for +k: String
229
+ for +v: C.Raw
230
+ for +o: C.Raw
231
+ {C.drop_key(k, C.RKey{k, v, o}) == o : C.Raw}
232
+
233
+ law drop_other:
234
+ for +k: String
235
+ for +j: String
236
+ for +r: C.Raw
237
+ for h: {False{} == String.eq(k, j) : Bool}
238
+ {C.lookup(j, C.drop_key(k, r)) == C.lookup(j, r) : C.Raw}
239
+
240
+ # ---- ready-made rules ----
241
+ #
242
+ # Each pins its rule to what it means: on the value it is about, it reports
243
+ # nothing exactly when the value is in the bounds (both ends included).
244
+
245
+ law str_len_in_meaning:
246
+ for +lo: Nat
247
+ for +hi: Nat
248
+ for +x: String
249
+ {Maybe.is_none(&2, C.Err, C.str_len_in(lo, hi, C.RStr{x})) == Bool.and(Nat.is_le(lo, String.length(x)), Nat.is_le(String.length(x), hi)) : Bool}
250
+
251
+ law nat_in_meaning:
252
+ for +lo: Nat
253
+ for +hi: Nat
254
+ for +n: Nat
255
+ {Maybe.is_none(&2, C.Err, C.nat_in(lo, hi, C.RNum{n})) == Bool.and(Nat.is_le(lo, n), Nat.is_le(n, hi)) : Bool}
256
+
257
+ # ---- a bound is a constructor ----
258
+ #
259
+ # Approved with these constructors (D4): a bound belongs to the schema, not to
260
+ # a rule's tag, so a host can build "a number from 1 to 6" or "a string of 1 to
261
+ # 64 characters" without a Bend program. Each one keeps the meaning of the rule
262
+ # it comes from -- str_len_in_meaning and nat_in_meaning restated -- and adds a
263
+ # shape of its own. lo > hi is an empty range: nothing is in bounds, and the
264
+ # laws still hold for every lo and hi (nothing in the core refuses the schema
265
+ # itself, it just accepts no value).
266
+ #
267
+ # Every other law covers them too: check_exact, check_accurate,
268
+ # too_large_reported, decode_encode, checked_decodes and encode_conforms are
269
+ # stated for every schema, so the new constructors are cases of them (see
270
+ # encode_conforms: a value outside a bound is written out unchanged, which is
271
+ # why it is a premise of that law rather than something the encoder can know).
272
+
273
+ # SBool accepts a boolean and nothing else: any b, and no other value.
274
+ law sbool_meaning:
275
+ for ~rule: Nat -> C.Raw -> Maybe<&2, C.Err>
276
+ for +r: C.Raw
277
+ {C.conforms(~rule, C.SBool{}, r, None{}) == C.is_bool(r) : Bool}
278
+
279
+ # SStrLen{lo, hi, s} accepts a string whose length is in the bounds, and that
280
+ # also satisfies s: the length part is str_len_in_meaning's, restated for the
281
+ # constructor (both ends included).
282
+ law sstr_len_meaning:
283
+ for ~rule: Nat -> C.Raw -> Maybe<&2, C.Err>
284
+ for +lo: Nat
285
+ for +hi: Nat
286
+ for +s: C.Schema
287
+ for +x: String
288
+ {C.conforms(~rule, C.SStrLen{lo, hi, s}, C.RStr{x}, None{}) == Bool.and(C.conforms(~rule, s, C.RStr{x}, None{}), Bool.and(Nat.is_le(lo, String.length(x)), Nat.is_le(String.length(x), hi))) : Bool}
289
+
290
+ # SNatIn{lo, hi} accepts a number in the bounds, both ends included:
291
+ # nat_in_meaning's, restated for the constructor.
292
+ law snat_in_meaning:
293
+ for ~rule: Nat -> C.Raw -> Maybe<&2, C.Err>
294
+ for +lo: Nat
295
+ for +hi: Nat
296
+ for +n: Nat
297
+ {C.conforms(~rule, C.SNatIn{lo, hi}, C.RNum{n}, None{}) == Bool.and(Nat.is_le(lo, n), Nat.is_le(n, hi)) : Bool}
298
+
299
+ # SListLen{lo, hi, s} accepts a list whose element count is in the bounds, and
300
+ # that also satisfies s. The count is spelled out here rather than left to the
301
+ # helper the core reads, so that a bound off by one is the law's business too:
302
+ # a list of exactly lo or exactly hi elements is in, and one of any other count
303
+ # is not, for every lo and hi (lo > hi accepts no list at all). A value that is
304
+ # not a list is not in bounds, and the inner schema decides it.
305
+ law slist_len_meaning:
306
+ for ~rule: Nat -> C.Raw -> Maybe<&2, C.Err>
307
+ for +lo: Nat
308
+ for +hi: Nat
309
+ for +s: C.Schema
310
+ for +r: C.Raw
311
+ {C.conforms(~rule, C.SListLen{lo, hi, s}, r, None{}) == Bool.and(Bool.and(C.raw_list(r), Bool.and(Nat.is_le(lo, C.raw_len(r)), Nat.is_le(C.raw_len(r), hi))), C.conforms(~rule, s, r, None{})) : Bool}
312
+
313
+ # SOptional{inner} accepts an absent value, and every other value exactly as
314
+ # the inner schema accepts it: the constructor adds absence and nothing else.
315
+ # enc writes None as RMissing (no key at all, in the host's JSON) and dec reads
316
+ # RMissing back as None; null is not absent, so it is refused unless the inner
317
+ # is nullable itself.
318
+ law soptional_meaning:
319
+ for ~rule: Nat -> C.Raw -> Maybe<&2, C.Err>
320
+ for +i: C.Schema
321
+ for +r: C.Raw
322
+ {C.conforms(~rule, C.SOptional{i}, r, None{}) == Bool.or(C.is_missing(r), C.conforms(~rule, i, r, None{})) : Bool}
323
+
324
+ # ---- a value too big to walk ----
325
+
326
+ # The codec refuses to build a value past its budget and puts one RTooBig in
327
+ # place of the list or object that ran past it (codec.ts); nothing else in the
328
+ # core knows a size. So the claim about that node is local: it conforms to
329
+ # nothing, and the walk that reaches it reports it right there (the empty
330
+ # path, for the value it was given) with TooLarge -- never the schema kind's
331
+ # own reason, which would say the host sent the wrong shape when what it sent
332
+ # was too much of the right one. A node further down is reported one step
333
+ # along by check's own step machinery, which check_accurate covers, and the
334
+ # codec's budget (that a value inside it converts unchanged) is a run-time
335
+ # test, since the codec is TypeScript.
336
+ law too_large_reported:
337
+ for ~rule: Nat -> C.Raw -> Maybe<&2, C.Err>
338
+ for +s: C.Schema
339
+ for +prev: Maybe<&2, Nat>
340
+ {C.check(~rule, s, C.RTooBig{}, prev) == Some{C.Err{Nil{}, C.TooLarge{}}} : Maybe<&2, C.Err>}
341
+
342
+ # ---- a schema's meaning: the decoder, derived from the schema ----
343
+ #
344
+ # Meaning, enc and dec are written once for every schema (core.bend), so a
345
+ # project stops writing its reader and its round trip. The three laws below
346
+ # were approved with the design: wf is a premise (a schema is the
347
+ # programmer's, not the wire's), and an enum means its string.
348
+
349
+ # What was written reads back as itself.
350
+ law decode_encode:
351
+ for +s: C.Schema
352
+ for +x: C.Meaning(s)
353
+ for hw: {C.wf(s) == True{} : Bool}
354
+ {C.dec(s, C.enc(s, x)) == Some{x} : Maybe<&2, C.Meaning(s)>}
355
+
356
+
357
+ # A value check accepts can be read: a project's reader needs no
358
+ # "cannot happen" default.
359
+ law checked_decodes:
360
+ for ~rule: Nat -> C.Raw -> Maybe<&2, C.Err>
361
+ for +s: C.Schema
362
+ for +r: C.Raw
363
+ for hc: {C.conforms(~rule, s, r, None{}) == True{} : Bool}
364
+ {Maybe.is_some(&2, C.Meaning(s), C.dec(s, r)) == True{} : Bool}
365
+
366
+
367
+ # What is written is accepted by the schema (with no rule: a rule is the
368
+ # project's, and not the encoder's to satisfy), as long as each enum value is
369
+ # one of its names and each bound a constructor states holds of the value. A
370
+ # bound is a subset of the encoding's own shape, so the encoder -- which
371
+ # writes the meaning unchanged -- can write a value outside one; the host that
372
+ # built that value is the one at fault, and check refuses it on the way back.
373
+ law encode_conforms:
374
+ for +s: C.Schema
375
+ for +x: C.Meaning(s)
376
+ for hw: {C.wf(s) == True{} : Bool}
377
+ for hn: {C.names_ok(s, x) == True{} : Bool}
378
+ for hb: {C.bounds_ok(s, x) == True{} : Bool}
379
+ {C.conforms(~C.no_rule, s, C.enc(s, x), None{}) == True{} : Bool}
package/core/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ SPDX-License-Identifier: MIT
2
+
3
+ MIT License
4
+
5
+ Copyright (c) 2026 nohzafk
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.