Formal Silk
Formal Silk is Silk’s compile-time formal verification language. It is written using syntax that does not exist at runtime and is discharged at compile time using the Z3 SMT solver.
When Formal Silk syntax is present, compilation generates verification conditions (VCs), proves them with Z3, and fails the build if any VC cannot be proven. This behavior applies to:
- the
silkCLI (silk check,silk test,silk build), and - the C ABI build entrypoints (
silk_compiler_build,silk_compiler_build_to_bytes).
Proof requirements are opt-in by syntax#
Silk requires proofs only when verification syntax is present in the compiled module set:
- any use of
#...directives (#require,#assure,#assert,#invariant,#variant,#monovariant,#const) — including#requireattached tostructdeclarations.
When verification syntax is present, compilation MUST:
- generate VCs,
- prove them using Z3, and
- fail compilation with clear diagnostics if any VC cannot be proven.
When verification syntax is not present, compilation does not require proofs.
Z3 linkage and overrides#
On supported native hosts, Silk links the built-in Z3 static library and its
headers (vendor/include) directly into the compiler when the host archive is
present:
linux/x86_64->vendor/lib/x64-linux/libz3.amacos/aarch64->vendor/lib/aarch64-macos/libz3.a(optional and staged when present)
If no static host archive is present, the compiler still builds, but Formal Silk verification reports Z3 as unavailable unless a dynamic library override is provided.
To override the Z3 library at runtime (for example to test against a different Z3 build), provide a dynamic library path:
- CLI: pass
--z3-lib <path>, or - CLI/ABI: set
SILK_Z3_LIBin the environment.
When --z3-lib is provided, it overrides SILK_Z3_LIB.
Debugging proofs with Z3 (--debug)#
When a verification condition fails, the compiler reports a normal diagnostic at the failing annotation site.
When --debug is passed to silk build or silk test, the verifier also emits
additional Z3 debugging output to stderr and writes an SMT-LIB2 reproduction
script under .silk/z3/ in the current working directory (or $SILK_WORK_DIR/z3):
.silk/z3/silk_z3_m<module>_<n>.smt2
You can replay the query with an external Z3 binary:
z3 -smt2 .silk/z3/silk_z3_m0_0.smt2
Successful silk build runs that compile exported Formal Silk surface also emit
a distributable success-path bundle under .silk/formal/ (or
$SILK_WORK_DIR/formal/) keyed by the output artifact identity. See
“Distribution and export bundles” below.
Z3 model#
The current Formal Silk verifier maps Silk constructs directly to Z3:
bool→ Z3 Bool.string→ Z3 String (Supported forms: literals and equality/inequality comparisons).- integer primitives → fixed-width Z3 bitvectors:
i8/u8→ BV8i16/u16→ BV16i32/u32→ BV32i64/u64/int→ BV64
Arithmetic is modular 2^N (wraparound). Ordered comparisons and >> use
signed semantics for signed integers (i*/int) and unsigned semantics for
unsigned integers (u*).
- other primitive/runtime values that do not currently have dedicated numeric
reasoning support (for example
char, floating-point primitives,Range,Instant, andDuration) are modeled as opaque uninterpreted values. - non-primitive runtime values passed through contracts
(
&T, named values, optionals, arrays, function values, and applied types) are modeled as opaque uninterpreted values: - equality/inequality works when both sides have the same Silk type,
- the verifier does not infer field layout or numeric ordering from these values,
- this is enough for exported method receivers and distributable contracts that only need identity-style reasoning over non-primitive parameters.
Supported operators in specification expressions (Supported forms):
- boolean:
!,&&,||,==,!= - string:
==,!= - integer:
- unary:
-,~ - arithmetic:
+,-,*,/,% - bitwise:
&,|,^,<<,>> - comparisons:
<,<=,>,>=,==,!= - size/layout queries:
sizeof,alignof,offsetof(type operands and other statically-sized operands in the Supported forms)
Supported name-resolution sources in specification expressions (Supported forms):
- in-scope runtime/formal bindings,
- compiler-provided metadata constants such as
BUILD_*,OS_*, andSILK_*, - and const-evaluable module/package
constbindings, including exported qualified names such asstd::limits::I64_MAX.
Other operators and expression forms are currently rejected in verified code (see the notes below).
The ext boundary#
External declarations (ext) have no body available to the verifier.
Therefore:
- The verifier cannot generate VCs about the behavior of
extbodies. - In the current verifier subset, calls are supported only to functions and
methods that have Formal Silk contracts (see “Contracted calls” below).
extdeclarations do not have Formal Silk contracts yet, so verified code cannot callextfunctions.
See ext for the external-declaration rules.
The main constructs are:
#const— formal Silk declarations used inside specifications.#require— precondition.#assure— postcondition.#assert— block-local proof obligation.#invariant— loop or state invariant.#variant— well-founded termination measure (ranking function).#monovariant— monotonic measure (non-decreasing or non-increasing).theory/#theory— reusable, parameterized proof obligations.
Key properties:
- These annotations appear before the function or loop they describe.
- They are used by the verifier only and incur no runtime cost.
Formal Silk declarations (#const)#
Formal Silk declarations let you name intermediate values for use in specifications.
Syntax:
#const name = <Expr>;
Rules:
#constis a statement that may appear inside function bodies (inside blocks).- The binding is compile-time-only and is not lowered into runtime code.
- A
#constbinding is visible only inside specification expressions: - function specs (
#require,#assure), - loop specs (
#invariant,#variant,#monovariant). - Using a
#constname in a runtime expression (e.g. inwhileconditions or normalletinitializers) is a compile-time error. Use a normalletbinding for runtime values, and (optionally) introduce a#constalias for specifications.
Example:
fn main () -> int {
let limit: int = 3;
#const original_limit = limit;
let mut i: int = 0;
#invariant i >= 0;
#invariant i <= original_limit;
#variant original_limit - i;
while i < limit {
i = i + 1;
}
return 0;
}
Function annotations#
For functions, the initial surface syntax is:
#require <Expr>;
#require <Expr2>;
#assure <Expr3>;
#theory TheoryName(args...);
fn name (params) -> ResultType {
...
}
- One or more
#require,#assure, and contract-theory attachments (#theory Name(args...);) may appear, in any order, immediately before thefndeclaration (and before anyexportmodifier). - Each annotation is terminated by a semicolon.
- The compiler front-end:
- lexes these annotations as dedicated tokens,
- parses the annotation expressions using the normal expression grammar,
- type-checks each annotation expression as
boolso obvious mistakes are rejected early (specifications are still compile-time-only metadata), - attaches them to the corresponding function in the AST as lists of preconditions, postconditions, and contract theories.
Struct requirements (#require on struct)#
Struct declarations may be preceded by one or more #require directives:
#require <Expr>;
struct Name {
field: int,
}
These #require expressions are struct requirements: properties that must
hold for all values constructed for that struct type.
Rules (Supported forms):
- Struct requirement expressions may reference the struct's fields by name.
- When Formal Silk syntax is present, the verifier proves all requirements at
struct literal construction sites (
Name{ ... }andnew Name{ ... }), using the literal's field initializers and default initialization for any omitted fields. - When a struct extends a base struct, the derived struct inherits the base struct's requirements (all requirements must be proven at construction).
- If the verifier cannot prove a requirement, compilation fails with
E3006. The diagnostic names the failed predicate and, when the predicate references fields initialized by the literal or by defaults, reports those field values.
Loop specifications (#invariant, #variant, #monovariant) follow a similar
pattern for loops.
Loop annotations#
For while loops, the initial surface syntax is:
#invariant <Expr>;
#variant <Expr2>;
#monovariant <Expr3>;
while condition {
...
}
Rules:
- One or more
#invariantannotations, zero or more#monovariantannotations, and at most one#variantannotation may appear immediately before thewhilekeyword. - Each annotation is terminated by a semicolon.
- The compiler front-end:
- lexes these annotations as directive tokens,
- parses the annotation expressions using the normal expression grammar,
- attaches them to the corresponding loop in the AST as invariants, monovariants, and a (single) variant expression.
The verifier will interpret:
#invariantexpressions (typeboolin the Supported forms) as properties that must hold:- before entering the loop,
- after each iteration (assuming the body and condition do not diverge),
- and at
breakexits (so proofs after the loop may rely on the invariant). #variantexpressions as a well-founded measure that must decrease on each iteration (and be non-negative at the loop head), used for termination proofs.#monovariantexpressions as measures that must be monotonic on each iteration (either non-decreasing or non-increasing, proved consistently across all continuation paths).
Compiler requirements:
- Parse and represent these annotations in the AST.
- Integrate with the verifier to check specifications.
- Ensure that, if verification fails, compilation fails with clear diagnostics.
Block assertions (#assert)#
Formal Silk also supports block-local proof obligations:
#assert <Expr>;
Rules:
#assertis a statement that may appear inside function/test bodies (inside blocks).- It is compile-time-only metadata and is not lowered into runtime code.
- The verifier must prove the assertion holds in the current symbolic state at
the
#assertsite. If it cannot be proven, compilation fails. - After a
#assertsucceeds, the asserted expression is assumed to hold for the remainder of the block (so later proofs may rely on it).
Notes#
Implemented end-to-end (Z3-backed, Supported forms):
- The verifier runs only when Formal Silk syntax is present.
#require/#assure:- generate VCs and prove them for verified
fndeclarations and verifiedimplmethods. #assuremay referenceresult(the return value) as a built-in formal declaration.- build metadata constants are available in Formal Silk expressions:
BUILD_KIND,BUILD_MODE,BUILD_VERSIONas built-in compile-timestringvalues,- and
BUILD_VERSION_MAJOR/BUILD_VERSION_MINOR/BUILD_VERSION_PATCHas built-in compile-timeu64values. - Struct requirements (
#requireonstructdeclarations): - generate VCs and prove them at struct literal construction sites (
Type{ ... }andnew Type{ ... }), - include inherited requirements from base structs (
struct Child extends Base { ... }). #assert:- proves the asserted expression holds at the
#assertsite, - and then assumes it for the remainder of the block.
#invariant/#variant/#monovariantonwhileloops:- prove invariants at entry and preservation across one iteration,
- prove variants are non-negative and decrease across one iteration.
- prove monovariants are monotonic across one iteration (either non-decreasing or non-increasing, consistent across all continuation paths).
- Formal Silk declarations via
#const: - may be referenced only by specification expressions,
- are rejected in runtime expressions (
E2014). theory(reusable assertions, initial subset):theory Name(params) { ... }defines a reusable set of proof obligations (exportable/importable at top level),#theory Name(args);applies it in a function body as compile-time-only assertions,#theory Name(params) { ... }may also declare an inline (non-exportable) local theory inside a block.- Contracted calls:
- direct calls of the form
Name(args...)are checked whenNameresolves to a function with a Formal Silk contract, - receiver calls of the form
expr.method(args...)are checked when the receiver resolves to a concrete method owner andmethodresolves to a method with a Formal Silk contract, - at every checked call site, including ordinary callers that have no Formal
Silk annotations of their own, the verifier proves the callee’s
preconditions (explicit
#requireand any attached-theory#require) under the caller’s current path condition; errors reportE3007, - the caller path condition begins with its own explicit and attached-theory preconditions plus inherited requirements on aggregate parameters, so a contracted wrapper may delegate every input obligation it declares,
- after the call, the verifier assumes the callee’s postconditions (explicit
#assureplus attached-theory#assure/#invariant) into verified callers' symbolic state so subsequent proofs can use them, - if the callee has a source-visible body, the Supported forms requires that body to be a single return expression (no runtime statements); the verifier inlines that return expression in the caller’s symbolic state,
- if the callee has no body (a declaration-only prototype, typically used when linking against a precompiled implementation), the verifier treats the call as opaque:
- it proves the preconditions at the call site,
- introduces an uninterpreted symbolic value for the return,
- and assumes postconditions about that return value,
- if the callee has no Formal Silk contract, verified code may still call it in the Supported forms:
- if the source-visible body is a single return expression, the verifier may inline that return expression into the caller’s symbolic state,
- otherwise the verifier treats the return value as opaque and assumes no additional facts about it,
- unused expression-statement calls are permitted under the same rule,
- and the verifier does not infer side-effect facts from these contractless calls,
- recursion is not supported yet.
- Stdlib modules are verified when they use Formal Silk syntax, subject to the same current-subset restrictions described here.
Not implemented yet :
- Counterexample models (the verifier reports errors, but does not yet print a model).
- Verified local bindings are still limited to primitive/string-like symbolic
types plus opaque parameter-style values. Field projections from typed
parameters/results/receivers used in contracts and
#theoryarguments are supported through uninterpreted projections. Direct field assignment through a named aggregate or receiver (name.field = expr) is modeled by creating a fresh aggregate value, constraining the assigned field, preserving the other fields, and rechecking the aggregate's struct requirements. Verified function/method entry also assumes struct requirements for non-optional typed aggregate parameters, so direct field writes can prove requirements over untouched fields from the aggregate's starting invariant. Fully nested field-sensitive named-struct local-state reasoning is not yet supported in verified blocks. - Verification of the full expression language and full statement language
(
match, nested loops, indirect calls, and many operators are not supported yet in verified code). Statement-levelifpath splitting is implemented in the Supported forms. - Verified assignment statements currently support local names and direct field
writes through named aggregate or receiver values (
name.field = expr). Direct field writes re-prove the target aggregate's struct#requireclauses after the write. Optional-field, index, nested-field, and compound assignment targets are rejected withE3005.
Theories (theory / #theory)#
A theory is a reusable, parameterized block of Formal Silk directives that can be applied at points inside a function body to assert properties about the current symbolic state.
Syntax#
Declaration form (top-level):
export theory a_custom_theory (x: int, y: int) {
#const z = x + y;
#invariant x != 0 && y != 0;
#invariant z > 1;
}
An inline theory declaration may also appear as a statement inside a function/test block:
fn main (x: int, y: int) -> int {
#theory local_sum_nonzero (x: int, y: int) {
#const z = x + y;
#assure z != 0;
}
#theory local_sum_nonzero(x, y);
return 0;
}
Use form (statement inside a function body, or inside another theory body):
fn main (x: int, y: int) -> int {
#theory a_custom_theory(x, y);
return 0;
}
Theories may apply other theories:
export theory nonzero (x: int) {
#require x != 0;
}
export theory nonzero_sum (x: int, y: int) {
#theory nonzero(x);
#theory nonzero(y);
#assure (x + y) != 0;
}
Notes:
- Top-level theory declarations use the
theorykeyword. - Inline (block) theory declarations and theory use sites share the
#theorytoken; the parser disambiguates by the token that follows the argument/parameter list: { ... }starts an inline theory declaration,;terminates a theory use.- A top-level theory declaration may be exported (
export theory ...). Exported theories may be imported from other modules and reused. - Inline theory declarations inside a block are not exportable/importable; they
exist only in the containing block and may be applied via
#theory Name(...);after they are declared. - A theory body may contain
#theory Name(args);statements. These are compile-time-only theory applications; they are checked by the verifier in the current symbolic state at the point they appear in the theory body. - Theory recursion is rejected (direct or indirect cycles).
- Theories are not runtime functions. They can only be applied via
#theoryuse statements; calling a theory with normal call syntax (Name(...)) is a compile-time error.
Prefix #require / #assure on theories#
For ergonomics, a theory declaration may be preceded by #require and/or
#assure directives:
#require x >= 0;
export theory ensure_nonnegative_x (x: int) {
#assure x >= 0;
}
These prefix directives are treated as if they were written at the beginning of the theory body.
Theories as function contracts#
A function may attach one or more theories as part of its Formal Silk contract
surface by placing #theory Name(args...); directives in the function-spec
prelude:
import { bounded_nonneg_add } from "./theories.slk";
#theory bounded_nonneg_add(x, y);
export fn add (x: int, y: int) -> int {
return x + y;
}
Contract-theory attachments:
- are compile-time-only metadata (not runtime statements),
- contribute additional preconditions/postconditions to the function contract:
#requirebecome additional function preconditions,#assureand#invariantbecome additional function postconditions,- are used by the verifier to check contracted call preconditions and to enable contracted calls in verified code (see “Contracted calls” above),
- are not permitted before a top-level
theorydeclaration (only#require/#assuremay prefix a theory declaration).
Importing theories#
Exported theories may be imported via JS-style named imports and then applied
via #theory use statements.
Example:
// theories.slk
export theory ensure_positive_x (x: int) {
#assure x > 0;
}
// main.slk
import { ensure_positive_x as pos_x } from "./theories.slk";
fn main () -> int {
let x: int = 1;
#theory pos_x(x);
return 0;
}
Rules:
- Only exported theories may be imported.
- A theory use (
#theory Name(args);or#theory pkg::Name(args);) resolves the theory name as either: - a local theory declared in the same module, or
- an imported theory name from
import { ... } from "<specifier>";, or - an exported theory addressed by its package-qualified name.
Semantics#
When a theory is applied (#theory Name(args);):
- its parameters are bound to the provided argument expressions (as specification expressions),
- its
#constformal declarations are evaluated and are visible only within the theory during checking, - each
#require <Expr>;,#invariant <Expr>;, and#assure <Expr>;directive in the theory body is treated as a compile-time proof obligation that must hold in the current symbolic state at the use site, - each
#variant <Expr>;directive in the theory body is treated as a non-negativity obligation (Expr >= 0) at the use site (the theory form does not model decrease across iterations).
Theory bodies are verifier-only:
- Theory argument expressions and theory directive expressions are Formal Silk specification expressions evaluated by the verifier.
- In the Supported forms, specification expressions do not support function
calls or value construction (for example
foo(x),Type{...}, arrays, ornew). Such expressions are rejected as unsupported Formal Silk.
The theory form is compile-time-only and has no runtime semantics.
Distribution and export bundles#
When a successful silk build compiles a module set that exposes reusable
Formal Silk surface, the compiler emits a machine-readable export bundle.
Exported surface that participates today:
export theory Name(...) { ... }- exported top-level functions whose contract surface is non-empty
(
#require,#assure, or contract#theory) - exported/public
implmethods whose contract surface is non-empty
The bundle is written under the compiler work directory:
- direct builds:
.silk/formal/<output-identity>/manifest.json.silk/formal/<output-identity>/bundle.smt2- when
SILK_WORK_DIRis set, the same layout is rooted there instead of.silk/
The manifest records, for each entry:
- a stable entry id
- whether it is a
theory,function, ormethod - the originating module path
- the package name
- the exported symbol name
- the owner type for methods
- the normalized signature string
- the payload section id inside
bundle.smt2 - the count of exported
#require/#assureobligations - and the attached theory ids for contracted functions/methods
The payload contract is intentionally source-oriented and portable:
payload_solver = "z3"payload_format = "smt2"payload_encoding = "source"
That is, Silk currently distributes normalized SMT-LIB2 source, not a
solver-private binary snapshot. This keeps the artifact inspectable, stable
across hosts, and suitable for replay with an external z3 -smt2 ... tool.
Installed packages copy the same bundle under the package root:
share/silk/formal/<artifact-relative-path>/manifest.jsonshare/silk/formal/<artifact-relative-path>/bundle.smt2
This makes Formal Silk metadata distributable alongside definitions, headers,
and native artifacts, while keeping source-visible theory/prototype declarations
as the authoritative import-time verification surface. Installed package
loading and silk package inspect discover these bundle paths so downstream
tooling can locate the exported Formal Silk payload directly.
Source repository · Edit this page · View Markdown