Frml
Frml is a small, statically typed, contract-based programming language for
teaching formal verification. It feels like a simplified combination of Python
and Dafny: ordinary imperative programs, plus requires / ensures contracts,
assert, loop invariants, decreases termination measures, old(...), and
forall / exists quantifiers. Verification is fully automatic: Frml
translates each program into verification conditions and asks the Z3 SMT
solver to check them. There are no interactive proof tactics and no handwritten
SMT formulas.
A program that verifies looks like this:
``` fn abs(x: Int) -> Int ensures result >= 0 { if x >= 0 { return x; } else { return -x; } }
fn main() -> Int { return abs(-7); } ```
Running the verifier prints VERIFIED; running the program prints nothing and
uses main's return value as the process exit code.
Installation
Frml requires Python 3.13 or newer.
bash
uv venv
source .venv/bin/activate
uv sync --dev
uv pip install -e .
This installs a frml command on your PATH. You can also run Frml without
installing it, using the module form:
bash
python -m frml check examples/abs.frml
Usage
Frml exposes three subcommands.
frml check FILE.frml
Parse, type-check, and verify the program. Prints VERIFIED on success, or
FAILED/UNKNOWN with source locations on failure.
frml run FILE.frml
Type-check the program (without static verification) and execute main(). The
exit status is main's return value.
frml verify-run FILE.frml
Verify first, and then execute main() if verification succeeded.
The language
Types
Frml has three basic types:
Int arbitrary-precision integers (no overflow)
Bool true or false
String a sequence of characters, written in double quotes
Frml supports homogeneous one-dimensional arrays:
Array<Int> mutable, fixed-length, zero-indexed arrays
Array<Bool> mutable, fixed-length, zero-indexed arrays
Array<String> mutable, fixed-length, zero-indexed arrays of strings
Nested arrays (Array<Array<Int>>) are not allowed, and Frml does not (yet)
have a mapping (dictionary) type.
Functions
fn max(a: Int, b: Int) -> Int
ensures result >= a
ensures result >= b
ensures result == a or result == b
{
if a >= b {
return a;
} else {
return b;
}
}
requiresclauses are assumptions about the caller's inputs.ensuresclauses are guarantees about the result.resultnames the return value.old(e)refers to the value ofeat function entry.
- Multiple
requires/ensuresclauses are implicitly ANDed. - A function may omit
-> TYPE, making it a void procedure:
fn increment_first(a: Array<Int>)
requires length(a) > 0
ensures a[0] == old(a[0]) + 1
{
a[0] = a[0] + 1;
}
- Recursive functions must declare a
decreasesmeasure.
Statements
let NAME: TYPE = expr; // local variable declaration
NAME = expr; // scalar assignment
a[i] = expr; // array element assignment
if expr { ... } else { ... } // conditional (else optional)
while expr
invariant expr // loop invariants (optional)
decreases expr // termination measure (optional)
{ ... }
return expr; // (value-returning functions only)
assert expr; // runtime + static assertion
f(args); // procedure call statement
Expressions
Operators, from lowest to highest precedence: =>, or, and, ==/!=,
</<=/>/>=, +/-/++, *///%, unary !/-/`, then indexing
and calls.
/ and % use Euclidean integer division compatible with Z3 (the remainder is
always non-negative). Division and modulo by zero are runtime errors and
verification obligations.
String literals are written in double quotes and support the usual C-style
backslash escapes (\n, \t, \", \\, and so on). The ++ operator
concatenates two strings, and a single backtick before a value converts it to a
string (5 is "5", x is the string form of x's value):
let greeting: String = "hello" ++ " " ++ "world";
let n: Int = 42;
let label: String = `n; // "42"
Quantifiers are available in specifications:
forall i: Int :: 0 <= i and i < length(a) => a[i] >= 0
exists i: Int :: 0 <= i and i < length(a) and a[i] == 0
length(a) is the single built-in function.
Examples
The examples/ directory contains small programs:
abs.frml,max.frml,count.frml: the core required examples.required.frml: all three core examples plus amain.factorial.frml: recursive function withdecreases.all_nonnegative.frml: array property proved with a quantifier loop invariant.increment_first.frml: a procedure mutating an array, withold.bad.frml: a deliberately unprovable postcondition (FAILED).precondition.frml: a runtime precondition violation.
Try them:
bash
frml check examples/required.frml
frml verify-run examples/factorial.frml
frml check examples/bad.frml
frml run examples/precondition.frml
Intentional limitations
These choices keep the verifier sound and the implementation small; they match the specification's own "simplest recommended model":
Arrays are references, not values.
: let b: Array<Int> = a; (array-to-array assignment) is rejected to avoid
aliasing ambiguity. Create fresh arrays with array literals instead.
Functions return Int or Bool.
: Array-returning functions are not supported. Array mutation is expressed
with procedures, as shown earlier.
A call that takes array arguments must appear on its own.
: It may be a statement, or the whole right-hand side of, let, return, or
assignment. It may not be nested inside a larger expression (f(a) + g(a)
is rejected during verification).
Runtime quantifier checking is best-effort.
: Quantified postconditions over a finite array index range (0 <= i and i <
length(a)) are evaluated at runtime. Other quantified expressions are
skipped during runtime checking but still fully verified statically.
The verifier never claims success on a program it cannot prove. If Z3 cannot
decide an obligation it reports UNKNOWN, and a genuinely unprovable program is
reported as FAILED: it is never silently accepted.
Project layout
frml/
errors.py error types and source-location formatting
lexer.py tokenizer
parser.py recursive-descent parser -> AST
ast_nodes.py AST node definitions
types.py Int / Bool / Array<T> types
typechecker.py name resolution + static type checking
interpreter.py concrete executor with runtime checks
prover.py verification-condition generation + Z3 proof checking
cli.py the `frml` command-line interface