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.
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:
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:
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
Metadata
Release files for frml 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| frml-0.2.0.tar.gz | 255.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| frml-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 283.3 kB
Release files / frml-0.2.0.tar.gz
| Download URL | frml-0.2.0.tar.gz |
|---|---|
| Size | 255.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
57c5d2c13f5d867b39f14a966afe870c6d40cd960930fd414f1b2579aa748467
|
|
BLAKE2b-256 checksum How to use checksums |
fa2cc396ea2c181372f0908858ee8b6e1c3e9ed900a33e1f219c4939e6beecce
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.9
|
Release files / frml-0.2.0-py3-none-any.whl
| Download URL | frml-0.2.0-py3-none-any.whl |
|---|---|
| Size | 28.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ea2d8bc7abfe97f0a4b306576a65f50da316985646bb5df55906bd29f0a5d85f
|
|
BLAKE2b-256 checksum How to use checksums |
754c92483a7021b97560cf4a5dc90694e6793b6e715a4ad1496595317081489e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.9
|