How to Make AI-Generated Code Provably Correct
A task-oriented walkthrough of the RES-3780 verification stack: tag AI-authored code for the audit trail, opt a module into mandatory contracts, bound its loops, and produce a portable proof artifact a downstream consumer can check without re-running the compiler.
Table of contents
- Before you start: provenance is not enforcement
- Step 1 — Tag provenance (optional, informational only)
- Step 2 — Opt the module into contract verification
- Step 3 — Bound your loops
- Step 4 — Make violations fatal:
--typecheck-strict - Step 5 — Verify with Z3
- Step 6 — Emit a portable proof certificate
- Step 7 — Cheap CI signal:
--vibe-gate - Putting it all together
- Related reading
Before you start: provenance is not enforcement
Earlier drafts of this feature (RES-3780 as originally filed) tied
verification to the @ai_generated tag itself: tag a function and the
compiler would force contracts and loop bounds onto it. RES-3854 /
RES-3858 deliberately reversed that design — correctness is not a
property of who (or what) wrote the code. A hand-written function
deserves the same proof obligations as a model-generated one, and a
tag you can delete to skip checks was never a real guarantee.
The current model splits the two concerns:
| Concern | Mechanism |
|---|---|
| “Was this AI-authored?” (audit trail) | @ai_generated / #[generated(...)] — pure metadata |
| “Must this be verified?” (enforcement) | @require_contracts / @require_contracts(strict) — a module-level policy |
Adding or removing @ai_generated changes zero compiler
diagnostics. If you want the compiler to actually check something, you
need @require_contracts. The rest of this guide builds that up from
the tagging step to a signed-off proof certificate. See
AI_GENERATED_DESIGN.md for the full design
history if you want the “why,” not just the “how.”
Step 1 — Tag provenance (optional, informational only)
If you want an audit trail of what a model wrote, tag it:
@ai_generated
fn clamp_positive(int x) -> int {
if x < 0 { return 0; }
return x;
}
For a richer trail (what prompt, what intent), use the underlying annotation directly instead:
#[generated(intent = "clamp a signed reading to non-negative", prompt_hash = "9f2a…")]
fn clamp_positive(int x) -> int { /* … */ }
@ai_generated is recorded as an alias of #[generated(...)]
(RES-3835 / RES-3858). Either form shows up in
--emit-contract-certificate output as an informational
"provenance" array on that function — nothing more. Tagging alone
buys you no checking. Continue to Step 2 for that.
Step 2 — Opt the module into contract verification
Add @require_contracts at the top of the file to enrol every
function in it:
@require_contracts
fn clamp_positive(int x) -> int requires x < 1000 ensures result >= 0 {
if x < 0 { return 0; }
return x;
}
Under the bare directive, any clause you do write must be non-vacuous:
- every
requiresmust reference at least one parameter (requires trueis rejected); - every
ensuresmust referenceresult(ensures trueor an input-only restatement is rejected).
Functions with no contract at all are still accepted under the bare directive — it only stops you from writing a fake contract, not from writing none.
Mandatory contracts: @require_contracts(strict)
To also require every function to have a contract, use the strict variant:
@require_contracts(strict)
fn add(int x, int y) -> int requires x >= 0 requires y >= 0 ensures result >= 0 {
return x + y;
}
Under strict, every named function except main must declare at
least one ensures clause, and at least one requires clause if it
takes parameters. main is exempt — it has no caller-supplied
parameters and returns no result the caller inspects. This is the
“safety-critical module” posture: nobody can opt a function out simply
by not writing a contract.
If you drop the requires/ensures above, the compiler reports:
error[contract_policy]: function `add` violates `@require_contracts`: strict policy
demands at least one `requires` clause constraining its inputs — add `requires <param_condition>`
Step 3 — Bound your loops
Once a function is enrolled (via either @require_contracts variant),
any while loop in its body must carry a #[loop_bound(N)] attribute
naming its maximum iteration count:
@require_contracts(strict)
#[loop_bound(100)]
fn count_up(int n) -> int requires n >= 0 requires n <= 100 ensures result >= 0 {
let i = 0;
while (i < n) {
i = i + 1;
}
return i;
}
Omit #[loop_bound(N)] on an enrolled function with a while loop and
the compiler reports:
error[loop_bound]: function `count_up` enrolled in contract verification: contains
a while-loop and requires #[loop_bound(N)] (RES-3780 Tier 2)
With --features z3, the compiler additionally tries to prove (or
refute) the declared bound for loops that follow a simple
monotonic-counter shape (while (i < n) { i = i + step; }-style). A
bound the prover can’t statically match falls back to a runtime check
instead of a hard compile error — you get a warning, not a failure.
#[loop_bound(N)] is unaffected by provenance: it fires purely off
@require_contracts enrolment plus the presence of a while loop,
regardless of whether @ai_generated is attached.
Step 4 — Make violations fatal: --typecheck-strict
This is the step people miss. By default, rz runs the type
checker but treats violations as soft diagnostics: they print to
stderr, and the program still compiles and runs (RES-1088 — this keeps
legacy scripts executing while surfacing what they’re missing). That
means everything in Steps 2–3 above will warn by default, not fail
your build.
To make contract-policy and loop-bound violations (or any type error)
fatal, pass --typecheck-strict:
$ rz --typecheck-strict my_module.rz
This is the flag to wire into CI — it exits non-zero the moment any
enrolled function violates its contract policy or loop-bound
requirement, with none of the extra “Running type checker…” /
“Type check passed” lines that plain -t/--typecheck prints.
Step 5 — Verify with Z3
Build (or run) with --features z3 to get real proof attempts instead
of warning[partial-proof]: Z3 returned Unknown placeholders:
cargo build --manifest-path resilient/Cargo.toml --features z3
With Z3 available, each requires/ensures clause and each
#[loop_bound(N)] is discharged against an SMT query. A clause that
Z3 refutes is a hard type error with a counterexample; a clause it
can’t decide (unsupported theory, solver timeout) degrades to
unknown and falls back to a runtime check — it never silently
“passes.” See
VERIFICATION_MODEL.md and
VERIFICATION_LIMITS.md for what the
prover can and can’t discharge.
Step 6 — Emit a portable proof certificate
--emit-contract-certificate <FILE> writes a deterministic JSON
document — per function, per clause — attesting what the verifier
established, independent of whether the caller has Z3 or even the
Resilient toolchain installed:
$ rz my_module.rz --emit-contract-certificate cert.json
{
"schema": "resilient-contract-certificate/v1",
"schema_version": 1,
"source": "my_module.rz",
"functions": [
{
"name": "count_up",
"enrolled": true,
"provenance": ["ai_generated"],
"clauses": [
{ "clause": "n >= 0", "kind": "requires", "verdict": "unknown" },
{ "clause": "n <= 100", "kind": "requires", "verdict": "unknown" },
{ "clause": "result >= 0", "kind": "ensures", "basis": "clause-only", "verdict": "unknown" }
]
}
]
}
Each clause’s "verdict" is one of:
"pass"— Z3 proved it; a replayable SMT-LIB2 dump appears as"smtlib2"on the same object."fail"— Z3 refuted it; a"counterexample"appears alongside."unknown"— out of the supported theory subset, solver timeout, or a build without--features z3. The runtime check still guards the clause at execution time; this is not “unverified and unguarded,” it is “not statically proved.”
What an ensures proof actually attests — the "basis" field
An ensures clause carries a "basis" field recording against what
the verdict was established. This is the difference between “the
postcondition is self-consistent” and “the function actually
computes a value the postcondition admits”:
-
"basis": "implementation"— the verifier substituted the function body’s return expression forresultbefore proving, so the obligation is grounded in what the code returns. A"pass"here means the returned value provably satisfies the clause for every admitted input; a"fail"means there is a concrete input (in the"counterexample") that satisfies the preconditions yet returns a value the clause forbids. This is the guarantee that makes the certificate meaningful: a wrongmaxthat returnsxfn max(int x, int y) -> int ensures result >= x && result >= y { return x; }is refuted (
x >= x && x >= yfails fory > x), while the correctif x >= y { return x; } else { return y; }passes — the two no longer verify identically. -
"basis": "clause-only"— the body is outside the substituted subset (only singlereturn E;and singleif/else-of-returns over pure arithmetic/boolean expressions are modelled today), soresultwas left as a free variable. A"pass"here attests only that the clause is a tautology / consistent with the preconditions — not that the body returns a conforming value. Treatclause-onlypasses as “well-formed postcondition,” not “verified implementation,” and lean on the retained runtime check.
Bodies with loops, local let bindings, or function calls in the
return position fall back to clause-only; extending the substituted
subset to richer control flow is tracked as follow-up work.
Unlike --emit-certificate <DIR> (the older RES-071 SMT-LIB2 manifest
- signature format, see
CERTIFICATES.md),--emit-contract-certificateworks in every build configuration — including the default, non-z3 one most CI runners use — precisely because"unknown"is a legitimate, documented verdict rather than a failure.provenanceis informational only: it never changes a verdict, only records that the function carried@ai_generatedand/or#[generated(...)].
Trusting the certificate — schema_version and tamper-evidence
The "schema_version" field (currently 1) is a numeric contract on
the document’s shape: a consumer checks it before parsing anything
else, so a future field addition or reinterpretation fails closed
(a typed error) instead of silently misparsing. Resilient’s own
contract_certificate::verify_schema_version never panics on
malformed or missing input — see resilient/src/contract_certificate.rs.
The JSON document itself carries no cryptographic material by
default — trusting it means trusting the rz binary that produced
it. Under --features z3, contract_certificate::sign_bytes /
verify_signed reuse the same RES-194 Ed25519 primitives as
--emit-certificate/--sign-cert (see
CERTIFICATES.md) to bind a certificate’s exact
bytes to a keypair: any tampering, down to a single flipped bit,
fails verification.
Step 7 — Cheap CI signal: --vibe-gate
If you want a fast, syntactic, no-Z3-required smoke check — “did
anything in this file ship without contracts at all?” — use
--vibe-gate <threshold>. It runs the vibe_debt analyzer (a
lightweight heuristic, not the Tier 1–3 machinery above) and exits
based on a [0.0, 1.0] debt threshold:
$ rz --vibe-gate 0.3 my_module.rz
For each top-level function, vibe_debt scores 4 boolean signals —
has a requires, has an ensures, is referenced elsewhere in the
program, carries a pure/io/@pure effect annotation — and reports
1 - (sum_score / (4 * fn_count)) as the program’s debt percentage.
The gate prints one line of JSON to stderr and sets the exit code:
$ cat contracted.rz
fn add(int x, int y) -> int requires x >= 0 requires y >= 0 ensures result >= 0 {
return x + y;
}
fn main() {
println(add(1, 2));
}
main();
$ rz --vibe-gate 0.8 contracted.rz
{"vibe_debt": 0.50, "threshold": 0.80, "passed": true}
$ echo $?
0
$ rz --vibe-gate 0.01 contracted.rz
{"vibe_debt": 0.50, "threshold": 0.01, "passed": false}
$ echo $?
2
(add scores 2/4 signals — requires and ensures present, but
never called elsewhere and carries no effect annotation — and main
scores 0/4, so the file-wide debt lands at 50%. A // @requires /
// @ensures comment does not count; only the real requires
/ ensures keywords do.)
--vibe-gate=<threshold> (equals form) works too. An out-of-range
(<0.0 or >1.0) or non-numeric threshold exits 2 with a clean
error: --vibe-gate … message — never a panic.
--vibe-gate is a heuristic triage signal, not a soundness proof —
it doesn’t know about @require_contracts enrolment, #[loop_bound],
or Z3 verdicts at all; it only looks at what’s syntactically present.
Use it as an early, cheap gate on a large/legacy codebase where wiring
up full @require_contracts(strict) everywhere isn’t realistic yet;
use Steps 2–6 above for functions where you actually need a proof.
This repository does not wire
--vibe-gateinto its own required CI gates (doing so on a project this size would risk blocking auto-merge on unrelated work) — it’s exercised byresilient/tests/it/vibe_gate.rsand documented here so downstream projects can adopt it for their own CI.
Putting it all together
@require_contracts(strict)
#[loop_bound(100)]
@ai_generated
fn count_up(int n) -> int requires n >= 0 requires n <= 100 ensures result >= 0 {
let i = 0;
while (i < n) {
i = i + 1;
}
return i;
}
fn main() {
println(count_up(0));
println(count_up(5));
println(count_up(10));
}
main();
# Fail the build on any contract-policy or loop-bound violation:
rz --typecheck-strict loop_bound_demo.rz
# Attempt real Z3 proofs instead of "unknown" placeholders:
cargo build --features z3 --manifest-path resilient/Cargo.toml
./resilient/target/debug/rz --typecheck-strict loop_bound_demo.rz
# Emit a portable, offline-checkable proof artifact:
rz loop_bound_demo.rz --emit-contract-certificate cert.json
# Cheap heuristic pre-check on a large tree before you wire up contracts everywhere:
rz --vibe-gate 0.5 loop_bound_demo.rz
This exact program (modulo comments) is resilient/examples/loop_bound_demo.rz;
its golden output lives in the sibling .expected.txt, and
resilient/tests/it/contract_certificate_e2e_smoke.rs exercises the
--emit-contract-certificate step end-to-end (asserting certificate
structure, since verdicts are "unknown" without --features z3).
Related reading
AI_GENERATED_DESIGN.md— the design history of why provenance and enforcement were split apart.VERIFICATION_MODEL.md— what the Z3 verifier proves and how.VERIFICATION_LIMITS.md— the boundary of what today’s prover can and cannot discharge.CERTIFICATES.md— the older--emit-certificateSMT-LIB2 manifest + signature format, for comparison.AI_THREAT_MODEL.md—--ai-threats/#[ai_review_required], a complementary static-pattern scanner for AI-introduced security anti-patterns (a different concern from contract correctness).