mirror of
https://github.com/ratspeak/C6-Reticulum-ASM
synced 2026-08-12 18:07:18 -04:00
Lays the verifier-loop infrastructure that every subsequent function goes through. Every change is wired to make ci. Tooling (tools/): - parse_spec.py validates the ADR-0007 spec block on every .S file (required fields, @function ↔ basename, @module ↔ directory, @adrs ↔ Accepted ADRs, @verify/@tests path existence, unbounded-cycles ↔ not-required-ct). - check_registry.py cross-checks FUNCTIONS.md against src/ — every registered global has a source file (and vice versa), every depends-on resolves, status disagreements raise warnings. Test harness (tests/harness/): - log_parser.py parses the ADR-0004 log line format with selectors. - target.py: Target ABC + NullTarget loopback + Emu/Hw skeletons that detect tool availability and raise TargetUnavailable until wired (deferred until there's a binary to drive end-to-end). - oracle.py: pure-Python KISS encode/decode reference + RNS.Packet shim for differential testing. - verify.py + verify_cli.py + ./verify wrapper: the dispatcher. Runs spec-validate, pytest, and the @verify-pointed tool (kat-only, .saw, .tla, .py); reports pass/fail/skip/not-implemented. Build chain (toolchain/): - qemu-virt.ld: DRAM @ 0x80000000, __global_pointer$ anchored, 16 KiB stack reserved at top. C6 linker script lands with hardware bring-up. - versions.lock pinned to riscv64-elf-binutils 2.46 + qemu 11.0 + Python 3.12 + RNS 1.2 (per ADR-0008). Includes (src/include/): - psabi.S with the RISC-V ABI register aliases (ADR-0001). - config.S with capacity constants (stack, ring buffers, KISS frame). - regs.S with the qemu-virt NS16550A UART addresses; C6 stub with .error until populated. ADR-0008: bundles four decisions surfaced during bring-up: 1. Rename verify/ → proofs/ to free ./verify for the dispatcher. 2. Adopt vanilla riscv64-elf-binutils (homebrew) over the Espressif crosstool-NG fork; -march=rv32imac -mabi=ilp32. 3. Log timestamps are 8 hex digits emitted by log_hex(width=8). 4. Spec-block prefix is # (RISC-V GAS line comment), not ;;. ADR-0006/0007 status lines flagged as partially superseded; ADR-0004 flagged as refined. ADR index updated. parse_spec accepts both ;; and # prefixes during the transition. open-questions.md tracks OQ-1..OQ-3 as resolved by ADR-0008 and OQ-4 (check_stack.py) deferred. make ci: 51 tests, all green.
145 lines
4.2 KiB
Python
145 lines
4.2 KiB
Python
"""ADR-0004 structured log parser.
|
|
|
|
The on-wire format is::
|
|
|
|
<ts>\\t<module>\\t<event>\\t<key>=<value>[\\t<key>=<value>...]\\r\\n
|
|
|
|
* `ts` is the timestamp emitted by `log_event` (typically 8 hex chars =
|
|
milliseconds since boot encoded as a 32-bit hex; the asm uses `log_hex`
|
|
with width 8, which is the cheapest format to emit). The parser stores
|
|
the raw token and a parsed integer interpretation.
|
|
* `module` and `event` are short ASCII identifiers (no whitespace).
|
|
* `key=value` pairs carry hex or decimal values; whitespace inside a value
|
|
is forbidden by the ADR.
|
|
|
|
Lines that do not conform to the format (boot ROM noise, partial lines)
|
|
parse to `None`; the caller decides what to do with them.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
import re
|
|
from collections.abc import Iterable
|
|
|
|
# Strict line shape: ts<TAB>module<TAB>event[<TAB>key=value]*
|
|
LINE_RE = re.compile(
|
|
r"^(?P<ts>[0-9a-fA-F]+)\t"
|
|
r"(?P<module>[A-Za-z_][A-Za-z0-9_]*)\t"
|
|
r"(?P<event>[A-Za-z_][A-Za-z0-9_]*)"
|
|
r"(?P<rest>(?:\t[^\t=\s]+=[^\t\s]+)*)\s*$"
|
|
)
|
|
KV_RE = re.compile(r"\t([^\t=]+)=([^\t]+)")
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class LogEvent:
|
|
ts_raw: str
|
|
ts_ms: int # parsed as hex; consistent with `log_hex`-emitted timestamps
|
|
module: str
|
|
event: str
|
|
fields: dict[str, str]
|
|
raw: str
|
|
|
|
def __getitem__(self, key: str) -> str:
|
|
return self.fields[key]
|
|
|
|
def get(self, key: str, default: str | None = None) -> str | None:
|
|
return self.fields.get(key, default)
|
|
|
|
|
|
def parse_line(line: str) -> LogEvent | None:
|
|
"""Parse one log line. Returns None for non-conforming input."""
|
|
# Tolerate trailing CR/LF and surrounding whitespace.
|
|
stripped = line.rstrip("\r\n").rstrip()
|
|
if not stripped:
|
|
return None
|
|
m = LINE_RE.match(stripped)
|
|
if not m:
|
|
return None
|
|
ts_raw = m.group("ts")
|
|
try:
|
|
ts_ms = int(ts_raw, 16)
|
|
except ValueError:
|
|
return None
|
|
fields = dict(KV_RE.findall(m.group("rest")))
|
|
return LogEvent(
|
|
ts_raw=ts_raw,
|
|
ts_ms=ts_ms,
|
|
module=m.group("module"),
|
|
event=m.group("event"),
|
|
fields=fields,
|
|
raw=stripped,
|
|
)
|
|
|
|
|
|
def parse_lines(lines: Iterable[str]) -> list[LogEvent]:
|
|
"""Parse an iterable of lines, dropping non-conforming ones."""
|
|
out: list[LogEvent] = []
|
|
for line in lines:
|
|
ev = parse_line(line)
|
|
if ev is not None:
|
|
out.append(ev)
|
|
return out
|
|
|
|
|
|
def find_event(
|
|
events: Iterable[LogEvent],
|
|
*,
|
|
module: str | None = None,
|
|
event: str | None = None,
|
|
**fields: str,
|
|
) -> LogEvent | None:
|
|
"""Return the first event matching all given selectors, or None.
|
|
|
|
Each kwarg in `fields` must equal the event's same-named field. Use
|
|
`find_event(events, module='kiss', event='rx_frame', dest='a1b2c3d4')`.
|
|
"""
|
|
for ev in events:
|
|
if module is not None and ev.module != module:
|
|
continue
|
|
if event is not None and ev.event != event:
|
|
continue
|
|
if any(ev.fields.get(k) != v for k, v in fields.items()):
|
|
continue
|
|
return ev
|
|
return None
|
|
|
|
|
|
def find_all(
|
|
events: Iterable[LogEvent],
|
|
*,
|
|
module: str | None = None,
|
|
event: str | None = None,
|
|
**fields: str,
|
|
) -> list[LogEvent]:
|
|
"""Return every event matching the selectors."""
|
|
out: list[LogEvent] = []
|
|
for ev in events:
|
|
if module is not None and ev.module != module:
|
|
continue
|
|
if event is not None and ev.event != event:
|
|
continue
|
|
if any(ev.fields.get(k) != v for k, v in fields.items()):
|
|
continue
|
|
out.append(ev)
|
|
return out
|
|
|
|
|
|
def assert_event(
|
|
events: Iterable[LogEvent],
|
|
*,
|
|
module: str | None = None,
|
|
event: str | None = None,
|
|
**fields: str,
|
|
) -> LogEvent:
|
|
"""Like `find_event` but raises AssertionError if no match."""
|
|
found = find_event(events, module=module, event=event, **fields)
|
|
if found is None:
|
|
selector = ", ".join(
|
|
[f"module={module!r}" if module else "",
|
|
f"event={event!r}" if event else ""]
|
|
+ [f"{k}={v!r}" for k, v in fields.items()]
|
|
).strip(", ")
|
|
raise AssertionError(f"no log event matching {selector}")
|
|
return found
|