Repository navigation
Informative errors. They're important. - #37
Merged
Merged
Conversation
Every keyword spelled its error message twice: an f-string in `evaluate` and `str()`-joined parts in `lower`. That was workable while messages held only schema constants, but messages are about to carry instance values, where JSON and Python rendering differ. `core/messages.py` adds `realize`, which interprets the IR subset a message may use against a concrete instance, so a keyword can describe an error once and hand the same description to both tiers. It also adds the formatting helpers messages will call, wired into the closed `HelperName` set and the compiled runtime: - `preview`: compact JSON, cut at 64 characters with an ellipsis. Lazy, so a huge instance costs no more than a short one; errors are often recorded only to be dropped. - `apparent_type`, `index_ranges`, `name_list`, `duplicate_groups` and `ranges`. A property test checks over generated instances that `realize` and the compiled evaluator produce identical results. No keyword uses any of this yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The numeric bounds, `multipleOf`, the length and count bounds, `enum` and `const` named their limit but never the value that broke it. Each now shows both: - `must be >= 5, got 3` - `must be at least 5 characters, got "abc" (3)` - `must have at least 3 items, got 1` - `must be one of [1, 2, 3], got 4` Values render as compact JSON, cut at 64 characters. Params gain the same facts in full: `value`, plus `length` or `count`. `assertion()` now takes one builder, `describe(value, instance)`, which `lower` emits and `evaluate` realizes (P18), so the two tiers share a single source for each message. The IR-shape tests compare conditions and treat the `Fail` as opaque; each keyword's message test realizes the lowered `Fail` and compares text and params with `evaluate`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `type`: `expected string, got 3 (integer)`. A scalar shows its value and
apparent type, with `integer` for a mathematical integer; a container
shows only its type. `params.actual` is now the apparent type, and
`value` is added.
- `pattern`: `must match pattern "^a", got "b"`. Asserting `format`:
`must match format "ipv4", got "999.1.1.1"`. Both add `value`.
- `required`: one error naming every missing property,
`missing required properties "a", "c"`, with params `{"missing": [...]}`
replacing one error per name.
- `dependentRequired` and draft-07 `dependencies` arrays: one error per
keyword, `"a" requires "b", "c"; "d" requires "e"`, with params
`{"missing": {"a": ["b", "c"], "d": ["e"]}}`. For `dependencies` it comes
after any schema members' errors in both tiers.
New message helpers (`typed_preview`, `labeled_names`, `missing_names`,
`missing_dependencies`, `dependency_list`) keep each message to one call
in emitted code. The flag tier's `required` check becomes one
short-circuit `or`, equivalent to the separate guards it replaces.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `uniqueItems`: `items are not unique: 0 = 2 = 5; 1 = 3`, every group of equal items rather than the first pair. Params `duplicates` becomes the list of groups. Indexes only, never the items. - `contains`: `the contains subschema matched 2 items (0, 3), expected at most 1`, adding the matching indexes as params `matched`. The compiled evaluator now always collects them; the flag tier still does not. - `oneOf`: `matched 2 branches (0, 1), expected exactly 1 of 3`, or `matched none, ...`. An empty `oneOf` lowers to a constant failure, fixing an evaluator path that referenced bindings no apply had set. - `anyOf`: `does not match any of the 3 anyOf branches`. Naming the failing branches would add nothing, since all of them failed and each says why. `index_ranges` keeps a pair as `0, 1`, collapsing only runs of three or more. Message helpers are now wired through one table (`emit.MESSAGE_HELPERS`) instead of a hand-written case each. A new compiled-tier test checks every runtime-data message shape against the interpreter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A `false` subschema fails everything and explains nothing. `additionalProperties: false` reported `schema is false` once per extra property, at each property, while the useful fact -- which properties were extra -- was the applicator's and never got said. An applicator now never applies a `false` subschema of its own. It names the keys it would have applied it to, in one error at its own location: - `additional properties "b", "c" not allowed` - `items not allowed from index 1: 1-4` - `unevaluated items not allowed, first at index 2: 2, 3, 6` - `allOf branch 1 is false` This covers `additionalProperties`, `unevaluatedProperties`, `properties`, `patternProperties`, `propertyNames`, `dependentSchemas`, `prefixItems`, both forms of `items`, `additionalItems`, `unevaluatedItems`, `allOf`, `contains` and draft-07 `dependencies`. The IR gains `Reject`/`RejectCheck` and `Collect(errors=True)`. The evaluator collects the rejected keys and reports once; a verdict-only artifact treats `Reject` as a `Fail`, so `compile_validator` still stops at the first offending key and its flag goldens are unchanged. A `$ref` to `false` keeps `schema is false`. `then`/`else` are left for now: they lower as `if`'s siblings and would need a sibling error site. Also amends D13 with the message rulings, adds a per-keyword params table to the reference, and a "Reading error messages" guide section. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Building and realizing a message description for every interpreter error cost up to a fifth of the interpreter's time on error-heavy corpora: most records are never rendered -- a verdict-only evaluation renders none, and an error under a losing `anyOf` branch or an `if` condition is dropped. `KeywordContext.report(describe)` defers the whole description. The `ErrorRecord` holds a builder and realizes it on the first read of `message` or `params`, so readers see no difference. The built-in keywords report this way; `error(message, params)` is unchanged for custom keywords. Against `main` the interpreter is back within noise (`profile` identical; `event`, `user`, `api-payload` within a few percent). The compiled tiers were already unaffected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…number - `uniqueItems`: `items are not unique: [0, 2, 5] are equal; [1, 3] are equal`, rather than `0 = 2 = 5; 1 = 3`, which read like assignment. - `contains`: when `minContains` and `maxContains` are equal, the message says `expected 1` rather than `expected 1-1`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`KeywordContext.report` wrapped each keyword's description in a second closure plus two cells, all long-lived and tracked, so error-heavy evaluations paid a garbage-collection tax even in flag mode (0.9s to 1.5s for 160k errors), and the 3.13 CI leg overran the verbose-level budget. The record now keeps the description itself and realizes it against its own cursor's value. `realize` dispatches the common nodes by exact type ahead of the structural match, and `preview` goes straight to text for a scalar instead of through the bounded generator, which a container still uses. A `Binding` in an evaluate-side description raises `LookupError` naming the `Const` rule instead of a bare `KeyError`. The unreachable second ellipsis chunk is gone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`dependents_rejected` joins the other `false`-subschema builders in `_rejects`, so `legacy` no longer reaches into `applicator` for it, and `positions_sweep` hands back a `step` callable like `tail_sweep` does instead of a sentinel binding. Integer-aware type matching has one home, `json_model.type_matches`, shared by the `type` keyword and the message realizer. The helper table is typed loosely once, with one cast at its only call site, instead of nine suppressions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`EVALUATOR_NAMES` takes the message-helper identifiers from `MESSAGE_HELPERS` instead of repeating them, and two tests pin the three tables (`HelperName`, `messages.HELPERS`, `MESSAGE_HELPERS`) to each other, so a helper added to one cannot go missing from another until a compile hits the emitter's fallback. `KeywordContext.report` now states its contract: an evaluate-side description carries runtime values as `Const` nodes, since the record realizes it with no bindings, and a `Binding` there raises `LookupError` when rendered. DESIGN P18 points at `HelperName` for the helper set rather than naming six of thirteen. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… 12s A keyword whose description depends on nothing but its own value (the bounds, `type`, `pattern`, `format`, `const`, lengths, counts) rebuilt the identical IR for every rejection, and an array of 160k wrong items rejects 160k times: the short-lived objects kept triggering full collections over the records already alive. `describe_once` hands back one prebuilt thunk per distinct value, keyed with the value's type so `1`, `true` and `1.0` stay apart; an unhashable value (an `enum` list) is built per report as before. Verbose rendering of that case drops from 2.45s to 2.1s locally, and flag mode no longer allocates a closure per error at all. The budget on that test goes from 10s to 12s: each of its 160k messages now names the item and its type by design, and the slowest CI runner seen is four times slower than a laptop. The changelog gains an Added section for what this branch exposes to keyword authors: `KeywordContext.report`, the `Reject`/`RejectCheck` lowering statements with `Collect(errors=True)`, and the thirteen message-formatting `HelperName` members. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What it says on the tin. Hopefully these errors are sufficiently informative, but as I'm using this package in another project I'm sure I'll notice soon enough.