v0.0.174
Verification of refinement-type predicates — static and runtime (#746)
A user refinement type { @T | P } (e.g. type PosInt = { @Int | @Int.0 > 0 }) previously parsed but its predicate was never checked — a violating value verified silently (a Tier-0 silent failure). This release makes refinement predicates real, both at verify time and at run time.
Static verification (the verifier)
The predicate is discharged as a Tier-1 obligation at every site where a value narrows into a refined slot, and at a refined return position:
letbindings, call arguments (incl. piped and generic-instantiated formals), constructor fields, effect-operation arguments, match bindings, ADT sub-pattern binds- tuple construction components (recovered from the construction site's expected type —
Tupleis not a user-registered constructor, so it is obligated separately from ADT fields) - literal / projected tuple-destructure components
A refined parameter is dually assumed to satisfy its predicate in the body (sound precisely because every call site discharges the obligation). A provably-violating narrowing is an E505 error with a counterexample; a refinement over a non-primitive base ({ @Array<Int> | array_length(...) > 0 }) or an undecidable predicate is an E506 Tier-3 warning — never a silent pass. This generalises the @Nat >= 0 machinery to an arbitrary translated predicate (the forward-promise in the 0.0.172 notes).
Runtime guard (#762)
Codegen compiles the predicate to WebAssembly and checks it at every function boundary — a refined parameter at entry, a refined return at exit (via $vera.contract_fail) — so even a program compiled without vera verify traps on a violating value, including at a public/FFI entry point an untrusted caller can't bypass (call arguments are covered transitively).
- Collection bases (
{ @Array<Int> | array_length(...) > 0 }) are Tier-3 statically but lowered directly by codegen, so an empty array into a@NonEmptyArrayparameter traps. - A tuple with refined /
@Natcomponents (Tuple<PosInt, Int>) carries no top-level refinement, so codegen decomposes it at the boundary — loading and guarding each component (recursively, for nested tuples) at both entry and exit. An external caller passingTuple(-5, 3)traps on the violating component. - A predicate codegen can't lower to a guard (e.g. one calling a generic function with no monomorphised instance at the guard site) now reports a clean
E617diagnostic rather than crashing the compiler.
Review hardening
A long tail of soundness and completeness fixes from the PR review. In every case a genuine narrowing stays obligated — source-type facts never launder an unproven value into a false Tier-1:
- Generic tuple aliases (
type Box<T> = Tuple<T, Int>) substitute their type argument when resolving component types, soBox<PosInt>still guards its refined component. - A mutually-recursive (infinite) tuple alias fails closed with
E617rather than silently emitting partial guards. - Nested
RefinedTypecomponents unwrap to their base Z3 sort, soTuple<PosInt, Int>gets a proper datatype sort rather than degrading to a scalar model. - Refinement over a tuple (
type Pair = { @Tuple<PosInt, Int> | true }) unwraps the refined base so components are obligated statically and guarded at the boundary (recursively throughTuple<Pair, Int>), with component guards emitted before the enclosing top-level predicate. - Refined-ADT sub-pattern binds (
Some(@PosInt)onOption<PosInt>) carry the field's source-type predicate into the arm body — both to the narrowing walk (fixing a falseE503) and, via an SMT match-translation hook, to the arm body's call preconditions (fixing a falseE501). - A refined value returned from a match arm (
Some(@PosInt) -> @PosInt.0) discharges its refined-return goal via a globalarm-matched ⇒ source-factSMT implication — on the generic fast path too. - A callee's alias-base refined return (
{ @Age | @Age.0 >= 18 },type Age = Nat) is assumed by the caller through the predicate's binder name (@Age.0), not the resolvedNat. - Nested constructor sub-pattern narrowings (
Some(Some(@PosInt))onOption<Option<Int>>) are recursed and obligated statically — previously an unguarded false Tier-1 — with the nested-bind runtime guard tracked as #765. - Refined ADT scrutinees (
matchon{ @Option<Int> | P }) and refined tuple sources (alet-destructure of{ @Tuple<PosInt, Int> | P }) unwrap the refined base before reading the constructor's / tuple's type args.
Internal: hardened the @Nat narrowing guard data structures (#759)
Single-sourced the checker→verifier side-table span key through ast.span_key (previously hand-rolled at three sites), and added a ConstructorLayout.__post_init__ assertion that nat_fields stays length-aligned with field_offsets, so a drifted built-in literal fails loudly at construction rather than as a silently mis-indexed guard. Also pinned the #757 generic-instantiated-constructor-field runtime deferral with a codegen test (#760). No behaviour change — internal robustness follow-ups from the #756 review.