Project 2 represented a FIFO with a circular buffer. We now realize that representation
in native memory, with dequeue implemented by encoded RISC instructions and enqueue
reusing the verified cell-write mechanism from bounded append. We will connect the result
to PL/OS's runnable-queue interface, including the host policies that surround the bounded ring.
The starter source
pins the same PL/OS revision as Projects 7 and 8. The append routine, generic memory lemmas,
loader, and host queue policy are supplied. You implement dequeue and the persistent
pop/push adapters, then prove their representation and interface contracts. No earlier
student solution is required.
Build and Explore
From the repository root:
cd projects/risc-queue
lake build
Open this directory as a Lean project. It uses Lean 4.27.0 and downloads the pinned dependencies.
RiscQueue/Spec.lean: standalone dequeue inputs and results.
RiscQueue/Program.lean: the assembly body to implement.
RiscQueue/View.lean: physical slots, logical FIFO view, and supplied arithmetic lemmas.
RiscQueue/Image.lean: the verified loader for student dequeue code, supplied append, and data.
RiscQueue/Table.lean: persistent ring state, validity, initialization, and readable-cell lemmas.
RiscQueue/Calls.lean: argument layouts and return markers.
RiscQueue/Native.lean: execution wrappers and the two adapter placeholders.
RiscQueue/Writer.lean: the supplied proof that append writes one selected physical cell.
RiscQueue/API.lean: the contracts required of a verified ring.
RiscQueue/Policy.lean: supplied admission, growth, duplicate suppression, and removal policies.
RiscQueue/Fixtures.lean, Checks.lean, and Audit.lean: examples, checks, and assumption reports.
You may use upstream Append, Memory, TableUpdate, and Kernel.Components. Do not
import the completed RingPop, RingView, RingImage, Ring, or Kernel.NativeQueue.
The audit rejects the completed dequeue and native ring/queue implementations.
Separate Mechanism and Policy
The bounded ring stores word-sized values in FIFO order. It permits duplicates and has
fixed positive capacity. Dequeue reads one cell and changes metadata; enqueue writes an
unused tail cell. Neither operation needs to clear cells that are no longer active.
The kernel interface is broader. PIDs are natural numbers, enqueue suppresses an existing
PID, full queues can grow, and removal deletes every occurrence. The supplied host policy
handles those cases around the verified bounded mechanism. This separation lets us reuse
small native routines without pretending the entire scheduler runs as RISC code.
Describe Physical and Logical State
/-- One wrap suffices for an active offset below capacity and a valid head. -/defslot(headcapacityoffset:Nat):Nat:=ifhead+offset<capacitythenhead+offsetelsehead+offset-capacitydefwords(cells:ListWord)(headcountcapacity:Nat):ListWord:=letsnapshot:=cells.toArray(List.rangecount).mapfuni=>snapshot[slotheadcapacityi]!
head names the first active physical cell; count gives the number of active entries.
slot head capacity offset maps a logical offset to a physical index. One subtraction
suffices when both the head and offset are below capacity. The active sequence may cross
the end of the physical array.
For example, cells [10, 20, 30, 40], head 3, and count 2 represent [40, 10]. After
popping 40, cell 3 still contains 40 but is inactive. A later push may reuse it. Stale
values outside the active sequence do not affect FIFO behavior; this is not a data-erasure guarantee.
Prove these two properties before reasoning about assembly. ViewPop shifts the logical
head. ViewPush changes exactly the unused tail cell and extends the logical sequence.
Use the supplied slot bounds, injectivity, zero-offset, and next-head lemmas.
Injectivity explains why a write at logical offset count cannot overwrite an existing
active element when there is room. The full case is different: that offset would name
the head again. A caller must establish room before pushing.
On entry, a0 holds the buffer address, a1 the head, a2 the count, and a3 the capacity.
On return, a0 is the dequeued value, a1 the next head, a2 the next count, and a3 a
found flag. You may use a4 as scratch. Return through ra.
/-- Dequeue returns a value and new metadata, with no memory writes. -/defCorrect(headcountcapacity:Nat)(valueret:Word)(before:State)(result:CallResult):Prop:=result.stop=.returned∧result.state.pc=ret∧result.retired≤16∧result.state.memory=before.memory∧result.state.mode=before.mode∧(∀r:Register,r≠10→r≠11→r≠12→r≠13→r≠14→result.state.readRegr=before.readRegr)∧ifcount=0thenresult.state.readReg10=0∧(result.state.readReg11).toNat=head∧result.state.readReg12=0∧result.state.readReg13=0elseresult.state.readReg10=value∧(result.state.readReg11).toNat=nextHeadheadcapacity∧(result.state.readReg12).toNat=count-1∧result.state.readReg13=1
On an empty dequeue, return zero and a false flag, retaining the head and zero count.
Do not access the data buffer. On a nonempty dequeue, read the old head cell, advance
the head with conditional wraparound, decrement the count, and return a true flag.
Zero is a valid stored value, so it cannot serve as the absence flag by itself.
Both cases preserve all memory, the privilege mode, and every register outside a0–a4.
The instruction budget is an upper bound; your implementation need not match the reference
encoding or its exact counts.
Only a nonempty call needs a readable head cell. The capacity and metadata bounds justify
converting natural numbers to words and interpreting the results back as natural numbers.
The installed code and external return marker justify execution through the real fetch
and decode path. The standalone theorem permits arbitrary valid code locations.
Write and Prove the Encoded Routine
/-- Implement dequeue: a0 = buffer, a1 = head, a2 = count, a3 = capacity.
Return a0 = value (zero if empty), a1 = new head, a2 = new count, a3 = found.
Only a0–a4 may change. Empty dequeue must not access the data buffer. -/defprogram:ListAssembler.Item:=[]defcode:ArrayInstructionWord:=(Assembler.assembleprogram).toOption.get(⊢ (Assembler.assembleprogram).toOption.isSome=trueAll goals completed! 🐙)/-- The supplied persistent layout reserves 24 words for dequeue. -/theoremcode_bound:code.size≤24:=⊢ code.size≤24All goals completed! 🐙
Implement the empty, ordinary nonempty, and wrapping nonempty paths. Use symbolic labels,
unsigned comparisons, and the model's load and arithmetic instructions. The supplied
persistent loader reserves at most 24 instruction words; update the checked assembly proof
and retain the size theorem as you develop your routine.
Prove RunCorrect and CallCorrect in your solution module. Use CodeAt.step, terminating
Returns traces, and Returns.run to connect the encoded words to bounded execution.
Support.not_return excludes premature return at instruction positions. Treat the taken
wrap branch and the untaken branch explicitly, including the arithmetic bounds.
The empty path should not need a cell-load hypothesis. Its correctness still includes
termination and the full memory and register frames. Tests with an unreadable, misaligned
buffer address help expose implementations that touch data before checking emptiness.
Maintain a Persistent Ring
/-- Persistent runtime state, with no shadow list of logical queue entries. -/structureRingwherememory:Memorycapacity:Nathead:Natcount:NatderivingDecidableEqnamespaceRingdefcells(ring:Ring):ListWord:=(List.rangering.capacity).mapfuni=>BitVec.ofNat64((ring.memory.load.user.read(256+BitVec.ofNat64(8*i)).double).toOption.getD0)defwords(ring:Ring):ListWord:=View.wordsring.cellsring.headring.countring.capacitydefview(ring:Ring):ListNat:=ring.words.mapBitVec.toNatstructureValid(ring:Ring):PropwhereheadBound:ring.head<ring.capacitycountBound:ring.count≤ring.capacitybase:ring.memory.base=0size:ring.memory.bytes.size=256+8*ring.capacityregions:ring.memory.regions=Image.regionscodering.capacityfits:256+8*ring.capacity≤2^64popCode:CodeAtring.memory.user0codeappendCode:Append.Codering.memory.user128represented:∃cells:ListWord,cells.length=ring.capacity∧Lookup.Tablering.memory.user256cells
The runtime state contains bytes and metadata, without a shadow logical queue. cells
decodes all physical slots; words selects the active FIFO; view converts those words
to natural-number PIDs. These views support specifications and host policies, but native
pop and push must execute against the retained image directly.
Valid connects the state to installed code, bounded metadata, memory permissions, and
a readable physical table. The existential cell sequence is proof-only. It covers the
entire capacity, including inactive cells, so every future tail slot is available.
Initialization pads unused cells with zero and starts at head zero. The supplied
init_words theorem establishes its logical view. Other supplied lemmas expose cell
loads, physical slot permissions, and nonwrapping addresses.
The fixed layout places dequeue at 0, append at 128, and data at 256. Both routines return
at marker 192. Code is executable but not writable; data is writable but not executable.
Reuse Append as a Cell Writer
defpopResult(ring:Ring):CallResult:=resumeCall19216ring.popPrepareddefwritePrepared(ring:Ring)(index:Nat)(value:Word):State:=((ring.writeCallindexvalue).prepare{memory:=ring.memory}).toOption.get(byring:Ringindex:Natvalue:Word⊢ ((ring.writeCallindexvalue).prepare{memory:=ring.memory}).toOption.isSome=truesimp[writeCall,Append.invocation,Call.prepare]ring:Ringindex:Natvalue:Word⊢ (Except.ok{registers:=RegisterFile.ofFnfunr=>if↑r=1then192#64elseif↑r=2then0#64elseifh:10≤↑r∧↑r<18thenif↑r-10=0then256#64elseif↑r-10=1thenBitVec.ofNat64indexelseif↑r-10=2thenBitVec.ofNat64ring.capacityelseif↑r-10=3thenvalueelse0#64else0#64,pc:=128#64,memory:=ring.memory}).toOption.isSome=true;rflAll goals completed! 🐙)/-- Bounded append writes one physical cell, with the physical index passed as its length. -/defwriteResult(ring:Ring)(index:Nat)(value:Word):CallResult:=resumeCall192Append.allowance(ring.writePreparedindexvalue)
The cell writer passes a physical index as append's length and the physical capacity as
its limit. It uses the readable prefix cells.take index to invoke append's contract.
The supplied Writer proofs establish return and an exact store effect for any valid
physical index. Lookup.Table.set transfers that effect to the complete physical table.
The writer's returned length is index + 1. It is not the ring's new logical count.
Your push adapter must retain the old head and set count to the old count plus one.
This is a case where reusing a verified component requires interpreting its interface
under a different representation.
/-- Implement using the encoded dequeue result's value, head, and count. -/defpop(ring:Ring):Word×Ring:=(0,ring)/-- Implement using the verified write at the unused tail slot; retain its resulting memory. -/defpush(ring:Ring)(_value:Word):Ring:=ring
Implement both adapters. Pop takes the value, head, and count from the encoded dequeue
result, retaining capacity and memory. Push computes the unused tail slot, invokes the
supplied writer, and retains its resulting memory. Neither may rebuild an image from
words or silently fall back to a list implementation.
Prove NativePopCorrect and NativePushCorrect. For pop, use your execution theorem,
memory preservation, and ViewPop. For push, use write_effect, Lookup.Table.set,
CodeAt.store64, and ViewPush. Preserve both installed code images after the write.
Establish the concrete memory and metadata equalities as well as the logical FIFO result.
Build the Verified Interface
/-- A proof bundle to construct from student theorems, never an assumed axiom. -/structureCorrectRing:Propwherepop:∀ring:Ring,ring.Valid→0<ring.count→PopCorrectringpush:∀(ring:Ring)(value:Word),ring.Valid→ring.count<ring.capacity→PushCorrectringvalue
Construct a theorem named verifiedRing : CorrectRing from your actual pop and push
proofs. This bundle is a proposition: it supplies proof evidence to the host adapter,
not another runtime implementation or an assumed API. The unfinished starter does not
provide an inhabitant of this proposition.
The supplied policy carries validity evidence with each native ring. It can therefore
construct its next native state using the preservation fields of verifiedRing.
This keeps its kernel interface defined on every constructible queue value, without
adding a validity premise to every kernel queue operation.
Refine the Runnable-Queue Policy
/-- Native states carry validity evidence; fallback preserves arbitrary natural-number PIDs. -/inductiveQueuewhere|native(ring:Ring)(valid:ring.Valid)|fallback(pids:ListNat)defview:Queue→ListNat|.nativering_=>ring.view|.fallbackpids=>pids/-- Admission and growth occur on the host; no unrepresentable PID is silently truncated. -/deffromList(pids:ListNat):Queue:=letcapacity:=max4(2*pids.length)iffits:256+8*capacity≤2^64∧pids.all(funpid=>decide(pid<2^64))=truethen.native(Ring.init(pids.map(BitVec.ofNat64))capacity)(Ring.init_valid__(bypids:ListNatcapacity:Nat:=max4(2*pids.length)fits:256+8*capacity≤2^64∧(pids.allfunpid=>decide(pid<2^64))=true⊢ 0<capacityomegaAll goals completed! 🐙)(bypids:ListNatcapacity:Nat:=max4(2*pids.length)fits:256+8*capacity≤2^64∧(pids.allfunpid=>decide(pid<2^64))=true⊢ (List.map(BitVec.ofNat64)pids).length≤capacitysimponly[List.length_map]pids:ListNatcapacity:Nat:=max4(2*pids.length)fits:256+8*capacity≤2^64∧(pids.allfunpid=>decide(pid<2^64))=true⊢ pids.length≤capacity;omegaAll goals completed! 🐙)fits.1)else.fallbackpids
Admission chooses native storage when the image size and all PIDs fit, and otherwise
retains an exact list fallback. Test representability before conversion to a word.
An oversized natural-number PID must not wrap to another PID.
The duplicate check comes before reserve or growth: enqueueing an existing PID retains
the exact queue value, including a full native buffer. A fresh word-sized PID in a
nonfull native ring uses the verified tail write. Growth is a host operation that
linearizes the logical FIFO into a larger image.
The host checks count for an empty native queue. A nonempty native dequeue uses the
value and state returned by your pop adapter. Removal is also host policy: filter the
logical view and rebuild a checked image. It removes all occurrences.
Validity does not require uniqueness. A queue admitted from [1, 2, 1] must retain that
meaning, dequeue in that order, and remove both ones when asked. Duplicate suppression
prevents adding another occurrence; it does not repair preexisting duplicates. Any stronger
uniqueness claim belongs in a separate whole-kernel invariant.
Prove admission preserves arbitrary lists, then derive the queue laws through the
ring's logical-view contracts. Construct the actual Kernel.Components.QueueContract
with Policy.view as its interpretation. Keep this argument at the component level;
do not replay instruction traces to prove enqueue or dequeue policy correctness.
defNativeUse:Prop:=∀correct:CorrectRing,(∀(queue:Policy.Queue)(pid:Nat),Plos.Kernel.RunQueue.contains⟨Policy.viewqueue⟩pid=true→Policy.enqueuecorrectqueuepid=queue)∧(∀(ring:Ring)(valid:ring.Valid)(pid:Nat),Plos.Kernel.RunQueue.contains⟨ring.view⟩pid=false→∀(room:ring.count<ring.capacity),pid<2^64→Policy.enqueuecorrect(.nativeringvalid)pid=.native(ring.push(BitVec.ofNat64pid))(correct.pushring_validroom).valid)∧(∀(ring:Ring)(valid:ring.Valid)(nonempty:0<ring.count),Policy.dequeuecorrect(.nativeringvalid)=some(ring.pop.1.toNat,.nativering.pop.2(correct.popringvalidnonempty).valid))/-- Exhibit the completed ring proof and its concrete kernel contract; do not assume the API. -/defIntegrated:Prop:=∃correct:CorrectRing,∃contract:Plos.Kernel.Components.QueueContract(Policy.opscorrect),contract.view=Policy.view
Prove the native-use properties as well as abstract refinement. They record exact duplicate
reuse and the native paths taken for fresh enqueue and nonempty dequeue. Integrated
requires you to exhibit both the completed ring proof and its kernel contract. A theorem
that merely assumes CorrectRing is useful for composition, but is not a complete submission.
The queue contract is the interface consumed by PL/OS's generic storage-replacement proof.
Explain how it lets the kernel substitute this queue while retaining reference behavior,
and what additional theorem application would establish equivalence of whole executions.
Constructing the component contract alone is not that whole-execution theorem.
Check Executions and Sequences
After implementing dequeue and the two adapters:
lake exe check-queue
This command intentionally fails on the starter. It checks empty and nonempty calls,
wraparound, capacity one, zero and maximum-word values, relocation, frames, and all fuel
splits within the budget. Ring checks start with wrapped storage, pop an entry, reuse
inactive cells with pushes, fill the ring, and empty it again. They compare adapter
results to native execution and the logical FIFO after updates.
After constructing verifiedRing, include this in your solution module:
The policy checks exercise duplicates, removal of every matching PID, growth, FIFO order,
emptying, and fallback for an oversized PID. They require your proof bundle so the supplied
policy can construct valid native states. The ordinary starter build checks the loader,
cell writer, views, and admission examples; it does not complete the student obligations.
Add a regression check for a bug or plausible incorrect implementation you encountered.
Explain a trace that crosses the physical wrap boundary and why its logical order remains FIFO.
Submit and Explain
Implement program, Ring.pop, and Ring.push. Create RiscQueue/Solution.lean, importing
RiscQueue.Fixtures, with proofs of ViewPop, ViewPush, RunCorrect, CallCorrect,
NativePopCorrect, NativePushCorrect, AdmissionCorrect, PolicyCorrect, KernelContract,
NativeUse, and Integrated, together with your constructed verifiedRing bundle.
Import the solution from Checks.lean and add its theorem-assumption reports to Audit.lean.
lake build
lake exe check-queue
lake env lean Audit.lean
Keep the supplied contracts, execution wrappers, writer, and policy fixed. Do not use
new axioms, sorry, or admit. Explain the physical/logical distinction, reuse of append,
preservation of code and inactive cells, and each boundary between native mechanism and host
policy. Disclose reference material and describe one agent-generated error you diagnosed
without weakening the intended claim.
These guarantees concern the supplied sequential model under exclusive access. They do
not prove concurrent queue safety, interrupt handling, hardware correspondence, scheduler
fairness, or erasure of inactive data.
Optional Extensions
Instantiate PL/OS's generic storage-replacement theorem with the completed queue contract
and standard other components. State the related initial states and bounded execution
budgets explicitly, then prove whole-kernel execution refinement.
Prove the ring laws for finite mixed operation sequences using the component contracts.
Strengthen the empty-call theorem to remove unnecessary metadata bounds, while keeping
the encoded input interpretation precise and requiring no readable buffer.
Implement removal or duplicate detection as a verified RISC loop, replacing one host
policy step while preserving the same kernel contract.