feat(L08): add subroutines and ABI card

This commit is contained in:
user
2026-07-21 17:19:51 +02:00
commit ea50415a5e
42 changed files with 7250 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('L08 identity and three tasks are stable',()=>{assert.equal(source.card.number,'08');assert.equal(source.card.uuid,'c6b74348-02ea-56ae-bea1-357935bf1b49');assert.deepEqual(source.tasks_order,['task01','task02','task03']);});
test('link, ABI, LIFO and waveform evidence are explicit',()=>{const text=JSON.stringify(source);for(const token of ['JALR','ABI','LIFO','underflow','VCD'])assert.match(text,new RegExp(token,'i'));for(const task of Object.values(source.tasks))assert.equal(task.assessment_criterion_ref,'RV08.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 });
}
});
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
import sys
def task01():
pc = 0x100
assert pc + 24 == 0x118 and pc + 4 == 0x104
assert ((0x205 - 4) & ~1) == 0x200
print("PASS task01 jal_target=00000118 link=00000104 jalr_target=00000200")
def abi_class(index):
if index == 0: return "zero"
if index == 1: return "ra"
if index == 2: return "sp"
if 10 <= index <= 17: return "arg"
if index in (8, 9) or 18 <= index <= 27: return "saved"
if 5 <= index <= 7 or 28 <= index <= 31: return "temp"
return "system"
def task02():
classes = [abi_class(index) for index in range(32)]
assert classes.count("arg") == 8 and classes.count("saved") == 12
assert classes.count("temp") == 7 and sum(classes.count(x) for x in ("zero", "ra", "sp", "system")) == 5
print("PASS task02 args=8 saved=12 temporaries=7 fixed=5")
def task03():
stack = []
stack.extend([0x104, 0x204])
returns = [stack.pop(), stack.pop()]
underflow = not stack
assert returns == [0x204, 0x104] and underflow
print("PASS task03 returns=00000204,00000104 underflow=1 max_depth=2")
TASKS = {"task01_link_address": task01, "task02_abi_registers": task02, "task03_call_stack": 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]]()
+16
View File
@@ -0,0 +1,16 @@
module tb;
logic [31:0] pc, rs1, target, link;
logic signed [31:0] immediate;
logic jalr;
string trace_file;
link_address dut (.*);
initial begin
if ($value$plusargs("trace=%s", trace_file)) begin $dumpfile(trace_file); $dumpvars(0, tb); end
pc=32'h100; rs1=0; immediate=24; jalr=0; #1;
if (target !== 32'h118 || link !== 32'h104) $fatal(1, "FAIL jal target=%h link=%h", target, link);
rs1=32'h205; immediate=-4; jalr=1; #1;
if (target !== 32'h200 || link !== 32'h104) $fatal(1, "FAIL jalr target=%h link=%h", target, link);
$display("PASS task01 jal_target=00000118 link=00000104 jalr_target=00000200");
$finish;
end
endmodule
+23
View File
@@ -0,0 +1,23 @@
module tb;
logic [4:0] register_index;
logic [2:0] abi_class;
integer index, args=0, saved=0, temporaries=0, fixed=0;
string trace_file;
abi_registers dut (.*);
initial begin
if ($value$plusargs("trace=%s", trace_file)) begin $dumpfile(trace_file); $dumpvars(0, tb); end
for (index=0; index<32; index+=1) begin
register_index=index[4:0]; #1;
case (abi_class)
3: args+=1;
4: saved+=1;
5: temporaries+=1;
0,1,2,6: fixed+=1;
default: $fatal(1, "FAIL class index=%0d class=%0d", index, abi_class);
endcase
end
if (args!=8 || saved!=12 || temporaries!=7 || fixed!=5) $fatal(1, "FAIL counts %0d %0d %0d %0d", args,saved,temporaries,fixed);
$display("PASS task02 args=8 saved=12 temporaries=7 fixed=5");
$finish;
end
endmodule
+24
View File
@@ -0,0 +1,24 @@
module tb;
logic clk=0, rst=1, call=0, ret=0;
logic [31:0] link=0, return_address;
logic [2:0] depth;
logic overflow, underflow;
string trace_file;
call_stack dut (.*);
always #1 clk=~clk;
task automatic action(input logic do_call, input logic do_ret, input logic [31:0] value);
@(negedge clk); call=do_call; ret=do_ret; link=value;
@(posedge clk); #1; call=0; ret=0;
endtask
initial begin
if ($value$plusargs("trace=%s", trace_file)) begin $dumpfile(trace_file); $dumpvars(0, tb); end
@(posedge clk); #1; rst=0;
action(1,0,32'h104); if (depth!==1) $fatal(1,"FAIL depth1=%0d",depth);
action(1,0,32'h204); if (depth!==2) $fatal(1,"FAIL depth2=%0d",depth);
action(0,1,0); if (return_address!==32'h204 || depth!==1) $fatal(1,"FAIL return1=%h depth=%0d",return_address,depth);
action(0,1,0); if (return_address!==32'h104 || depth!==0) $fatal(1,"FAIL return2=%h depth=%0d",return_address,depth);
action(0,1,0); if (!underflow || depth!==0) $fatal(1,"FAIL underflow=%b depth=%0d",underflow,depth);
$display("PASS task03 returns=00000204,00000104 underflow=1 max_depth=2");
$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_link_address|task02_abi_registers|task03_call_stack) ;; *) 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