Skip to main content

Trusted Base Awareness

When extending Sector9, understanding what the verifier trusts without proof is essential for maintaining soundness.

Why This Matters for Contributors​

Every verification system has a trusted base - axioms and primitives assumed correct without proof. When you add features to Sector9, you must understand:

  1. What existing axioms your feature depends on
  2. Whether your feature introduces new trusted components
  3. How to minimize soundness risks

A bug in the trusted base can make the entire verification system unsound, proving false properties about user code.

The Trusted Base Architecture​

The trusted base has three layers, each with different stability guarantees:

┌─────────────────────────────────────────────────┐
│ Layer 3: User Assumption Boundaries │
│ (trusted functions, abstract proof interfaces) │
├─────────────────────────────────────────────────┤
│ Layer 2: Primitive Collection Operations │
│ (Seq, Set, Map, Multiset from prelude) │
├─────────────────────────────────────────────────┤
│ Layer 1: Viper Prelude Axioms │
│ (arrays, snapshots, options, arithmetic) │
└─────────────────────────────────────────────────┘

Layer 1: Prelude Axioms​

The core axioms are generated in src/viper/prelude.ml. Key axiom categories:

CategoryPurposeExample Axiom
Array InjectivityUnique heap locations$loc_inv1($loc(a, i)) == a
Snapshot Domainsold() expression support$snap_cons_int / $snap_uncons_int
Option TypesExhaustiveness guaranteesisNone(o) || isSome(o)
Place/AddressOwnership tracking$place_index, $addr_ref axioms
TuplesProjection correctnesstup$N$i(Tup$N(...)) axioms

When adding a new type encoding, you may need corresponding axioms. Follow these principles:

  • Axioms must be mathematically sound
  • Use triggers to control SMT instantiation
  • Test with pathological inputs (empty arrays, boundary values)

Layer 2: Collection Operations​

Collection operations in src/prelude/prelude.mo and src/prelude/prim.mo are marked pure trusted:

module Seq = {
public pure trusted func empty<T>() : Seq<T> {
(prim "vseq_empty" : () -> Seq<T>)()
};
public pure trusted func len<T>(s : Seq<T>) : Int {
(prim "vseq_len" : Seq<T> -> Int) s
};
// ...
};

These map to Viper's built-in SMT-LIB theories through the collection preprocessing path. The primitive-name table lives in src/viper/collection_helpers.ml and is used from src/viper/prep.ml:

let collection_prim_name (collection : string) (func : string) : string option =
match collection with
| "Seq" ->
(match func with
| "empty" -> Some "vseq_empty"
| "len" -> Some "vseq_len"
| "get" -> Some "vseq_get"
(* ... *)
| _ -> None)
(* ... *)

If adding new primitive collection operations:

  1. Define the Motoko signature in prelude.mo as pure trusted
  2. Map to a primitive name in collection_helpers.ml
  3. Handle the primitive in the collection lowering path, usually collection_lowering.ml or lower_stmt.ml
  4. Ensure semantics match Viper's built-in theory exactly

Layer 3: User Assumption Boundaries​

User assumptions come in two distinct forms:

  • trusted functions have bodies or declaration-only signatures whose contracts are assumed rather than proved. They must declare at least one requires and one ensures.
  • abstract functions are bodyless verifier placeholders for proof interfaces or verification-driven development. Their contracts are assumed by callers until the declaration is refined with a verified body, but they are not permanent trusted-base decisions.

Signature-only declarations that are neither trusted nor abstract are rejected. entry_requires is also rejected on declaration-only functions because there is no exported runtime body to guard.

Trusted and bodyless declarations are handled in lowering by emitting their contracts for call sites without verifying a body:

(* src/viper/lower.ml, simplified *)
let trusted = attrs.Fun_attrs.trusted || attrs.Fun_attrs.decl_only in
(* contracts are emitted; trusted bodies and bodyless declarations are not verified here *)

This layer is user-controlled, but the mechanism is part of Sector9's trusted base. Prefer verified lemmas, ordinary contracted helpers, and refined abstract declarations when the implementation can be checked.

Adding Features Safely​

Checklist for New Features​

When extending Sector9, verify:

  • Does your feature depend on existing axioms?
  • Do you introduce new axioms or trusted components?
  • Have you added _sat, _unsat, and _reject tests for boundary cases?
  • Are axiom triggers properly scoped?
  • Does your feature interact with old() or permissions?

When New Axioms Are Needed​

New axioms may be required when:

  1. Introducing a new type encoding - Needs injectivity and projection axioms
  2. Adding heap structure - Needs distinctness axioms
  3. Supporting new spec operations - Needs semantic axioms

