Refinement Types

Refinement types let you constrain values at the type level. A refined type is a base type plus a predicate — the compiler verifies the predicate holds, and erases the refinement in the emitted C#.


Quick Reference

SyntaxPurposeExample
§RTYPE{id:Name:base} (pred)Define a named refinement type§RTYPE{r1:NatInt:i32} (>= # INT:0)
§I{type:param} | (pred)Inline refinement on a parameter§I{i32:age} | (>= # INT:0)
§PROOF{id:desc} (expr)Proof obligation (assert a fact)§PROOF{p1:positive} (>= x INT:0)
#Self-reference (the value being refined)(>= # INT:0)

Named Refinement Types (§RTYPE)

Define a refinement type at module level. The predicate uses # to refer to the value being constrained.

Syntax

Plain Text
§RTYPE{id:Name:baseType} (predicate)
  • id — unique identifier (e.g., r1)
  • Name — the refinement type name, used in parameter declarations
  • baseType — the underlying type (i32, str, etc.)
  • predicate — a boolean expression using # for the value

Examples

Plain Text
§RTYPE{r1:NatInt:i32} (>= # INT:0)
§RTYPE{r2:Port:i32} (&& (>= # INT:1) (<= # INT:65535))
§RTYPE{r3:NonEmpty:str} (> (len #) INT:0)

These read as:

  • NatInt: an i32 that is non-negative
  • Port: an i32 between 1 and 65535
  • NonEmpty: a str with length greater than 0

Using Named Refinement Types

Once defined, use the name as a parameter type:

Plain Text
§RTYPE{r1:NatInt:i32} (>= # INT:0)

§F{f001:Process:pub}
  §I{NatInt:count}
  §O{void}
  // count is guaranteed non-negative
§/F{f001}

The obligation engine creates a verification obligation for count — proving the refinement predicate holds given the function's preconditions.


Inline Refinements (|)

For one-off constraints, attach a predicate directly to a parameter with |:

Syntax

Plain Text
§I{baseType:paramName} | (predicate)

Examples

Plain Text
§F{f001:Divide:pub}
  §I{i32:a}
  §I{i32:b} | (!= # INT:0)         // b must be non-zero
  §O{i32}
  §R (/ a b)
§/F{f001}
Plain Text
§F{f002:SetPort:pub}
  §I{i32:port} | (&& (>= # INT:1) (<= # INT:65535))
  §O{void}
  // ...
§/F{f002}

Inline Refinements with Default Values

Inline refinements can coexist with default parameter values:

Plain Text
§I{i32:age} | (>= # INT:0) = INT:18

Self-Reference (#)

The # symbol refers to the value being constrained. It is only valid inside refinement predicates:

Plain Text
§RTYPE{r1:Positive:i32} (> # INT:0)     // # = the i32 value
§I{i32:x} | (>= # INT:0)                // # = x

Using # outside a refinement predicate is a compile error (SelfRefOutsidePredicate).


Proof Obligations (§PROOF)

A proof obligation asserts that a condition holds at a specific point in the function body. The obligation engine attempts to prove it using Z3.

Syntax

Plain Text
§PROOF{id:description} (boolean-expression)
§PROOF{id} (boolean-expression)
  • id — unique identifier
  • description — optional human-readable label

Examples

Plain Text
§F{f001:Transfer:pub}
  §I{i32:balance}
  §I{i32:amount} | (> # INT:0)
  §O{i32}
  §Q (>= balance amount)

  §B{newBalance:i32} (- balance amount)
  §PROOF{p1:non-negative} (>= newBalance INT:0)

  §R newBalance
§/F{f001}

Z3 proves this: given balance >= amount and amount > 0, then balance - amount >= 0.

Proof Obligation Statuses

StatusMeaningC# Output
DischargedZ3 proved the condition// PROVEN: proof obligation [p1]
FailedZ3 found a counterexampleRuntime guard (throws on violation)
TimeoutZ3 couldn't decide in timeRuntime guard (configurable by policy)
BoundaryPublic API — can't verify callersRuntime guard (always emitted)

Erasure

Refinement types are a compile-time construct. In the emitted C#, they are erased:

CalorEmitted C#
§RTYPE{r1:NatInt:i32} (>= # INT:0)(nothing — erased)
§I{NatInt:count}int count
§I{i32:x} | (>= # INT:0)int x
§PROOF{p1:check} (cond) (Discharged)// PROVEN: proof obligation [p1: check]
§PROOF{p1:check} (cond) (Failed)if (!(cond)) throw new InvalidOperationException(...)

The refinement constrains what values are valid. The obligation engine verifies those constraints and emits runtime guards only where verification fails.


Obligation Engine

The obligation engine is the verification pipeline for refinement types:

  1. Generation — Walk the AST and create obligations for every refined parameter and §PROOF statement
  2. Solving — For each obligation, use Z3's assume-negate-check pattern:
    • Assume all preconditions (§Q)
    • Negate the obligation condition
    • If UNSAT → Discharged (proven)
    • If SAT → Failed (counterexample found)
    • If UNKNOWN → Timeout
  3. Guard Discovery — For failed obligations, discover the simplest guard that would discharge them
  4. Code Generation — Emit runtime guards based on obligation status and policy

Obligation Kinds

KindSource
RefinementEntryParameter with inline or named refinement
ProofObligation§PROOF statement

Public vs Private Functions

For public functions, caller constraints can't be verified — parameter refinement obligations are marked Boundary and always emit runtime guards. For private functions, the solver attempts full verification.


Obligation Policy

The obligation policy controls what happens for each obligation status. Three built-in policies:

Default

StatusAction
DischargedIgnore (proven, no guard needed)
FailedError (compilation fails)
TimeoutWarnAndGuard
BoundaryAlwaysGuard
UnsupportedWarnOnly

Strict

All non-discharged obligations are compilation errors. Use in CI for maximum safety.

Permissive

Failed obligations emit warnings and guards instead of errors. Use during development or migration.


Complete Example

Plain Text
§M{m001:Banking}

// Named refinement types
§RTYPE{r1:PositiveAmount:i32} (> # INT:0)
§RTYPE{r2:NonNegBalance:i32} (>= # INT:0)

§F{f001:Transfer:pub}
  §I{NonNegBalance:balance}
  §I{PositiveAmount:amount}
  §O{i32}
  §Q (>= balance amount)                  // sufficient funds

  §B{result:i32} (- balance amount)
  §PROOF{p1:balance-safe} (>= result INT:0)

  §R result
§/F{f001}

§/M{m001}

The obligation engine:

  1. Creates Boundary obligations for balance and amount (public function)
  2. Creates a ProofObligation for p1
  3. Z3 proves p1: given balance >= 0, amount > 0, and balance >= amount, then balance - amount >= 0
  4. Emitted C#: runtime guards for the public boundary parameters, proven comment for p1

See Also