feat(L06): add control flow and program counter card

This commit is contained in:
user
2026-07-21 17:09:05 +02:00
commit 3eb73ea715
42 changed files with 7212 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import test from 'node:test';
const source=JSON.parse(await readFile(new URL('../json/card_source.json',import.meta.url),'utf8'));
test('L06 identity and three tasks are stable',()=>{assert.equal(source.card.number,'06');assert.equal(source.card.uuid,'20d3f96a-8140-59d7-b909-c6244d4555c5');assert.deepEqual(source.tasks_order,['task01','task02','task03']);});
test('branch, immediate, PC and waveform evidence are explicit',()=>{const text=JSON.stringify(source);for(const token of ['signed','unsigned','immediate','pc=','VCD'])assert.match(text,new RegExp(token,'i'));for(const task of Object.values(source.tasks))assert.equal(task.assessment_criterion_ref,'RV06.KW01');});
+67
View File
@@ -0,0 +1,67 @@
import assert from 'node:assert/strict';
import { mkdtemp, rm } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { CardStateDatabase } from '../scripts/lib/card_state_db.mjs';
test('card state survives closing and reopening SQLite', async () => {
const directory = await mkdtemp(path.join(os.tmpdir(), 'stem-card-state-'));
const file = path.join(directory, 'state.sqlite3');
const state = {
schema: 'stem-card-navigation.v2',
revision: 7,
selected_at: '2026-07-17T12:00:00.000Z',
snapshot_ref: 'task04.alloc5.cursor'
};
try {
const first = new CardStateDatabase(file);
first.write('navigation', state);
first.close();
const second = new CardStateDatabase(file);
assert.deepEqual(second.read('navigation'), state);
assert.equal(second.read('missing'), null);
second.close();
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test('audit log is append-only, ordered and keeps navigation timestamps', async () => {
const directory = await mkdtemp(path.join(os.tmpdir(), 'stem-card-audit-'));
const file = path.join(directory, 'state.sqlite3');
try {
const database = new CardStateDatabase(file);
const navigation = {
task: { id: 'task04' },
block: { id: 'allocator-flow' },
phase: { id: 'alloc5' },
step: { id: 'alloc5-commit' },
snapshot_ref: 'task04.alloc5.commit'
};
const first = database.appendEvent({
eventType: 'navigation.activate',
actor: 'nvim',
navigation,
occurredAt: '2026-07-17T12:00:00.000Z',
payload: { source: 'F2' }
});
const second = database.appendEvent({
eventType: 'checkpoint.ready',
actor: 'nvim',
navigation,
occurredAt: '2026-07-17T12:00:01.000Z'
});
assert.equal(second.id, first.id + 1);
assert.deepEqual(
database.listEvents({ afterId: first.id, limit: 10 }).map(event => event.event_type),
['checkpoint.ready']
);
assert.equal(database.listEvents()[0].snapshot_ref, navigation.snapshot_ref);
database.close();
} finally {
await rm(directory, { recursive: true, force: true });
}
});
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env python3
import sys
def s32(value):
value &= 0xFFFFFFFF
return value - 0x100000000 if value & 0x80000000 else value
def task01():
lhs, rhs = 0xFFFFFFFF, 1
values = [lhs == rhs, lhs != rhs, s32(lhs) < s32(rhs), s32(lhs) >= s32(rhs), lhs < rhs, lhs >= rhs]
assert values == [False, True, True, False, False, True]
print("PASS task01 conditions=6 signed_lt=1 unsigned_lt=0 illegal=1")
def task02():
assert (0x100 + 12) & 0xFFFFFFFF == 0x10C
assert (0x100 - 8) & 0xFFFFFFFF == 0x0F8
print("PASS task02 branch_imm=12 branch_target=0000010c jal_imm=-8 jal_target=000000f8")
def task03():
sequence = [0, 4, 8, 0x20, 0x24, 0x10, 0x14]
assert sequence == [0, 4, 8, 32, 36, 16, 20]
print("PASS task03 pc=00000000,00000004,00000008,00000020,00000024,00000010,00000014 branches=1 jumps=1")
TASKS = {"task01_branch_compare": task01, "task02_control_immediate": task02, "task03_program_counter": task03}
if len(sys.argv) != 2 or sys.argv[1] not in TASKS:
raise SystemExit(f"usage: {sys.argv[0]} {'|'.join(TASKS)}")
TASKS[sys.argv[1]]()
+22
View File
@@ -0,0 +1,22 @@
module tb;
logic [31:0] lhs, rhs;
logic [2:0] funct3;
logic taken, illegal;
integer checks = 0;
string trace_file;
branch_compare dut (.*);
task automatic check(input logic [2:0] function_code, input logic expected);
funct3 = function_code; #1;
if (taken !== expected || illegal) $fatal(1, "FAIL f=%b taken=%b illegal=%b", function_code, taken, illegal);
checks += 1;
endtask
initial begin
if ($value$plusargs("trace=%s", trace_file)) begin $dumpfile(trace_file); $dumpvars(0, tb); end
lhs = 32'hffffffff; rhs = 1;
check(0,0); check(1,1); check(4,1); check(5,0); check(6,0); check(7,1);
funct3 = 2; #1;
if (!illegal || taken) $fatal(1, "FAIL illegal");
$display("PASS task01 conditions=%0d signed_lt=1 unsigned_lt=0 illegal=1", checks);
$finish;
end
endmodule
+22
View File
@@ -0,0 +1,22 @@
module tb;
logic [31:0] instruction, pc, target;
logic format_j;
logic signed [31:0] immediate;
string trace_file;
control_immediate dut (.*);
function automatic [31:0] encode_b(input logic signed [12:0] offset);
encode_b = {offset[12], offset[10:5], 5'd0, 5'd0, 3'b000, offset[4:1], offset[11], 7'h63};
endfunction
function automatic [31:0] encode_j(input logic signed [20:0] offset);
encode_j = {offset[20], offset[10:1], offset[11], offset[19:12], 5'd1, 7'h6f};
endfunction
initial begin
if ($value$plusargs("trace=%s", trace_file)) begin $dumpfile(trace_file); $dumpvars(0, tb); end
pc = 32'h100; format_j = 0; instruction = encode_b(13'sd12); #1;
if (immediate !== 12 || target !== 32'h10c) $fatal(1, "FAIL branch imm=%0d target=%h", immediate, target);
format_j = 1; instruction = encode_j(-21'sd8); #1;
if (immediate !== -8 || target !== 32'h0f8) $fatal(1, "FAIL jal imm=%0d target=%h", immediate, target);
$display("PASS task02 branch_imm=12 branch_target=0000010c jal_imm=-8 jal_target=000000f8");
$finish;
end
endmodule
+23
View File
@@ -0,0 +1,23 @@
module tb;
logic clk = 0, rst = 1, jump = 0, branch_taken = 0;
logic [31:0] target = 0, pc;
logic [31:0] expected [0:6] = '{32'h0,32'h4,32'h8,32'h20,32'h24,32'h10,32'h14};
string trace_file;
program_counter dut (.*);
always #1 clk = ~clk;
task automatic tick_and_check(input integer index);
@(posedge clk); #1;
if (pc !== expected[index]) $fatal(1, "FAIL index=%0d pc=%h expected=%h", index, pc, expected[index]);
endtask
initial begin
if ($value$plusargs("trace=%s", trace_file)) begin $dumpfile(trace_file); $dumpvars(0, tb); end
tick_and_check(0); @(negedge clk); rst = 0;
tick_and_check(1); tick_and_check(2);
@(negedge clk); branch_taken = 1; target = 32'h20; tick_and_check(3);
@(negedge clk); branch_taken = 0; tick_and_check(4);
@(negedge clk); jump = 1; target = 32'h10; tick_and_check(5);
@(negedge clk); jump = 0; tick_and_check(6);
$display("PASS task03 pc=00000000,00000004,00000008,00000020,00000024,00000010,00000014 branches=1 jumps=1");
$finish;
end
endmodule
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
mode=test
if [[ "${1:-}" == "--build-only" || "${1:-}" == "--trace" ]]; then mode="${1#--}"; shift; fi
task="${1:?task name is required}"
case "$task" in task01_branch_compare|task02_control_immediate|task03_program_counter) ;; *) printf 'Unknown RTL task: %s\n' "$task" >&2; exit 2 ;; esac
state="${STEM_STATE_DIR:-$root/.stem/instances/local}"
profile="${STEM_PROFILE:-hazard3-sim}"
target="${STEM_TARGET:-hazard3-baremetal}"
build_dir="$state/build/rtl/$task"
artifact_dir="$root/.stem/artifacts/$profile/$target/$task"
binary="$build_dir/sim"
mkdir -p "$build_dir" "$artifact_dir"
verilator --binary --timing --assert --trace -Wall -Wno-fatal -Wno-DECLFILENAME -Wno-BLKSEQ -Wno-UNUSEDSIGNAL --top-module tb --Mdir "$build_dir/obj" -o "$binary" "$root/src/tasks/$task.sv" "$root/tests/${task}_tb.sv" >/dev/null
cp -f "$root/src/tasks/$task.sv" "$artifact_dir/"
[[ "$mode" == build-only ]] && { printf 'BUILD %s binary=%s\n' "$task" "$binary"; exit 0; }
trace_file="$artifact_dir/$task.vcd"
if [[ "$mode" == trace ]]; then "$binary" "+trace=$trace_file"; test -s "$trace_file"; printf 'TRACE %s vcd=%s\n' "$task" "$trace_file"; else "$binary"; fi