The first two projects reason about functions that operate on data structures. This project
introduces programs whose behavior is a sequence of machine instructions. We will specify
individual steps, connect an executable interpreter to that specification, and verify a
loop with a precise result, termination argument, and register-frame property.
The starter source is
self-contained. You do not need solutions to the earlier projects.
Build and Explore
From the repository root:
cd projects/machine
lake build
Open this directory as a Lean project. The modules divide the work as follows:
MiniRisc/Model.lean: words, registers, programs, fetching, instruction execution, and a runner.
MiniRisc/Semantics.lean: successful instruction rules, fault rules, and finite executions.
MiniRisc/Countdown.lean: the small program we will verify.
MiniRisc/Exercises.lean: the correctness statements students must prove.
MiniRisc/Support.lean: a supplied arithmetic lemma for the termination argument.
Checks.lean: concrete checks for arithmetic, branches, fetch faults, fuel, and resumption.
The supplied executable definitions and checks build. Most correctness properties are
exercise propositions, not supplied theorems. A passing starter build does not complete them.
There are 32 registers, each holding a 64-bit word, and a program counter pc. A Register
is a bounded index: Fin 32 prevents referring to a nonexistent register. Register zero
always reads as zero and ignores writes, even if its backing vector slot contains another
value. Distinguish architectural reads through readReg from inspecting the vector directly.
Words use arithmetic modulo 2^64. For example, adding one to the greatest unsigned word
produces zero. This behavior also applies to program-counter arithmetic. It must appear in
our reasoning; it is not the arithmetic of unbounded natural numbers.
The starter supplies read_zero and write_zero. Your first proofs will show that a write
to a nonzero register reads back the new value and preserves all other registers.
add adds two registers; addi adds a signed 12-bit immediate. beq and bne branch on
equality and inequality using a signed 13-bit byte offset. The target is relative to the
current PC. Otherwise execution proceeds to pc + 4. ebreak requests a breakpoint trap.
It does not retire as a successful instruction.
signed converts an immediate to its signed integer interpretation, then represents that
integer as a 64-bit word. Thus a 12-bit immediate encoding minus one acts as a decrement,
rather than an addition of the unsigned number 4095.
Each array element occupies one four-byte instruction position, beginning at base.
Program.Valid describes an aligned image that does not cross the address-space boundary.
fetch checks the PC's alignment and whole-instruction bounds before selecting an entry.
Falling past the image is a fault, not successful completion.
The array contains decoded instructions, not binary encodings. Instruction storage is immutable
and there is no data memory yet. This lets us focus on control flow and register state before
introducing byte decoding, load/store behavior, and permissions.
Specify Successful and Faulting Steps
The relation Exec instruction state next specifies successful instruction execution.
Its rules compute arithmetic results and make control-flow conditions explicit:
There are separate rules for taken and untaken branches. A taken target must be four-byte
aligned. An untaken target is not checked. Register values used as operands come from the
pre-state, including when the destination register is also a source.
ExecFault specifies instruction faults. Faulted adds fetch failures and identifies the
instruction when fetching succeeds. No new state is returned by a fault: the runner must
retain the complete pre-instruction state. This project treats fetch as part of the supplied
program-image model; the relation does not independently derive byte-level instruction fetching.
The single-step and finite-execution relations connect instructions into program behavior:
Step program state next requires both a successful fetch and a permitted instruction step.
Steps program count state final describes exactly count successful steps. Its reflexive
case performs zero steps; its inductive case adds one step before the rest of the execution.
A trap is not an extra successful step.
Compute an Instruction Step
The executable implementation follows the specification's cases:
jump accepts an aligned target and changes only the PC, or returns a misalignment fault.
step fetches before executing; calling execute directly bypasses fetch checks. In particular,
testing an instruction with execute does not show that a program can fetch it at a given PC.
Students will prove both success equivalences and fault equivalences. Proving only that
successful executions are correct would leave room for an interpreter that reports incorrect
faults instead of carrying out valid instructions.
Specify and Implement Bounded Execution
We want a runner's result to explain both the execution that occurred and why it stopped:
If execution is suspended, every unit of fuel retired an instruction. If it traps, there is
a specified fault at the returned state, and the number of retired instructions is strictly
less than the fuel allowance. In both cases the returned state is reachable by the reported
number of successful steps.
Fuel counts successfully retired instructions. Zero fuel suspends before fetching, even if
the current PC would fault. A failed attempt consumes no retired instruction and leaves the
state at the faulting PC. Exhausting fuel does not establish that the program has finished.
The saved state can be passed to another run to continue execution.
The four instruction addresses are 0, 4, 8, and 12:
At 0, if register 10 is zero, branch to the breakpoint at 12.
At 4, decrement register 10.
At 8, take the always-true equality branch back to 0.
At 12, request a breakpoint.
The zero case exits before decrementing, which matters for unsigned arithmetic: decrementing
zero would wrap to 2^64 - 1. Each nonzero loop iteration retires three instructions, and
the final taken exit branch retires one. Attempting the breakpoint requires one additional
unit of fuel but does not retire another instruction.
The first run reaches the exit marker but has no fuel left to observe the breakpoint.
The second run observes it. These are different outcomes, even though both runs reach a
state with a zero counter at PC 12.
The theorem ranges over arbitrary initial register files with PC zero, not just start.
It specifies the stop reason, final PC, result, exact number of retired instructions, and
preservation of every other architectural register. An immediate trap, a zero-fuel suspension,
or an interpreter that simply clears the counter would not satisfy it.
To prove it, reason about executions beginning at the loop header. An iteration with a
positive counter returns to the header with its unsigned value decreased by one. The zero
case reaches the exit marker without another decrement. Induction on the unsigned counter
value supplies both the result and the finite execution bound.
The starter supplies an arithmetic bridge in Course.MiniRisc.Support:
Use it after establishing that the counter is positive. You will also need to relate adding
the sign-extended minus-one immediate to subtracting one. Explain why the positivity
hypothesis is essential. A bitvector alone is not a decreasing natural-number measure.
Core Assignment
Register behavior. Prove Exercises.RegisterLaws. Include aliasing of source and
destination registers when reasoning about arithmetic instructions.
Instruction conformance. Prove Exercises.ExecuteCorrect and
Exercises.ExecuteFaultCorrect. Treat taken and untaken branches separately.
Whole-step conformance. Prove Exercises.StepCorrect and Exercises.StepFaultCorrect,
connecting fetch and instruction execution to Step and Faulted.
Executions and stopping. Prove Exercises.RunCorrect by induction on fuel. Explain
why zero fuel does not imply that the next instruction is safe, and why a trapped
instruction is absent from the successful-step count.
A loop with a result. Prove Exercises.CountdownCorrect. First establish a theorem
for one nonzero iteration and a theorem for the zero case. Use an invariant at the loop
header and an induction on the counter's unsigned value. Include the register-frame
guarantee and account for the final breakpoint attempt.
Create MiniRisc/Solution.lean, import MiniRisc.Exercises and MiniRisc.Support, and import
your solution from Checks.lean. Keep the supplied definitions and exercise propositions
fixed. Audit your theorem dependencies using #print axioms, as demonstrated in Audit.lean.
Do not discharge obligations with new axioms or proof placeholders.
Submit the proofs with an explanation of the loop invariant, decreasing measure, fuel bound,
and trusted model. Include a trace for a small input and an explanation of why suspension
and trapping are different. Use an agent to explore proofs one lemma at a time; require it
to state which PC and counter relationships its induction hypothesis preserves.
Optional Extensions
Prove Exercises.Resumption: splitting a run at a suspension point agrees with one
longer run, including the total number of retired instructions. Explain why the hypothesis
says the first run suspended rather than trapped.
Replace the loop's conditional structure with a bne-based version. Specify its new
instruction count and prove that it computes the same counter result with the same frame.
Relocate the program to an aligned nonzero base. State and prove the bounds needed to
connect the relocated execution to the original. Do not assume PC addition cannot wrap.
What the Model Does and Does Not Establish
This project models sequential register operations and control flow. It does not model
data memory, binary decoding, interrupts, privilege modes, or hardware trap handling.
Arithmetic wraps at 64 bits; taken branch targets and fetch addresses require four-byte
alignment. These are deliberate instruction-level conventions, not a proof of agreement
with a physical processor.
The runner always terminates because its fuel decreases. That fact does not establish
termination of the simulated program. The countdown proof must show that a sufficient bound
reaches the specified exit, without an earlier fault. This distinction between a terminating
interpreter and a terminating guest program is central to the project.