Example: Adding a new tuple arity requires projection and injectivity axioms (see prelude_tuple_encoding in prelude.ml).

Testing Axiom Soundness​

Axioms cannot be proven within the system, but you can test them:

  1. Consistency tests - Ensure axioms don't derive false
  2. Semantic tests - Verify operations behave as expected
  3. Boundary tests - Check empty collections, zero indices, maximum values
  4. Interaction tests - Test axiom combinations

Create tests in test/viper/ with both _sat and _unsat variants:

// axiom_seq_len_sat.mo - Should verify
ghost {
let s = Seq.singleton(42);
assert Seq.len(s) == 1; // Tests singleton/len axiom
}

// axiom_seq_len_wrong_unsat.mo - Should fail
ghost {
let s = Seq.empty<Nat>();
assert Seq.len(s) == 1; // Wrong: empty has length 0
}

Key Source Files​

Understanding these files is essential for trusted base work:

FilePurpose
src/viper/prelude.mlViper prelude axiom generation
src/prelude/prelude.moSector9 prelude with spec collections
src/prelude/prim.moPrimitive prelude declarations used by builtin lowering
src/viper/collection_helpers.mlCollection primitive name mapping
src/viper/prep.mlAST preprocessing and collection primitive preparation
src/viper/lower_exp.mlExpression lowering including quantifiers
src/viper/builder.mlUninterpreted function emission
src/mo_def/fun_attrs.mlFunction attribute storage (trusted, pure, etc.)
src/viper/spec_meta.mlSpec metadata collection

Axiom Generation Flow​

When Sector9 generates Viper code, axioms are assembled in stages:

1. Core prelude (prelude_core)
└─ Written once to $XDG_CACHE_HOME/sector9/prelude.vpr
└─ Contains: Array, Place, Address, Snapshot domains

2. Per-file prelude (prelude_per_file)
└─ Generated based on accumulated requirements
└─ Contains: Tuple$N domains, Map range axioms, typed fields

3. Program body
└─ Methods, predicates, functions with contracts

The reqs structure in src/viper/common.ml tracks what axioms are needed:

type reqs = {
tuple_arities : Int_set.t ref; (* Tuple$2, Tuple$3, etc. *)
map_types : (typ * typ) list ref; (* Map<K,V> types used *)
mutable_record_snapshots : ... ref; (* Snapshot domains needed *)
(* ... *)
}

Common Pitfalls​

Missing Triggers​

Axioms without proper triggers can cause SMT timeout or unsoundness:

// Bad: No trigger, matches everything
axiom bad_axiom {
forall x: Int :: x >= 0 ==> f(x) >= 0
}

// Good: Trigger limits instantiation
axiom good_axiom {
forall x: Int :: {f(x)} x >= 0 ==> f(x) >= 0
}

Overly Strong Axioms​

Axioms that are too strong can prove false properties:

// Dangerous: Claims all arrays have same size
axiom wrong_size {
forall a: Array, b: Array :: $size(a) == $size(b)
}

Missing Domain Constraints​

Quantifiers over Nat types need non-negativity constraints:

(* src/viper/lower_exp.ml *)
let domain_constraint_for_binder at (binder, motoko_typ) =
if is_nat_like_type motoko_typ then
Some (!!! at (GeCmpE (binder, intLitE at 0)))
else None

Reviewing Trusted Base Changes​

When reviewing PRs that modify the trusted base:

  1. Justify the change - Why is a new axiom necessary?
  2. Check mathematical soundness - Is the axiom correct?
  3. Review triggers - Are they properly scoped?
  4. Examine interactions - Does the axiom compose with existing ones?
  5. Verify test coverage - Are boundary cases tested?

Minimizing Trusted Base Growth​

The smaller the trusted base, the higher verification confidence. Strategies:

  • Derive instead of axiomatize - If a property can be proven from existing axioms, prove it
  • Use lemmas over unchecked assumptions - Lemmas with bodies are verified; trusted declarations are permanent assumptions, while abstract signature-only declarations should be refined when possible
  • Test extensively - Testing catches specification errors before they become soundness bugs
  • Document trust decisions - Explain why each trusted component exists

Summary​

  • The trusted base has three layers: prelude axioms, collection operations, and user assumption boundaries
  • New features may require new axioms; design them carefully for soundness
  • Test axioms with _sat and _unsat variants covering boundary cases
  • Key files: prelude.ml, prelude.mo, collection_helpers.ml, prep.ml, lower_exp.ml
  • Minimize trusted base growth by preferring verification over trust
  • Review trusted base changes with extra scrutiny