From 3b94dcf8921558d3e86f31f6a2a146289b07f45c Mon Sep 17 00:00:00 2001 From: Orual Date: Sun, 22 Feb 2026 22:00:31 +0000 Subject: [PATCH] init plus plans --- .envrc | 6 ++++++ .gitignore | 3 +++ cm_inst.py | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ design-notes | 1 + dfasm.lark | 143 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ flake.lock | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ flake.nix | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ lang.py | 0 setup.sh | 161 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ sm_mod.py | 29 +++++++++++++++++++++++++++++ test_parser.py | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ token.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ .claude/settings.local.json | 12 ++++++++++++ docs/design-plans/2026-02-22-or1-emu.md | 296 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/implementation-plans/2026-02-22-or1-emu/phase_01.md | 446 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/implementation-plans/2026-02-22-or1-emu/phase_02.md | 351 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/implementation-plans/2026-02-22-or1-emu/phase_03.md | 318 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/implementation-plans/2026-02-22-or1-emu/phase_04.md | 204 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/implementation-plans/2026-02-22-or1-emu/phase_05.md | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ docs/implementation-plans/2026-02-22-or1-emu/phase_06.md | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 20 file(s) changed, 2685 insertion(s)(+), 0 deletion(s)(-) diff --git a/.envrc b/.envrc new file mode 100644 --- /dev/null +++ b/.envrc @@ -0,0 +1,6 @@ + +if ! has nix_direnv_version || ! nix_direnv_version 3.1.0; then + source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.1.0/direnvrc" "sha256-yMJ2OVMzrFaDPn7q8nCBZFRYpL/f0RcHzhmw/i6btJM=" +fi + +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.direnv +.venv +__pycache__ diff --git a/cm_inst.py b/cm_inst.py new file mode 100644 --- /dev/null +++ b/cm_inst.py @@ -0,0 +1,82 @@ +from dataclasses import dataclass +from enum import IntEnum +from token import CMToken, Port +from typing import Optional, Union + +from typing_extensions import IntVar + + +class ALUOp(IntEnum): + pass + + +class ArithOp(ALUOp): + ADD = 0b0000 + SUB = 0b0001 + INC = 0b0010 + DEC = 0b0011 + SHIFT_L = 0b0100 + SHIFT_R = 0b0101 + ASHFT_R = 0b0110 + + +class LogicOp(ALUOp): + AND = 0b0111 + OR = 0b1000 + XOR = 0b1001 + NOT = 0b1010 + EQ = 0b1011 + LT = 0b1100 + LTE = 0b1101 + GT = 0b1110 + GTE = 0b1111 + + +class RoutingOp(ALUOp): + BREQ = 0b10000000 + BRGT = 0b10000001 + BRGE = 0b10000010 + BROF = 0b10000011 + # + more? + SWEQ = 0b11000000 + SWGT = 0b11000001 + SWGE = 0b11000010 + SWOF = 0b11000011 + # + more? + GATE = 0b11100000 + PASS = 0b01000000 + CONST = 0b0010000 + FREE = 0b1010000 + # uncertain + SEL = 0b11110000 + MRGE = 0b11111000 + + +@dataclass(frozen=True) +class Addr(object): + a: int + port: Port + pe: Optional[int] + + +@dataclass(frozen=True) +class ALUInst(object): + """ + Instruction stored in IRAM + """ + + op: ALUOp + dest_l: Optional[Addr] + dest_r: Optional[Addr] + const: Optional[int] + + +@dataclass +class CMComputeOp(object): + """ + Operation with data tokens + """ + + inst: ALUInst + in_l: Optional[CMToken] + in_r: Optional[CMToken] diff --git a/design-notes b/design-notes new file mode 100644 --- /dev/null +++ b/design-notes @@ -0,0 +1,1 @@ +../../Documents/Notes/CPU Design Notes \ No newline at end of file diff --git a/dfasm.lark b/dfasm.lark new file mode 100644 --- /dev/null +++ b/dfasm.lark @@ -0,0 +1,143 @@ +// Dataflow Graph Assembly — Lark EBNF Grammar v0.2 +// Parser: Earley (required for ambiguity-free resolution of location_dir vs weak_edge) + +start: (_NL* statement)* _NL* + +?statement: func_def + | inst_def + | strong_edge + | weak_edge + | plain_edge + | data_def + | location_dir + +// --- Function / subgraph definition --- +// $name |> { body } +func_def: func_ref FLOW_OUT "{" (_NL* statement)* _NL* "}" + +// --- Instruction definition (named node) --- +// &label <| opcode [, arg ...] +inst_def: qualified_ref FLOW_IN opcode ("," argument)* + +// --- Strong inline edge (internal route, anonymous node) --- +// opcode input [, input ...] |> output [, output ...] +strong_edge: opcode argument ("," argument)* FLOW_OUT ref_list + +// --- Weak inline edge (token output, anonymous node) --- +// output [, output ...] opcode <| input [, input ...] +weak_edge: ref_list opcode FLOW_IN argument ("," argument)* + +// --- Plain edge (wiring between named nodes) --- +// source |> dest [, dest ...] +plain_edge: qualified_ref FLOW_OUT ref_list + +// --- Data / initialisation --- +// ref = value | ref = #macro args +data_def: qualified_ref "=" (macro_call | value_list) + +// --- Location directive (bare qualified ref, no operator) --- +// Sets location context for subsequent definitions. +location_dir: qualified_ref + +// === Shared productions === + +ref_list: qualified_ref ("," qualified_ref)* + +// === References === +// Qualifier chain: max one placement (|ident) and one port (:spec). +// @name — node reference +// &name — local label reference +// $name — function / subgraph reference +// Chaining: @sum|pe0:L (placement + port) + +qualified_ref: (node_ref | label_ref | func_ref) placement? port? + +node_ref: "@" IDENT +label_ref: "&" IDENT +func_ref: "$" IDENT + +placement: "|" IDENT +port: ":" PORT_SPEC + +PORT_SPEC: IDENT | HEX_LIT | DEC_LIT + +// === Arguments === +// An argument is a value, a qualified ref, or a named key=value pair. +// Named args are syntactically valid on any instruction. +// Semantic validation (which ops accept named args) is deferred to the assembler. + +?argument: named_arg | positional_arg +named_arg: IDENT "=" positional_arg +?positional_arg: value | qualified_ref + +// === Values (literals) === + +?value: HEX_LIT -> hex_literal + | DEC_LIT -> dec_literal + | CHAR_LIT -> char_literal + | STRING_LIT -> string_literal + | RAW_STRING_LIT -> raw_string_literal + | BYTE_STRING_LIT -> byte_string_literal + +value_list: value ("," value)* + +// === Macros === +// #name arg [arg ...] — expanded in a later pass, not during parsing. + +macro_call: "#" IDENT (value | qualified_ref)* + +// === Opcodes === +// Exhaustive keyword terminal. Priority 2 ensures opcodes win over IDENT +// at the lexer level. Semantic validation (monadic/dyadic arity, valid +// argument combinations) is deferred to the assembler. + +opcode: OPCODE + +OPCODE.2: "add" | "sub" | "inc" | "dec" + | "shiftl" | "shiftr" | "ashiftr" + | "and" | "or" | "xor" | "not" + | "eq" | "lt" | "lte" | "gt" | "gte" + | "breq" | "brgt" | "brge" | "brof" | "brty" + | "sweq" | "swgt" | "swge" | "swof" | "swty" + | "gate" | "sel" | "merge" + | "pass" | "const" | "free" + | "ior" | "iow" | "iorw" + | "load_inst" | "route_set" + +// === Flow operators === +// Priority 3 to win over any partial match of | or < or > + +FLOW_IN.3: "<|" +FLOW_OUT.3: "|>" + +// === Terminals === + +HEX_LIT: /0x[0-9a-fA-F]+/ +DEC_LIT: /[0-9]+/ + +// Character literals: single char or escape sequence. +// Supported escapes: \n \t \r \0 \\ \' \xNN +CHAR_LIT: /'([^'\\]|\\[ntr0\\']|\\x[0-9a-fA-F]{2})'/ + +// String literals — Rust-style semantics. +// "..." regular string, escape sequences processed by assembler +// r"..." raw string, no escape processing +// b"..." byte string, semantic difference only (raw byte values) +// Multi-line strings are permitted: /s flag makes . match \n. +STRING_LIT: /\"([^\"\\]|\\.)*\"/s +RAW_STRING_LIT: /r\"[^\"]*\"/s +BYTE_STRING_LIT: /b\"([^\"\\]|\\.)*\"/s + +IDENT: /[a-zA-Z_][a-zA-Z0-9_]*/ + +// === Whitespace & Comments === +// ; starts a comment to end of line (traditional asm behaviour). +// Newlines are significant as statement separators. + +COMMENT: /;[^\n]*/ +_NL: (NEWLINE | COMMENT) (NEWLINE | COMMENT)* + +%import common.NEWLINE +%import common.WS_INLINE +%ignore WS_INLINE +%ignore COMMENT diff --git a/flake.lock b/flake.lock new file mode 100644 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1771008912, + "narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a82ccc39b39b621151d6732718e3e250109076fa", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 --- /dev/null +++ b/flake.nix @@ -0,0 +1,87 @@ +{ + description = "NBCU Geospatial AI interview environment"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { + self, + nixpkgs, + flake-utils, + }: + flake-utils.lib.eachDefaultSystem ( + system: let + pkgs = import nixpkgs { + inherit system; + config.allowUnfree = true; + }; + pythonPackages = ps: + with ps; [ + ipykernel + jupyterlab + numpy + matplotlib + pip + simpy + ]; + pythonEnv = pkgs.python312.withPackages pythonPackages; + in { + devShells.default = pkgs.mkShell { + name = "nbcu-interview"; + + packages = with pkgs; [ + pythonEnv + uv + + # System libraries that manylinux wheels may dlopen. + # nix-ld covers glibc/libstdc++, but these are common extras + # that specific wheels look for at runtime. + zlib + libffi + openssl + expat + zstd + + # GDAL/PROJ — rasterio wheels bundle these now, but having + # them available doesn't hurt and helps if anything falls + # back to building from source + gdal + proj + + # HDF5 for h5py (transitive dep of some ML libs) + hdf5 + ]; + + # Point nix-ld at the libs these wheels need + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ + pkgs.stdenv.cc.cc.lib # libstdc++ + pkgs.zlib + pkgs.libffi + pkgs.openssl + pkgs.gdal + pkgs.zstd + pkgs.expat + pkgs.proj + pkgs.hdf5 + ]; + + shellHook = '' + # Create/reuse a local venv so uv has somewhere to install to. + # This keeps everything contained and reproducible. + if [ ! -d .venv ]; then + echo "Creating Python venv..." + uv venv --python ${pythonEnv}/bin/python3.12 + fi + source .venv/bin/activate + + echo "" + echo " Python: $(python --version)" + echo " uv: $(uv --version)" + echo "" + ''; + }; + } + ); +} diff --git a/lang.py b/lang.py new file mode 100644 --- /dev/null +++ b/lang.py diff --git a/setup.sh b/setup.sh new file mode 100644 --- /dev/null +++ b/setup.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Pre-install Python packages for the NBCU interview environment. +# Run this BEFORE the interview — torch alone is ~2.5GB. + +if [ ! -d ".venv" ]; then + echo "No .venv found. Enter the devshell first: nix develop" + exit 1 +fi + +source .venv/bin/activate + +echo "Installing core ML stack (trying ROCm torch first, CPU fallback)..." +echo " Attempting ROCm 6.4 wheels..." +if uv pip install \ + torch \ + torchvision \ + --index-url https://download.pytorch.org/whl/rocm6.4; then + echo " ROCm wheels installed. Checking GPU visibility..." + if python -c "import torch; print(f' GPU available: {torch.cuda.is_available()}'); assert torch.cuda.is_available()"; then + echo " ROCm torch with GPU acceleration ready." + else + echo "" + echo " ROCm torch installed but GPU not detected." + echo " To debug GPU detection later:" + echo " - Check /dev/kfd exists and is accessible" + echo " - Check user is in 'video' and 'render' groups" + echo " - Run: HSA_OVERRIDE_GFX_VERSION=10.3.0 python -c \"import torch; print(torch.cuda.is_available())\"" + echo "" + fi +else + echo " ROCm wheel install failed. Falling back to CPU torch." + uv pip install torch torchvision +fi + +uv pip install \ + numpy \ + scipy \ + scikit-learn \ + matplotlib \ + Pillow + +echo "" +echo "Installing geospatial stack..." +uv pip install \ + rasterio \ + laspy[lazrs] \ + opencv-python-headless \ + shapely \ + geopandas \ + fiona + +echo "" +echo "Installing extras (seaborn for confusion matrix plots, etc.)..." +uv pip install \ + seaborn \ + h5py + +echo "" +echo "Verifying critical imports..." +python -c " +import torch +print(f' torch {torch.__version__}') +if torch.cuda.is_available(): + print(f' GPU: {torch.cuda.get_device_name(0)}') + print(f' ROCm/CUDA: {torch.version.hip or torch.version.cuda}') +else: + print(f' CPU only') + +import torchvision +print(f' torchvision {torchvision.__version__}') + +import numpy +print(f' numpy {numpy.__version__}') + +import scipy +print(f' scipy {scipy.__version__}') + +import sklearn +print(f' sklearn {sklearn.__version__}') + +import rasterio +print(f' rasterio {rasterio.__version__}') + +import laspy +print(f' laspy {laspy.__version__}') + +import cv2 +print(f' opencv {cv2.__version__}') + +import shapely +print(f' shapely {shapely.__version__}') + +print() +print('All imports OK.') +" + +echo "" +echo "Smoke-testing torch inference..." +python -c " +import torch +from torchvision.models.segmentation import deeplabv3_resnet50, DeepLabV3_ResNet50_Weights + +weights = DeepLabV3_ResNet50_Weights.DEFAULT +model = deeplabv3_resnet50(weights=weights) +model.eval() + +x = torch.randint(0, 256, (3, 256, 256), dtype=torch.uint8) +x = weights.transforms()(x).unsqueeze(0) + +with torch.no_grad(): + out = model(x)['out'] + +print(f' DeepLabV3 inference OK — output shape: {out.shape}') +print(f' Predicted classes: {torch.unique(out.argmax(dim=1)).tolist()}') +" + +echo "" +echo "Smoke-testing rasterio + laspy..." +python -c " +import rasterio +import numpy as np +from rasterio.transform import from_bounds + +# Write and read a tiny GeoTIFF +data = np.random.randint(0, 255, (3, 64, 64), dtype=np.uint8) +transform = from_bounds(-73.58, 45.49, -73.55, 45.52, 64, 64) + +with rasterio.open( + '/tmp/_test_rio.tif', 'w', driver='GTiff', + height=64, width=64, count=3, dtype='uint8', + crs='EPSG:4326', transform=transform, +) as dst: + dst.write(data) + +with rasterio.open('/tmp/_test_rio.tif') as src: + print(f' rasterio read/write OK — CRS: {src.crs}, shape: {src.shape}') + +import laspy +header = laspy.LasHeader(point_format=0, version='1.2') +header.offsets = [0, 0, 0] +header.scales = [0.001, 0.001, 0.001] +las = laspy.LasData(header) +las.x = np.random.uniform(0, 100, 1000) +las.y = np.random.uniform(0, 100, 1000) +las.z = np.random.uniform(0, 50, 1000) +las.write('/tmp/_test_las.las') +las2 = laspy.read('/tmp/_test_las.las') +print(f' laspy read/write OK — {las2.header.point_count} points') + +import os +os.remove('/tmp/_test_rio.tif') +os.remove('/tmp/_test_las.las') +" + +echo "" +echo "=========================================" +echo " Environment ready." +echo "=========================================" +echo "" diff --git a/sm_mod.py b/sm_mod.py new file mode 100644 --- /dev/null +++ b/sm_mod.py @@ -0,0 +1,29 @@ +from dataclasses import dataclass +from enum import IntEnum +from token import CMToken, SMToken, Token +from typing import List, Optional, Tuple + +import simpy +from simpy import Environment, Resource + + +class Presence(IntEnum): + EMPTY = 0b00 + RESERVED = 0b01 + FULL = 0b10 + WAITING = 0b11 + + +@dataclass +class SMCell(object): + pres: Presence + data_l: Optional[int] # data or length + data_r: Optional[List[int]] # optional data + + +class StructureMem(Resource): + cells: List[SMCell] + + def __init__(self, env: Environment, size: int = 512, capacity: int = 2): + super().__init__(env, capacity) + self.cells = [SMCell(Presence.EMPTY, None, None)] * size diff --git a/test_parser.py b/test_parser.py new file mode 100644 --- /dev/null +++ b/test_parser.py @@ -0,0 +1,174 @@ +"""Test parser for the dataflow graph assembly grammar.""" + +from lark import Lark +from pathlib import Path +from textwrap import dedent + +GRAMMAR_PATH = Path(__file__).parent / "dfasm.lark" + + +def make_parser(): + return Lark( + GRAMMAR_PATH.read_text(), + parser="earley", + propagate_positions=True, + ) + + +def test_parse(parser, name, source): + print(f"\n{'='*60}") + print(f"TEST: {name}") + print(f"{'='*60}") + print(source.strip()) + print(f"{'-'*60}") + try: + tree = parser.parse(source) + print(tree.pretty()) + return True + except Exception as e: + print(f"PARSE ERROR: {e}") + return False + + +def main(): + parser = make_parser() + results = [] + + # --- Test 1: Basic instruction definitions --- + results.append(test_parse(parser, "inst_def basics", dedent("""\ + &my_add <| add + &my_sub <| sub + &my_const <| const, 10 + &my_shift <| shiftl + &my_not <| not + """))) + + # --- Test 2: Plain edges --- + results.append(test_parse(parser, "plain edges", dedent("""\ + &a |> &b:L + &a |> &b:R + &c |> &d, &e + """))) + + # --- Test 3: Fib function definition (from sketch) --- + results.append(test_parse(parser, "fib function", dedent("""\ + $fib |> { + &const_n <| const, 10 + &sub1 <| sub + &sub2 <| sub + &branch <| sweq + + &const_n |> &branch:L + &const_n |> &sub1:L + &const_n |> &sub1:R + &const_n |> &sub2:R + &sub1 |> &recurse_a:L + } + """))) + + # --- Test 4: PE and SM placement qualifiers --- + results.append(test_parse(parser, "placement qualifiers", dedent("""\ + &my_add|pe0 <| add + &result|pe1 <| pass + &my_add|pe0 |> &result|pe1:L + """))) + + # --- Test 5: Data definitions --- + results.append(test_parse(parser, "data definitions", dedent("""\ + @hello|sm0:0 = 0x05 + @hello|sm0:1 = 'h', 'e' + @hello|sm0:2 = 'l', 'l' + """))) + + # --- Test 6: Macro invocation --- + results.append(test_parse(parser, "macro invocation", dedent("""\ + @hello = #str "hello" + """))) + + # --- Test 7: Named arguments (IO operation) --- + results.append(test_parse(parser, "named args (IO)", dedent("""\ + &serial <| ior, dest=0x45, addr=0x91, data=0x43 + """))) + + # --- Test 8: Strong inline edge --- + results.append(test_parse(parser, "strong inline edge", dedent("""\ + add &a, &b |> &c, &d + """))) + + # --- Test 9: Weak inline edge --- + results.append(test_parse(parser, "weak inline edge", dedent("""\ + &c, &d sub <| &a, &b + """))) + + # --- Test 10: Comments --- + results.append(test_parse(parser, "comments", dedent("""\ + &my_add <| add ; this is a comment + &a |> &b:L ; wire a to b left port + """))) + + # --- Test 11: Location directive (bare qualified ref) --- + results.append(test_parse(parser, "location directive", dedent("""\ + @data_section|sm0 + """))) + + # --- Test 12: System config instructions --- + results.append(test_parse(parser, "system config", dedent("""\ + &loader <| load_inst, dest=0x01, addr=0x00, data_l=0xABCD, data_h=0x1234 + """))) + + # --- Test 13: Hex literal in const --- + results.append(test_parse(parser, "hex const", dedent("""\ + &mask <| const, 0xFF + """))) + + # --- Test 14: Multi-line string in data def --- + results.append(test_parse(parser, "multi-line string", dedent('''\ + @msg = "hello +world" + '''))) + + # --- Test 15: Raw string --- + results.append(test_parse(parser, "raw string", dedent("""\ + @path = r"no\\escapes\\here" + """))) + + # --- Test 16: Byte string --- + results.append(test_parse(parser, "byte string", dedent("""\ + @raw_data = b"\\x01\\x02\\x03" + """))) + + # --- Test 17: Fan-out from named node --- + results.append(test_parse(parser, "fan-out", dedent("""\ + &splitter <| pass + &input |> &splitter:L + &splitter |> &consumer_a:L, &consumer_b:R + """))) + + # --- Test 18: Mixed program --- + results.append(test_parse(parser, "mixed program", dedent("""\ + @counter|sm0:0 = 0x00 + + $main |> { + &init <| const, 0 + &loop_add <| add + &cmp <| lte + &branch <| breq + &output <| iow, dest=0x01 + + &init |> &loop_add:L + &loop_add |> &cmp:L + &loop_add |> &output:L + } + """))) + + # --- Summary --- + print(f"\n{'='*60}") + total = len(results) + passed = sum(results) + failed = total - passed + print(f"RESULTS: {passed}/{total} passed, {failed} failed") + print(f"{'='*60}") + + +if __name__ == "__main__": + main() diff --git a/token.py b/token.py new file mode 100644 --- /dev/null +++ b/token.py @@ -0,0 +1,78 @@ +from dataclasses import dataclass +from enum import Enum, IntEnum +from typing import List, Optional, Tuple + +from simpy import Event + + +class Port(IntEnum): + L = 0 + R = 1 + + +@dataclass(frozen=True) +class Token(object): + target: int + + +@dataclass(frozen=True) +class CMToken(Token): + offset: int + ctx: int + data: int + + +@dataclass(frozen=True) +class DyadToken(CMToken): + port: Port + gen: int + wide: bool + + +@dataclass(frozen=True) +class MonadToken(CMToken): + inline: bool + + +class MemOp(IntEnum): + READ = 0b000 + WRITE = 0b001 + ALLOC = 0b011 + FREE = 0b100 + CLEAR = 0b101 + # reserved + RD_INC = 0b1100 + RD_DEC = 0b1101 + CMP_SW = 0b1110 + # reserved + + +@dataclass(frozen=True) +class SMToken(Token): + op: MemOp + flags: Optional[int] # TBD + data: Optional[int] + ret: Optional[CMToken] # return path + + +@dataclass(frozen=True) +class SysToken(Token): + addr: Optional[int] + + +@dataclass(frozen=True) +class IOToken(SysToken): + data: Optional[List[int]] + + +class CfgOp(IntEnum): + LOAD_INST = 0 + ROUTE_SET = 1 + + +@dataclass(frozen=True) +class CfgToken(SysToken): + op: CfgOp + data: List[ + Tuple[int, int] + ] # data low + data high for address and sequential following diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,12 @@ +{ + "permissions": { + "allow": [ + "WebFetch(domain:simpy.readthedocs.io)", + "WebFetch(domain:arxiv.org)", + "WebFetch(domain:dvcon-proceedings.org)", + "WebFetch(domain:hypothesis.readthedocs.io)" + ] + }, + "outputStyle": "Humanist Coder", + "spinnerTipsEnabled": false +} diff --git a/docs/design-plans/2026-02-22-or1-emu.md b/docs/design-plans/2026-02-22-or1-emu.md new file mode 100644 --- /dev/null +++ b/docs/design-plans/2026-02-22-or1-emu.md @@ -0,0 +1,296 @@ +# OR1 Dataflow CPU Behavioural Emulator Design + +## Summary + +This design describes a behavioural emulator for the OR1 dataflow CPU architecture, built using Python's SimPy discrete event simulation framework. The emulator models three core hardware components: Processing Elements (PEs) that execute dataflow instructions using a matching store for operand pairing and generation counters for dataflow synchronization; Structure Memory (SM) modules that implement I-structure semantics with blocking reads and a depth-1 deferred read register; and a token-routing network that connects these components via bounded FIFOs with backpressure. + +The implementation prioritizes behavioural correctness over cycle-accurate timing. Tokens flow as complete Python objects between modules connected through SimPy Stores that represent hardware FIFOs, with backpressure emerging naturally when stores reach capacity. The system provides a direct Python initialization API for configuring IRAM contents, SM state, and routing tables without requiring configuration token processing. A property-based test suite using pytest and hypothesis validates matching store invariants, I-structure state transitions, ALU correctness across all v0 operations, output formatter modes, and end-to-end token flow through multi-PE programs. + +## Definition of Done + +1. **SimPy-based emulator** with behaviourally correct implementations of: Processing Elements (matching store with generation counters and presence bits, ALU execution for all v0 operations, output formatter with SUPPRESS/SINGLE/DUAL/SWITCH modes), Structure Memory (I-structure semantics with depth-1 deferred read register, 2-bit presence state machine for all transitions, all v0 memory operations), and a token-routing network (type-based routing, backpressure via bounded SimPy Stores). + +2. **Direct initialization API** — Python-native setup of IRAM contents, SM initial state, and routing configuration. CfgToken processing as abstract IRAM/route mutation is a stretch goal. + +3. **Property-based test suite** (pytest + hypothesis) validating: matching store invariants (generation counter ABA prevention, presence bit state machine correctness), SM state transitions and I-structure semantics (deferred read depth-1 constraint, blocking read/write ordering), ALU operation correctness across the full v0 opcode set, output formatter mode semantics (token count and routing per mode), and end-to-end token flow through a minimal multi-PE path. + +4. **End-to-end demonstration**: the emulator can run a small hand-constructed program (a few instructions across 1-2 PEs) end-to-end, demonstrating correct token flow, matching, execution, and output routing. + +## Acceptance Criteria + +### or1-emu.AC1: Processing Element Behaviour +- **or1-emu.AC1.1 Success:** Monadic token bypasses matching store and executes immediately +- **or1-emu.AC1.2 Success:** First dyadic token for an offset/ctx stores in matching store, does not fire +- **or1-emu.AC1.3 Success:** Second dyadic token for same offset/ctx retrieves partner, fires instruction +- **or1-emu.AC1.4 Success:** Generation counter mismatch causes stale token discard (no match, no fire) +- **or1-emu.AC1.5 Success:** Output formatter SINGLE mode emits exactly one token to dest_l +- **or1-emu.AC1.6 Success:** Output formatter DUAL mode emits two tokens with same data to dest_l and dest_r +- **or1-emu.AC1.7 Success:** Output formatter SWITCH mode routes data to taken side, inline trigger to not-taken side +- **or1-emu.AC1.8 Success:** Output formatter SUPPRESS mode emits zero tokens +- **or1-emu.AC1.9 Failure:** Token targeting non-existent IRAM offset is handled without crash + +### or1-emu.AC2: ALU Correctness +- **or1-emu.AC2.1 Success:** Arithmetic ops (ADD, SUB) produce correct 16-bit wrapping results for all uint16 pairs +- **or1-emu.AC2.2 Success:** INC/DEC monadic ops correctly increment/decrement +- **or1-emu.AC2.3 Success:** Shift ops use const as shift amount; ASHIFTR sign-extends from bit 15 +- **or1-emu.AC2.4 Success:** Logic ops (AND, OR, XOR, NOT) produce correct bitwise results +- **or1-emu.AC2.5 Success:** Comparison ops interpret operands as signed 2's complement (0xFFFF < 0x0001) +- **or1-emu.AC2.6 Success:** Comparison ops produce 0x0001/0x0000 result and correct bool_out +- **or1-emu.AC2.7 Success:** Routing ops (BR*, SW*, GATE) compute boolean and pass data through unchanged +- **or1-emu.AC2.8 Success:** PASS returns left operand, CONST returns const field +- **or1-emu.AC2.9 Edge:** Signed boundary: GT(0x7FFF, 0x8000) is true (32767 > -32768) + +### or1-emu.AC3: Structure Memory Behaviour +- **or1-emu.AC3.1 Success:** READ on FULL cell returns data immediately via result token +- **or1-emu.AC3.2 Success:** READ on EMPTY cell with empty deferred register stashes return route, sets WAITING +- **or1-emu.AC3.3 Success:** WRITE on WAITING cell satisfies deferred read — emits result token to stashed return route +- **or1-emu.AC3.4 Success:** WRITE on EMPTY/RESERVED sets cell to FULL +- **or1-emu.AC3.5 Success:** CLEAR sets cell to EMPTY, cancels deferred read if targeting that cell +- **or1-emu.AC3.6 Success:** READ_INC/READ_DEC atomically modify and return value (lower 256 cells only) +- **or1-emu.AC3.7 Failure:** Depth-1 constraint: second blocking READ on different empty cell stalls until first deferred read is satisfied +- **or1-emu.AC3.8 Edge:** WRITE on FULL cell overwrites data (diagnostic flag set if modelled) + +### or1-emu.AC4: Network and Routing +- **or1-emu.AC4.1 Success:** Token with dest PE_id N arrives at PE N's input Store +- **or1-emu.AC4.2 Success:** SM token (type 10) routes to correct SM by SM_id +- **or1-emu.AC4.3 Success:** Backpressure: PE blocks on put() when destination Store is at capacity +- **or1-emu.AC4.4 Success:** Backpressure releases when consumer drains destination Store + +### or1-emu.AC5: Direct Initialization API +- **or1-emu.AC5.1 Success:** System constructed from PEConfig with IRAM contents — PE has expected instructions at expected offsets +- **or1-emu.AC5.2 Success:** System constructed from SMConfig with initial cell data — SM cells match config +- **or1-emu.AC5.3 Success:** inject(token) delivers seed token to correct module's input Store + +### or1-emu.AC6: End-to-End Execution +- **or1-emu.AC6.1 Success:** CONST on PE0 emits token that arrives at PE1, triggers ADD, produces correct result +- **or1-emu.AC6.2 Success:** PE writes to SM, different PE reads from SM, receives correct data +- **or1-emu.AC6.3 Success:** DUAL mode fan-out delivers same result to two different consumers +- **or1-emu.AC6.4 Success:** SWITCH mode routes data and trigger to correct destinations based on comparison result + +## Glossary + +- **ALU (Arithmetic Logic Unit)**: The execution unit within a Processing Element that performs arithmetic, logical, comparison, and routing operations on operands. +- **Backpressure**: Flow control mechanism where a downstream component that cannot accept more data signals the upstream component to pause, implemented via blocking on full SimPy Stores. +- **CfgToken**: Configuration token type used to mutate IRAM contents or routing tables at runtime (stretch goal in this design). +- **Context slot**: An index dimension in the matching store corresponding to a dataflow context, used to isolate concurrent activations of the same instruction. +- **Dataflow architecture**: A computation model where instruction execution is triggered by operand availability rather than a program counter, enabling natural parallelism. +- **Deferred read**: I-structure pattern where a READ on an EMPTY cell blocks by storing the return route for later satisfaction when a WRITE arrives. +- **Dyadic operation**: An instruction requiring two operands (left and right), contrasted with monadic operations requiring only one. +- **Generation counter**: A 2-bit counter per context slot that increments on reallocation, used to detect and discard stale tokens from previous activations (ABA prevention). +- **I-structure**: A synchronization primitive from dataflow computing where cells have blocking read-when-empty semantics, with writes satisfying pending reads. +- **IRAM (Instruction RAM)**: Instruction memory within a Processing Element, indexed by token offset to fetch the operation and destination routing. +- **Matching store**: Hardware structure within a PE that holds the first operand of a dyadic instruction until its partner arrives, enabling dataflow firing when both are present. +- **Monadic operation**: An instruction requiring only one operand, which bypasses the matching store and executes immediately. +- **Output formatter**: PE stage that determines how many result tokens to emit and where to route them based on instruction mode (SUPPRESS/SINGLE/DUAL/SWITCH). +- **Presence state**: Per-entry flag in the matching store (occupied/empty) or per-cell 2-bit state in Structure Memory (EMPTY/RESERVED/FULL/WAITING). +- **Property-based testing**: Testing approach using hypothesis to generate randomized test cases validating invariants rather than testing specific examples. +- **SimPy**: Python discrete event simulation framework providing processes, resources, and stores. +- **SimPy Store**: SimPy's queue primitive with optional capacity bounds; `get()` and `put()` block when empty/full respectively. +- **Token**: The fundamental unit of data flow in the OR1 architecture, carrying a value, destination routing, context information, and port assignment. +- **Type-based routing**: Routing mechanism where tokens are directed to destinations based on their type field (types 00/01 for PE, type 10 for SM, type 11 for system) combined with a destination ID. + +## Architecture + +Behavioural emulator built on SimPy's discrete event simulation. Three module types — Processing Element (PE), Structure Memory (SM), and Network — communicate via bounded SimPy Stores representing hardware FIFOs. Tokens flow as whole Python objects (not flit-level serialization). Backpressure emerges naturally: `yield store.put(token)` blocks when a destination FIFO is full. + +**Route table + direct put topology.** Each module holds a `route_table: dict[int, simpy.Store]` mapping destination IDs to other modules' input Stores. A top-level `build_topology()` function wires these up at initialization. No central router process — modules resolve destinations and put directly. This mirrors the hardware's type-based routing without modelling bus arbitration. + +**System object** is the top-level handle returned by `build_topology()`. It provides `inject(token)` for seed token insertion, `pes[id]` / `sms[id]` for direct module access, and `env` for running the simulation. Tests and the initialization API interact through System. + +### Module Layout + +``` +or1-design/ + token.py # Token hierarchy (shared with assembler) + cm_inst.py # ALU ops, instruction types (shared with assembler) + sm_mod.py # SM data types: Presence, SMCell (shared with assembler) + emu/ + __init__.py + alu.py # Pure function: execute(op, left, right, const) -> (result, bool_out) + pe.py # ProcessingElement: SimPy process, matching store, output formatter + sm.py # StructureMemory: SimPy process, I-structure cells, deferred read + network.py # build_topology(), System class + types.py # Emulator-specific types (MatchEntry, DeferredRead, PEConfig, SMConfig) + tests/ + conftest.py # Shared fixtures, hypothesis strategies + test_alu.py # ALU property-based tests + test_pe.py # PE matching + output formatter tests + test_sm.py # SM state machine + deferred read tests + test_network.py # Routing + backpressure tests + test_integration.py # End-to-end multi-module programs +``` + +Root-level modules (`token.py`, `cm_inst.py`, `sm_mod.py`) contain shared data representations used by both the emulator and the future assembler. The `emu/` package imports from them and adds simulation behaviour. + +### Processing Element + +Single SimPy process per PE. Five pipeline stages implemented as separable methods (can later become independent SimPy processes connected by inter-stage Stores). + +**State:** +- `iram: dict[int, ALUInst]` — instruction memory keyed by offset. Each ALUInst holds opcode, dest_l, dest_r, const. Mutable via CfgToken. +- `matching_store: list[list[MatchEntry]]` — 2D indexed by `[ctx_slot][offset]`. Each MatchEntry has `occupied: bool`, `data: Optional[int]`, `port: Port`. +- `gen_counters: list[int]` — 2-bit generation counter per context slot. Incremented on slot reallocation. +- `input_store: simpy.Store` — bounded input FIFO. +- `route_table: dict[int, simpy.Store]` — destination PE/SM ID to output Store. + +**Process loop:** +1. **Token Input**: `yield input_store.get()` — blocks until token available. +2. **Cfg Check**: If CfgToken, apply abstract mutation to IRAM or route_table. Continue. +3. **Match/Bypass**: Monadic tokens bypass matching, returning `(data, None)`. Dyadic tokens check generation counter (discard if stale), then check presence bit at `[ctx][offset]` — store partial operand and wait, or retrieve partner and fire. +4. **Instruction Fetch**: Read `iram[offset]` to get opcode and destination fields. +5. **Execute**: Call `alu.execute(op, left, right, const)` → `(result, bool_out)`. +6. **Token Output**: Output formatter inspects mode (derived from opcode + has_dest2): + - SUPPRESS: no tokens emitted. Used by FREE, GATE-when-false. + - SINGLE: one token to dest_l. `yield route_table[dest_l.pe].put(token)`. + - DUAL: two tokens with same data to dest_l and dest_r. + - SWITCH: data token to `bool_out ? dest_l : dest_r`, inline monadic trigger to the other. + +Each `yield store.put()` in the output stage blocks on backpressure. + +### Structure Memory + +Single SimPy process. Implements I-structure semantics with a depth-1 deferred read register. + +**State:** +- `cells: list[SMCell]` — 512 cells, each with `presence: Presence` and `data: Optional[int]`. +- `deferred_read: Optional[DeferredRead]` — single register: `(cell_addr, return_route)`. Only one pending deferred read at a time. +- `input_store: simpy.Store` — bounded input FIFO. +- `route_table: dict[int, simpy.Store]` — for sending result tokens back to PEs. + +**Operation dispatch:** +- **READ on FULL**: immediate result token via return route. +- **READ on EMPTY/RESERVED**: if deferred register empty, stash return route, set cell to WAITING. If deferred register occupied (depth-1 constraint), SM process yields on an internal `simpy.Event` until the existing deferred read is satisfied, then retries. +- **WRITE on WAITING**: satisfy deferred read — emit result token to stashed return route, set cell to FULL. +- **WRITE on EMPTY/RESERVED**: set to FULL, store data. +- **CLEAR**: set to EMPTY, cancel deferred read if targeting this cell. +- **READ_INC / READ_DEC / CAS**: atomic operations restricted to lower 256 cells, immediate-result pattern. + +### ALU + +Pure function in `emu/alu.py`. No state, no SimPy involvement. + +**Contract:** `execute(op: ALUOp, left: int, right: Optional[int], const: Optional[int]) -> tuple[int, bool]` + +Returns `(result & 0xFFFF, bool_out)`. All values stored as unsigned 16-bit. Comparisons and overflow detection interpret operands as signed 2's complement via `to_signed()` helper. + +**Opcode groups:** +- Arithmetic (ADD, SUB, INC, DEC): 16-bit wrapping. INC/DEC monadic. +- Shifts (SHIFTL, SHIFTR, ASHIFTR): monadic, amount from const. ASHIFTR sign-extends. +- Logic (AND, OR, XOR, NOT): bitwise. NOT monadic. +- Comparison (EQ, LT, LTE, GT, GTE): signed 2's complement. Result is 0x0001/0x0000, sets bool_out. +- Routing (BR*, SW*, GATE, SEL, MERGE): compute boolean condition, data passes through. Output formatter uses bool_out for routing decisions. GATE and SEL are dyadic: left=data, right=boolean. bool_out comes from right operand (port R bit 0 in hardware). +- Data (PASS, CONST, FREE): PASS = identity, CONST = return const, FREE = no output (SUPPRESS). + +### Network and Topology + +`build_topology(env, pe_configs, sm_configs, fifo_capacity=8) -> System` + +Creates bounded SimPy Stores (one per module input), instantiates PE and SM objects, populates route_tables bidirectionally (PE→PE, PE→SM, SM→PE). Returns System object. + +**PEConfig**: PE ID, IRAM contents, matching store dimensions (ctx_slots, offsets), initial generation counters. + +**SMConfig**: SM ID, cell count, initial cell contents. + +## Existing Patterns + +Existing code in `token.py`, `cm_inst.py`, and `sm_mod.py` establishes: +- `@dataclass(frozen=True)` for immutable value types (tokens, instructions, addresses) +- Mutable `@dataclass` for stateful containers (SMCell) +- `IntEnum` for opcodes and state enums (ALUOp, Presence, MemOp, Port) +- Type annotations throughout +- SimPy Resource as base class for SM (`sm_mod.py`) + +The emulator follows these patterns. New emulator-specific types (MatchEntry, DeferredRead, PEConfig, SMConfig) use the same conventions. The PE class does not extend a SimPy resource — it owns a SimPy Store and runs as a process, which is the canonical SimPy pattern for active components (per Graphcore's "SimPy for Chips" approach). + +## Implementation Phases + + +### Phase 1: Emulator Scaffold and ALU +**Goal:** Project structure, emulator-specific types, and a fully tested ALU. + +**Components:** +- `emu/__init__.py` — package init +- `emu/types.py` — MatchEntry, DeferredRead, PEConfig, SMConfig dataclasses +- `emu/alu.py` — `execute()` pure function with full v0 opcode dispatch +- `tests/conftest.py` — hypothesis strategies: `uint16()`, `alu_ops()`, `int16()` +- `tests/test_alu.py` — property-based tests for all opcode groups + +**Dependencies:** None (first phase) + +**Done when:** ALU passes property-based tests for all v0 opcodes including signed comparison edge cases (0x7FFF vs 0x8000, overflow wrapping). `or1-emu.AC2` criteria covered. + + + +### Phase 2: Processing Element Core +**Goal:** PE with matching store, instruction fetch, and output formatter — testable in isolation. + +**Components:** +- `emu/pe.py` — ProcessingElement class: matching store state, generation counters, process loop, stage methods (match, fetch, execute, emit) +- `tests/test_pe.py` — matching store invariants (hypothesis), output formatter mode tests (SimPy) +- `tests/conftest.py` — additional strategies: `token_sequences()`, PE fixtures with minimal IRAM + +**Dependencies:** Phase 1 (ALU, types) + +**Done when:** PE correctly processes monadic tokens (bypass matching), dyadic tokens (store partial, fire on complete), discards stale tokens (generation mismatch), and emits correct token count per output mode. `or1-emu.AC1` criteria covered. + + + +### Phase 3: Structure Memory +**Goal:** SM with I-structure semantics, depth-1 deferred read, and all v0 operations. + +**Components:** +- `emu/sm.py` — StructureMemory class: cell state machine, deferred read register, process loop, operation dispatch +- `tests/test_sm.py` — presence state machine (hypothesis), deferred read stall (SimPy), atomic ops +- `tests/conftest.py` — additional strategies: `sm_op_sequences()` + +**Dependencies:** Phase 1 (types) + +**Done when:** SM handles all state transitions (EMPTY→FULL, EMPTY→WAITING→FULL with deferred satisfaction), enforces depth-1 deferred read constraint, and atomic ops work on lower 256 cells. `or1-emu.AC3` criteria covered. + + + +### Phase 4: Network and Topology +**Goal:** Route table wiring, System object, token routing, and backpressure. + +**Components:** +- `emu/network.py` — `build_topology()`, System class with `inject()`, module accessors +- `tests/test_network.py` — routing correctness (token reaches right destination by type+ID), backpressure (PE stalls when output FIFO full) + +**Dependencies:** Phase 2 (PE), Phase 3 (SM) + +**Done when:** Tokens route correctly by type and destination ID. Backpressure propagates: filling a destination Store causes the sending PE to block on `put()`. `or1-emu.AC4` criteria covered. + + + +### Phase 5: Direct Initialization API +**Goal:** Python-native setup of IRAM, SM contents, and routes without CfgTokens. + +**Components:** +- Initialization helpers in `emu/network.py` or `emu/__init__.py` — accept PEConfig/SMConfig dicts, populate IRAM and SM cells at construction time +- `tests/test_integration.py` — initialization smoke tests: create system, verify IRAM and SM state match config + +**Dependencies:** Phase 4 (network/System) + +**Done when:** A System can be constructed from Python config objects with pre-populated IRAM and SM state, ready to accept seed tokens. `or1-emu.AC5` criteria covered. + + + +### Phase 6: End-to-End Integration +**Goal:** Run a small hand-constructed program across 1-2 PEs and verify results. + +**Components:** +- `tests/test_integration.py` — multi-PE programs: PE0 CONST feeds PE1 ADD, SM round-trip (PE writes, PE reads), fan-out via DUAL mode, conditional routing via SWITCH +- Stretch: CfgToken abstract mutation test (load IRAM via token injection) + +**Dependencies:** Phase 5 (initialization API) + +**Done when:** A hand-constructed program executes end-to-end across 2 PEs with correct token flow, matching, execution, and output routing. SM read/write round-trip produces correct data. `or1-emu.AC6` criteria covered. + + +## Additional Considerations + +**Split-ready PE stages.** The PE process loop calls stage methods sequentially. To split into independent SimPy processes later, replace method calls with inter-stage SimPy Stores. The method signatures are designed for this: each takes explicit inputs and returns explicit outputs, no implicit shared state between stages. + +**CfgToken stretch goal.** Abstract mutation (direct IRAM/route_table write from a CfgToken) can be added in Phase 6 without architectural changes — the PE process loop already has a cfg check branch. Full hardware-faithful cfg processing (PE locking, multi-cycle writes) is out of scope. + +**Shared mutable list footgun.** `sm_mod.py` line 29 uses `[SMCell(...)] * size` which creates a list of references to the same object. The emulator's SM will need to create independent cell instances. diff --git a/docs/implementation-plans/2026-02-22-or1-emu/phase_01.md b/docs/implementation-plans/2026-02-22-or1-emu/phase_01.md new file mode 100644 --- /dev/null +++ b/docs/implementation-plans/2026-02-22-or1-emu/phase_01.md @@ -0,0 +1,446 @@ +# OR1 Dataflow CPU Behavioural Emulator — Phase 1: Emulator Scaffold and ALU + +**Goal:** Create the `emu/` package structure, emulator-specific types, and a fully tested ALU. + +**Architecture:** Pure-function ALU with no SimPy dependency. Emulator types as dataclasses following existing project conventions (`@dataclass(frozen=True)` for immutable, `@dataclass` for mutable). Property-based testing via hypothesis validates all v0 opcodes. + +**Tech Stack:** Python 3.12, pytest, hypothesis + +**Scope:** 6 phases from original design (phase 1 of 6) + +**Codebase verified:** 2026-02-22 + +--- + +## Acceptance Criteria Coverage + +This phase implements and tests: + +### or1-emu.AC2: ALU Correctness +- **or1-emu.AC2.1 Success:** Arithmetic ops (ADD, SUB) produce correct 16-bit wrapping results for all uint16 pairs +- **or1-emu.AC2.2 Success:** INC/DEC monadic ops correctly increment/decrement +- **or1-emu.AC2.3 Success:** Shift ops use const as shift amount; ASHIFTR sign-extends from bit 15 +- **or1-emu.AC2.4 Success:** Logic ops (AND, OR, XOR, NOT) produce correct bitwise results +- **or1-emu.AC2.5 Success:** Comparison ops interpret operands as signed 2's complement (0xFFFF < 0x0001) +- **or1-emu.AC2.6 Success:** Comparison ops produce 0x0001/0x0000 result and correct bool_out +- **or1-emu.AC2.7 Success:** Routing ops (BR*, SW*, GATE) compute boolean and pass data through unchanged +- **or1-emu.AC2.8 Success:** PASS returns left operand, CONST returns const field +- **or1-emu.AC2.9 Edge:** Signed boundary: GT(0x7FFF, 0x8000) is true (32767 > -32768) + +--- + + + +### Task 1: Rename token.py, add pytest/hypothesis/typing-extensions to project dependencies + +**Files:** +- Rename: `token.py` → `tokens.py` +- Modify: `cm_inst.py:3` (update import) +- Modify: `sm_mod.py:3` (update import) +- Modify: `flake.nix:2` (description) +- Modify: `flake.nix:20-28` (pythonPackages list) + +**Step 0: Rename token.py to avoid stdlib shadow** + +Python's stdlib has a `token` module (used internally by `dataclasses` → `inspect` → `tokenize`). The project's `token.py` at root shadows it, causing circular import failures at runtime. Rename it: + +```bash +mv token.py tokens.py +``` + +Update existing imports in `cm_inst.py` line 3: +```python +# Change: from token import CMToken, Port +# To: +from tokens import CMToken, Port +``` + +Update existing imports in `sm_mod.py` line 3: +```python +# Change: from token import CMToken, SMToken, Token +# To: +from tokens import CMToken, SMToken, Token +``` + +Verify: `python -c "from tokens import CMToken; print('OK')"` +Expected: `OK` + +**Step 1: Update flake.nix description and add dependencies** + +Update the description on line 2 from `"NBCU Geospatial AI interview environment"` to `"OR1 Dataflow CPU development environment"`. + +Update the shell name on line 32 from `"nbcu-interview"` to `"or1-dev"`. + +In the `pythonPackages` function (line 20-28), add `pytest`, `hypothesis`, and `typing-extensions`: + +```nix +pythonPackages = ps: + with ps; [ + ipykernel + jupyterlab + numpy + matplotlib + pip + simpy + pytest + hypothesis + typing-extensions + ]; +``` + +Note: `typing-extensions` is required because `cm_inst.py` line 6 imports `from typing_extensions import IntVar`. Without it, any module importing `cm_inst` will fail with `ModuleNotFoundError`. + +**Step 2: Rebuild the nix environment** + +Run: +```bash +# If using direnv: +direnv reload +# Otherwise: +exit +nix develop +``` + +**Step 3: Verify packages are available** + +Run: `python -c "import pytest; import hypothesis; import typing_extensions; print('OK')"` +Expected: `OK` + +**Commit:** `chore: rename token.py to tokens.py, add pytest/hypothesis/typing-extensions` + + + +### Task 2: Create emu/ package, types module, and add SMInst to cm_inst.py + +**Files:** +- Create: `emu/__init__.py` +- Create: `emu/types.py` +- Modify: `cm_inst.py` (append `SMInst` class) + +**Implementation:** + +`emu/__init__.py` — empty package init: +```python +``` + +`emu/types.py` — emulator-specific types following project conventions: + +```python +from dataclasses import dataclass +from typing import Optional + +from cm_inst import ALUInst, SMInst +from sm_mod import Presence +from tokens import CMToken, Port + + +@dataclass +class MatchEntry: + occupied: bool = False + data: Optional[int] = None + port: Port = Port.L + + +@dataclass(frozen=True) +class DeferredRead: + cell_addr: int + return_route: CMToken + + +@dataclass(frozen=True) +class PEConfig: + pe_id: int + iram: dict[int, ALUInst | SMInst] + ctx_slots: int = 4 + offsets: int = 64 + gen_counters: Optional[list[int]] = None + + +@dataclass(frozen=True) +class SMConfig: + sm_id: int + cell_count: int = 512 + initial_cells: Optional[dict[int, tuple[Presence, Optional[int]]]] = None +``` + +Append `SMInst` to `cm_inst.py` (after the existing `ALUInst` class): + +```python +@dataclass(frozen=True) +class SMInst(object): + """ + SM instruction stored in IRAM. Causes PE to emit an SMToken + instead of routing through the ALU. + + Operand mapping (PE constructs SMToken from instruction + token operands): + READ (monadic): cell_addr = const or token.data, result → ret + WRITE (monadic): cell_addr = const, write_data = token.data + WRITE (dyadic): cell_addr = left, write_data = right + CLEAR/ALLOC/FREE: cell_addr = const or token.data + RD_INC/RD_DEC: cell_addr = const or token.data, result → ret + CMP_SW (dyadic): cell_addr = const, expected = left, new = right, result → ret + """ + + op: MemOp + sm_id: int + const: Optional[int] = None + ret: Optional[Addr] = None +``` + +This requires adding `from tokens import MemOp` to `cm_inst.py`'s imports (Task 1 already renamed `token.py` → `tokens.py`). + +Key decisions: +- `MatchEntry` is mutable (PE mutates presence/data during matching) +- `DeferredRead` is frozen (value type representing a stashed read) +- `PEConfig`/`SMConfig` are frozen (immutable configuration) +- `PEConfig.iram` is typed as `dict[int, ALUInst | SMInst]` — IRAM can hold ALU instructions (PE→PE) or SM instructions (PE→SM) +- `PEConfig.gen_counters` is typed as `Optional[list[int]]` — 2-bit values per ctx slot +- `SMConfig.initial_cells` is typed as `Optional[dict[int, tuple[Presence, Optional[int]]]]` — maps cell address to (presence state, data) +- `SMInst` lives in `cm_inst.py` (not `emu/types.py`) because it's part of the instruction set definition, shared with the assembler +- `SMInst.ret` uses `Addr` (existing type in `cm_inst.py`) for the return route — `Addr.pe` is the destination PE, `Addr.a` is the offset + +**Verification:** + +Run: `python -c "from emu.types import MatchEntry, DeferredRead, PEConfig, SMConfig; from cm_inst import SMInst; print('OK')"` +Expected: `OK` + +**Commit:** `feat: add emu package scaffold, emulator types, and SMInst instruction type` + + + + + +### Task 3: Implement ALU execute() function + +**Verifies:** or1-emu.AC2.1, or1-emu.AC2.2, or1-emu.AC2.3, or1-emu.AC2.4, or1-emu.AC2.5, or1-emu.AC2.6, or1-emu.AC2.7, or1-emu.AC2.8, or1-emu.AC2.9 + +**Files:** +- Create: `emu/alu.py` + +**Implementation:** + +```python +from cm_inst import ALUOp, ArithOp, LogicOp, RoutingOp + +UINT16_MASK = 0xFFFF + + +def to_signed(val: int) -> int: + """Interpret a 16-bit unsigned value as signed 2's complement.""" + return val - 0x10000 if val & 0x8000 else val + + +def execute(op: ALUOp, left: int, right: int | None, const: int | None) -> tuple[int, bool]: + """ + Execute an ALU operation. + + Returns (result & 0xFFFF, bool_out). + All values stored as unsigned 16-bit. Comparisons interpret as signed 2's complement. + """ + if isinstance(op, ArithOp): + return _execute_arith(op, left, right, const) + if isinstance(op, LogicOp): + return _execute_logic(op, left, right) + if isinstance(op, RoutingOp): + return _execute_routing(op, left, right, const) + raise ValueError(f"Unknown ALU operation: {op}") + + +def _execute_arith(op: ArithOp, left: int, right: int | None, const: int | None) -> tuple[int, bool]: + match op: + case ArithOp.ADD: + result = (left + right) & UINT16_MASK + case ArithOp.SUB: + result = (left - right) & UINT16_MASK + case ArithOp.INC: + result = (left + 1) & UINT16_MASK + case ArithOp.DEC: + result = (left - 1) & UINT16_MASK + case ArithOp.SHIFT_L: + result = (left << const) & UINT16_MASK + case ArithOp.SHIFT_R: + result = (left >> const) & UINT16_MASK + case ArithOp.ASHFT_R: + signed = to_signed(left) + result = (signed >> const) & UINT16_MASK + case _: + raise ValueError(f"Unknown arithmetic op: {op}") + return result, False + + +def _execute_logic(op: LogicOp, left: int, right: int | None) -> tuple[int, bool]: + match op: + case LogicOp.AND: + return (left & right) & UINT16_MASK, False + case LogicOp.OR: + return (left | right) & UINT16_MASK, False + case LogicOp.XOR: + return (left ^ right) & UINT16_MASK, False + case LogicOp.NOT: + return (~left) & UINT16_MASK, False + case LogicOp.EQ: + sl, sr = to_signed(left), to_signed(right) + cmp = sl == sr + return (0x0001 if cmp else 0x0000), cmp + case LogicOp.LT: + sl, sr = to_signed(left), to_signed(right) + cmp = sl < sr + return (0x0001 if cmp else 0x0000), cmp + case LogicOp.LTE: + sl, sr = to_signed(left), to_signed(right) + cmp = sl <= sr + return (0x0001 if cmp else 0x0000), cmp + case LogicOp.GT: + sl, sr = to_signed(left), to_signed(right) + cmp = sl > sr + return (0x0001 if cmp else 0x0000), cmp + case LogicOp.GTE: + sl, sr = to_signed(left), to_signed(right) + cmp = sl >= sr + return (0x0001 if cmp else 0x0000), cmp + case _: + raise ValueError(f"Unknown logic op: {op}") + + +def _execute_routing(op: RoutingOp, left: int, right: int | None, const: int | None) -> tuple[int, bool]: + match op: + case RoutingOp.BREQ: + cmp = to_signed(left) == to_signed(right) + return left, cmp + case RoutingOp.BRGT: + cmp = to_signed(left) > to_signed(right) + return left, cmp + case RoutingOp.BRGE: + cmp = to_signed(left) >= to_signed(right) + return left, cmp + case RoutingOp.BROF: + raw = left + right + cmp = raw > UINT16_MASK + return left, cmp + case RoutingOp.SWEQ: + cmp = to_signed(left) == to_signed(right) + return left, cmp + case RoutingOp.SWGT: + cmp = to_signed(left) > to_signed(right) + return left, cmp + case RoutingOp.SWGE: + cmp = to_signed(left) >= to_signed(right) + return left, cmp + case RoutingOp.SWOF: + raw = left + right + cmp = raw > UINT16_MASK + return left, cmp + case RoutingOp.GATE: + cmp = right != 0 + return left, cmp + case RoutingOp.PASS: + return left, False + case RoutingOp.CONST: + return const & UINT16_MASK, False + case RoutingOp.FREE: + return 0, False + case RoutingOp.SEL: + cmp = left != 0 + return (right if cmp else left), cmp + case RoutingOp.MRGE: + return left, False + case _: + raise ValueError(f"Unknown routing op: {op}") +``` + +Key design decisions: +- Pure function, no state, no SimPy dependency +- `isinstance` dispatch on ALUOp subclass hierarchy (matches existing code structure in `cm_inst.py`) +- `to_signed()` helper for 2's complement interpretation +- All results masked to 16-bit unsigned +- `bool_out` is Python `bool` — used by output formatter for routing decisions +- BROF/SWOF use unsigned overflow detection (raw sum > 0xFFFF) +- GATE is dyadic: left=data (port L), right=boolean (port R). bool_out from right operand, result = left (data passthrough). In hardware, bool_out is port R bit 0. +- FREE returns 0 (output formatter will suppress emission) +- SEL: conditional mux — left is the boolean condition, right is the selected value. Returns right when left != 0 (condition true), returns left (which is 0) when condition is false. This is the standard dataflow select: downstream consumer sees either the selected value or zero. + +**Verification:** + +Run: `python -c "from emu.alu import execute; from cm_inst import ArithOp; print(execute(ArithOp.ADD, 1, 2, None))"` +Expected: `(3, False)` + +**Commit:** `feat: implement ALU execute() with full v0 opcode dispatch` + + + +### Task 4: Create test infrastructure and hypothesis strategies + +**Files:** +- Create: `tests/__init__.py` +- Create: `tests/conftest.py` + +**Implementation:** + +`tests/__init__.py` — empty: +```python +``` + +`tests/conftest.py` — shared hypothesis strategies: + +```python +from hypothesis import strategies as st + +from cm_inst import ArithOp, LogicOp, RoutingOp + +uint16 = st.integers(min_value=0, max_value=0xFFFF) +int16 = st.integers(min_value=-32768, max_value=32767) +shift_amount = st.integers(min_value=0, max_value=15) + +arith_dyadic_ops = st.sampled_from([ArithOp.ADD, ArithOp.SUB]) +arith_monadic_ops = st.sampled_from([ArithOp.INC, ArithOp.DEC]) +shift_ops = st.sampled_from([ArithOp.SHIFT_L, ArithOp.SHIFT_R, ArithOp.ASHFT_R]) +logic_dyadic_ops = st.sampled_from([LogicOp.AND, LogicOp.OR, LogicOp.XOR]) +comparison_ops = st.sampled_from([LogicOp.EQ, LogicOp.LT, LogicOp.LTE, LogicOp.GT, LogicOp.GTE]) +branch_ops = st.sampled_from([RoutingOp.BREQ, RoutingOp.BRGT, RoutingOp.BRGE]) +switch_ops = st.sampled_from([RoutingOp.SWEQ, RoutingOp.SWGT, RoutingOp.SWGE]) +overflow_ops = st.sampled_from([RoutingOp.BROF, RoutingOp.SWOF]) +data_routing_ops = st.sampled_from([RoutingOp.SEL, RoutingOp.MRGE]) +``` + +**Verification:** + +Run: `python -c "from tests.conftest import uint16; print(uint16)"` +Expected: `integers(0, 65535)` (or similar strategy repr) + +**Commit:** `feat: add test infrastructure with hypothesis strategies` + + + +### Task 5: ALU property-based tests + +**Verifies:** or1-emu.AC2.1, or1-emu.AC2.2, or1-emu.AC2.3, or1-emu.AC2.4, or1-emu.AC2.5, or1-emu.AC2.6, or1-emu.AC2.7, or1-emu.AC2.8, or1-emu.AC2.9 + +**Files:** +- Create: `tests/test_alu.py` + +**Testing:** + +Tests must verify each AC listed above. The test file should contain property-based tests for each opcode group: + +- **or1-emu.AC2.1** (Arithmetic ADD/SUB): Property — ADD/SUB produce results equal to Python `(a +/- b) & 0xFFFF` for all uint16 pairs. Use `@given(uint16, uint16)`. +- **or1-emu.AC2.2** (INC/DEC monadic): Property — INC(x) == (x+1) & 0xFFFF, DEC(x) == (x-1) & 0xFFFF. Use `@given(uint16)`. Edge `@example(0xFFFF)` for INC wrap, `@example(0)` for DEC wrap. +- **or1-emu.AC2.3** (Shifts with const): Property — SHIFT_L matches `(x << amt) & 0xFFFF`, SHIFT_R matches `x >> amt`, ASHFT_R sign-extends from bit 15. Use `@given(uint16, shift_amount)`. Edge `@example(0x8000, 1)` for ASHFT_R sign extension. +- **or1-emu.AC2.4** (Logic ops): Property — AND/OR/XOR match Python bitwise operators. NOT matches `(~x) & 0xFFFF`. Use `@given(uint16, uint16)` for dyadic, `@given(uint16)` for NOT. +- **or1-emu.AC2.5** (Signed comparison semantics): Property — comparison ops interpret as signed. `@example(0xFFFF, 0x0001)` verifies LT(0xFFFF, 0x0001) is true (-1 < 1). +- **or1-emu.AC2.6** (Comparison result format): Property — all comparison ops return exactly 0x0001 or 0x0000, and bool_out matches (result == 0x0001). Use `@given(uint16, uint16)`. +- **or1-emu.AC2.7** (Routing ops): Property — BR*/SW*/GATE compute boolean and pass left operand through unchanged as result. All are dyadic, use `@given(uint16, uint16)`. GATE: bool_out = right != 0, result = left (data passthrough). +- **or1-emu.AC2.8** (PASS/CONST): Property — PASS returns left operand unchanged, CONST returns const field masked to 16-bit. Use `@given(uint16)` and `@given(uint16)` respectively. +- **or1-emu.AC2.9** (Signed boundary): Explicit `@example` — GT(0x7FFF, 0x8000) returns (0x0001, True) because 32767 > -32768. + +Additional properties: +- All results are in range [0, 0xFFFF] for any uint16 inputs +- bool_out is always a Python bool + +**Verification:** + +Run: `python -m pytest tests/test_alu.py -v` +Expected: All tests pass + +**Commit:** `test: add property-based ALU tests for all v0 opcodes` + + diff --git a/docs/implementation-plans/2026-02-22-or1-emu/phase_02.md b/docs/implementation-plans/2026-02-22-or1-emu/phase_02.md new file mode 100644 --- /dev/null +++ b/docs/implementation-plans/2026-02-22-or1-emu/phase_02.md @@ -0,0 +1,351 @@ +# OR1 Dataflow CPU Behavioural Emulator — Phase 2: Processing Element Core + +**Goal:** PE with matching store, instruction fetch, and output formatter — testable in isolation with SimPy. + +**Architecture:** Single SimPy process per PE. Five pipeline stages as separable methods: token input, cfg check, match/bypass, execute, output format. Matching store is 2D `[ctx_slot][offset]` with generation counters for ABA prevention. Output formatter derives mode from opcode, destination availability, and bool_out. + +**Tech Stack:** Python 3.12, SimPy 4.1, pytest, hypothesis + +**Scope:** 6 phases from original design (phase 2 of 6) + +**Codebase verified:** 2026-02-22 + +--- + +## Acceptance Criteria Coverage + +This phase implements and tests: + +### or1-emu.AC1: Processing Element Behaviour +- **or1-emu.AC1.1 Success:** Monadic token bypasses matching store and executes immediately +- **or1-emu.AC1.2 Success:** First dyadic token for an offset/ctx stores in matching store, does not fire +- **or1-emu.AC1.3 Success:** Second dyadic token for same offset/ctx retrieves partner, fires instruction +- **or1-emu.AC1.4 Success:** Generation counter mismatch causes stale token discard (no match, no fire) +- **or1-emu.AC1.5 Success:** Output formatter SINGLE mode emits exactly one token to dest_l +- **or1-emu.AC1.6 Success:** Output formatter DUAL mode emits two tokens with same data to dest_l and dest_r +- **or1-emu.AC1.7 Success:** Output formatter SWITCH mode routes data to taken side, inline trigger to not-taken side +- **or1-emu.AC1.8 Success:** Output formatter SUPPRESS mode emits zero tokens +- **or1-emu.AC1.9 Failure:** Token targeting non-existent IRAM offset is handled without crash + +--- + +## Design Notes + +**BR* ops (BREQ, BRGT, BRGE, BROF):** These are intended to have EM-4 style internal routing semantics that the v0 PE spec does not currently include. For this implementation, BR* ops with two destinations use DUAL mode (emitting to both). Their ALU bool_out is computed but not used for output routing. Future revisions may add internal conditional routing for BR* ops. + +**SM instructions (SMInst):** IRAM entries can be either `ALUInst` (PE→PE via ALU) or `SMInst` (PE→SM via direct token construction). When the PE fetches an `SMInst`, it bypasses the ALU entirely and constructs an `SMToken` from the instruction fields and token operands. This mirrors the hardware where the instruction encoding determines the output token type. The PE dispatches on `isinstance(inst, SMInst)` in its main loop. + +--- + + + +### Task 1: Implement ProcessingElement class — matching store and process loop + +**Verifies:** or1-emu.AC1.1, or1-emu.AC1.2, or1-emu.AC1.3, or1-emu.AC1.4, or1-emu.AC1.9 + +**Files:** +- Create: `emu/pe.py` + +**Implementation:** + +```python +import logging +from typing import Optional + +import simpy + +from cm_inst import ALUInst, Addr, RoutingOp, SMInst +from emu.alu import execute +from emu.types import MatchEntry +from tokens import CMToken, CfgToken, DyadToken, MemOp, MonadToken, Port, SMToken + +logger = logging.getLogger(__name__) + + +class ProcessingElement: + def __init__( + self, + env: simpy.Environment, + pe_id: int, + iram: dict[int, ALUInst | SMInst], + ctx_slots: int = 4, + offsets: int = 64, + fifo_capacity: int = 8, + ): + self.env = env + self.pe_id = pe_id + self.iram = iram + self.input_store: simpy.Store = simpy.Store(env, capacity=fifo_capacity) + self.route_table: dict[int, simpy.Store] = {} + self.sm_routes: dict[int, simpy.Store] = {} + self.matching_store: list[list[MatchEntry]] = [ + [MatchEntry() for _ in range(offsets)] + for _ in range(ctx_slots) + ] + self.gen_counters: list[int] = [0] * ctx_slots + self._ctx_slots = ctx_slots + self._offsets = offsets + self.process = env.process(self._run()) + + def _run(self): + while True: + token = yield self.input_store.get() + + if isinstance(token, CfgToken): + self._handle_cfg(token) + continue + + if isinstance(token, MonadToken): + operands = self._match_monadic(token) + elif isinstance(token, DyadToken): + operands = self._match_dyadic(token) + else: + logger.warning("PE%d: unknown token type: %s", self.pe_id, type(token)) + continue + + if operands is None: + continue + + left, right = operands + inst = self._fetch(token.offset) + if inst is None: + logger.warning("PE%d: no IRAM entry at offset %d", self.pe_id, token.offset) + continue + + if isinstance(inst, SMInst): + yield from self._emit_sm(inst, left, right) + else: + result, bool_out = execute(inst.op, left, right, inst.const) + yield from self._emit(inst, result, bool_out, token.ctx) + + def _handle_cfg(self, token: CfgToken) -> None: + pass + + def _match_monadic(self, token: MonadToken) -> tuple[int, None]: + return (token.data, None) + + def _match_dyadic(self, token: DyadToken) -> Optional[tuple[int, int]]: + ctx = token.ctx % self._ctx_slots + offset = token.offset % self._offsets + + if token.gen != self.gen_counters[ctx]: + logger.debug( + "PE%d: stale token discarded (gen %d != %d) at ctx=%d off=%d", + self.pe_id, token.gen, self.gen_counters[ctx], ctx, offset, + ) + return None + + entry = self.matching_store[ctx][offset] + + if not entry.occupied: + entry.occupied = True + entry.data = token.data + entry.port = token.port + return None + + partner_data = entry.data + partner_port = entry.port + entry.occupied = False + entry.data = None + + if partner_port == Port.L: + return (partner_data, token.data) + else: + return (token.data, partner_data) + + def _fetch(self, offset: int) -> Optional[ALUInst | SMInst]: + return self.iram.get(offset) + + def _emit(self, inst: ALUInst, result: int, bool_out: bool, ctx: int): + mode = self._output_mode(inst, bool_out) + + if mode == "SUPPRESS": + return + + if mode == "SINGLE": + out_token = self._make_output_token(inst.dest_l, result, ctx) + yield self.route_table[inst.dest_l.pe].put(out_token) + + elif mode == "DUAL": + out_l = self._make_output_token(inst.dest_l, result, ctx) + out_r = self._make_output_token(inst.dest_r, result, ctx) + yield self.route_table[inst.dest_l.pe].put(out_l) + yield self.route_table[inst.dest_r.pe].put(out_r) + + elif mode == "SWITCH": + if bool_out: + taken, not_taken = inst.dest_l, inst.dest_r + else: + taken, not_taken = inst.dest_r, inst.dest_l + + data_token = self._make_output_token(taken, result, ctx) + yield self.route_table[taken.pe].put(data_token) + + trigger_token = MonadToken( + target=not_taken.pe, + offset=not_taken.a, + ctx=ctx, + data=0, + inline=True, + ) + yield self.route_table[not_taken.pe].put(trigger_token) + + def _emit_sm(self, inst: SMInst, left: int, right: int | None): + cell_addr = inst.const if inst.const is not None else left + data = left if inst.const is not None else right + + ret: CMToken | None = None + if inst.ret is not None: + ret = CMToken( + target=inst.ret.pe, + offset=inst.ret.a, + ctx=0, + data=0, + ) + + sm_token = SMToken( + target=cell_addr, + op=inst.op, + flags=left if inst.op == MemOp.CMP_SW and right is not None else None, + data=data, + ret=ret, + ) + yield self.sm_routes[inst.sm_id].put(sm_token) + + def _output_mode(self, inst: ALUInst, bool_out: bool) -> str: + if inst.op == RoutingOp.FREE: + return "SUPPRESS" + if inst.op == RoutingOp.GATE and not bool_out: + return "SUPPRESS" + if inst.dest_l is None: + return "SUPPRESS" + if inst.dest_r is None: + return "SINGLE" + if isinstance(inst.op, RoutingOp) and inst.op in ( + RoutingOp.SWEQ, RoutingOp.SWGT, RoutingOp.SWGE, RoutingOp.SWOF, + ): + return "SWITCH" + return "DUAL" + + def _make_output_token(self, dest: Addr, data: int, ctx: int) -> DyadToken: + return DyadToken( + target=dest.pe, + offset=dest.a, + ctx=ctx, + data=data, + port=dest.port, + gen=0, + wide=False, + ) +``` + +Key design decisions: +- Single SimPy process via `env.process(self._run())` +- Stage methods are separable (can later be split into independent SimPy processes) +- Matching store uses modular indexing for ctx/offset bounds safety +- **Instruction dispatch:** `_run()` checks `isinstance(inst, SMInst)` after fetch. SM instructions bypass the ALU entirely and go to `_emit_sm()`. ALU instructions go through `execute()` → `_emit()` as before. +- `_emit` handles only `Addr` destinations (PE→PE routing via `route_table`). Clean separation — no type dispatch in the output path. +- `_emit_sm` constructs `SMToken` from `SMInst` fields + operands, routes via `sm_routes[sm_id]`. Operand mapping: if `const` is set, cell_addr = const and data = left; otherwise cell_addr = left, data = right. For CMP_SW (dyadic), flags = left (expected), data = right (new value). +- `_output_mode` accepts `bool_out` for GATE conditional: GATE-when-false returns SUPPRESS immediately. GATE-when-true falls through to normal dest_l/dest_r logic (SINGLE if only dest_l, DUAL if both dests). This matches the hardware where GATE's output mode is conditional on bool_out via EEPROM address input. +- BR* ops with two dests get DUAL mode (v0 behaviour — EM-4 internal routing deferred) +- SW* ops with two dests get SWITCH mode +- `_make_output_token` creates DyadToken with gen=0 — this is correct for v0 where all ctx/gen values are compile-time constants and gen_counters initialize to 0. Future versions may source gen from the IRAM destination field (Addr does not carry gen in v0). +- SWITCH not-taken path creates MonadToken inline trigger +- CfgToken handling is a stub (stretch goal per design) + +**Verification:** + +Run: `python -c "import simpy; from emu.pe import ProcessingElement; env = simpy.Environment(); pe = ProcessingElement(env, 0, {}); print('OK')"` +Expected: `OK` + +**Commit:** `feat: implement ProcessingElement with matching store and output formatter` + + + +### Task 2: Add PE-specific strategies to conftest.py + +**Files:** +- Modify: `tests/conftest.py` + +**Implementation:** + +Append these strategies to the existing `tests/conftest.py`: + +```python +from tokens import DyadToken, MonadToken, Port + +@st.composite +def dyad_token(draw, target: int = 0, offset: int | None = None, ctx: int | None = None, gen: int | None = None) -> DyadToken: + return DyadToken( + target=target, + offset=draw(st.integers(min_value=0, max_value=63)) if offset is None else offset, + ctx=draw(st.integers(min_value=0, max_value=3)) if ctx is None else ctx, + data=draw(uint16), + port=draw(st.sampled_from(list(Port))), + gen=draw(st.integers(min_value=0, max_value=3)) if gen is None else gen, + wide=False, + ) + +@st.composite +def monad_token(draw, target: int = 0, offset: int | None = None, ctx: int | None = None) -> MonadToken: + return MonadToken( + target=target, + offset=draw(st.integers(min_value=0, max_value=63)) if offset is None else offset, + ctx=draw(st.integers(min_value=0, max_value=3)) if ctx is None else ctx, + data=draw(uint16), + inline=False, + ) +``` + +**Verification:** + +Run: `python -c "from tests.conftest import dyad_token, monad_token; print('OK')"` +Expected: `OK` + +**Commit:** `feat: add PE token strategies to test conftest` + + + +### Task 3: PE matching store and output formatter tests + +**Verifies:** or1-emu.AC1.1, or1-emu.AC1.2, or1-emu.AC1.3, or1-emu.AC1.4, or1-emu.AC1.5, or1-emu.AC1.6, or1-emu.AC1.7, or1-emu.AC1.8, or1-emu.AC1.9 + +**Files:** +- Create: `tests/test_pe.py` + +**Testing:** + +Tests must verify each AC listed above. Mix of property-based tests (matching store invariants) and SimPy-based functional tests (output formatter modes): + +**Matching store tests (hypothesis):** +- **or1-emu.AC1.1:** Monadic token bypasses matching — send MonadToken with PASS instruction, verify output token emitted with same data. SimPy test: create PE with PASS at offset 0 (dest_l only → SINGLE), inject MonadToken, run sim, check output store. +- **or1-emu.AC1.2:** First dyadic stores partial — send one DyadToken, verify no output and matching store entry is occupied. Property: for any valid DyadToken, after first injection the matching_store[ctx][offset].occupied is True. +- **or1-emu.AC1.3:** Second dyadic fires — send two DyadTokens with same ctx/offset but different ports, verify output token emitted. SimPy test: inject L then R token, check output. +- **or1-emu.AC1.4:** Stale token discarded — send DyadToken with gen != PE's gen_counter for that ctx, verify no output and matching store unchanged. Property: for any token where gen != gen_counter, matching_store remains unmodified. + +**Output formatter tests (SimPy):** +- **or1-emu.AC1.5:** SINGLE mode — IRAM with ADD and only dest_l set (dest_r=None). Inject two dyadic tokens. Verify exactly 1 output token in destination store. +- **or1-emu.AC1.6:** DUAL mode — IRAM with ADD and both dest_l and dest_r set (non-SW* op). Inject two dyadic tokens. Verify exactly 2 output tokens, both with same data. +- **or1-emu.AC1.7:** SWITCH mode — IRAM with SWEQ and both dests. Inject two equal tokens → verify data goes to dest_l, trigger to dest_r. Inject two unequal tokens → verify data goes to dest_r, trigger to dest_l. Verify trigger is MonadToken with inline=True. +- **or1-emu.AC1.8:** SUPPRESS mode — Two sub-tests: + - FREE instruction: Inject MonadToken. Verify 0 output tokens. + - GATE with false condition: IRAM with GATE and dest_l. Inject two DyadTokens — port L with data=42, port R with data=0 (boolean false → GATE suppresses). Verify 0 output tokens. + - Also verify GATE with true condition: IRAM with GATE and dest_l. Inject two DyadTokens — port L with data=42, port R with data=1 (boolean true → GATE passes). Verify 1 output token with data=42 (SINGLE mode, data from port L passed through). +- **or1-emu.AC1.9:** Non-existent offset — inject token targeting offset not in IRAM. Verify no crash, no output tokens. + +Each SimPy test should: +1. Create `simpy.Environment()` +2. Create PE with minimal IRAM for the test case +3. Wire a collector `simpy.Store` as the route_table entry +4. Inject token(s) via `pe.input_store.items.append(token)` (direct injection for test setup) +5. `env.run(until=100)` (bounded to avoid hangs) +6. Assert on collector store contents + +**Verification:** + +Run: `python -m pytest tests/test_pe.py -v` +Expected: All tests pass + +**Commit:** `test: add PE matching store and output formatter tests` + + diff --git a/docs/implementation-plans/2026-02-22-or1-emu/phase_03.md b/docs/implementation-plans/2026-02-22-or1-emu/phase_03.md new file mode 100644 --- /dev/null +++ b/docs/implementation-plans/2026-02-22-or1-emu/phase_03.md @@ -0,0 +1,318 @@ +# OR1 Dataflow CPU Behavioural Emulator — Phase 3: Structure Memory + +**Goal:** SM with I-structure semantics, depth-1 deferred read register, and all v0 memory operations. + +**Architecture:** Single SimPy process per SM. Cell state machine with 2-bit presence (EMPTY/RESERVED/FULL/WAITING). Depth-1 deferred read uses a `simpy.Event` for blocking when the deferred register is occupied. The emulator's SM creates independent SMCell instances (avoiding the shared-reference footgun in `sm_mod.py:29`). + +**Tech Stack:** Python 3.12, SimPy 4.1, pytest, hypothesis + +**Scope:** 6 phases from original design (phase 3 of 6) + +**Codebase verified:** 2026-02-22 + +--- + +## Acceptance Criteria Coverage + +This phase implements and tests: + +### or1-emu.AC3: Structure Memory Behaviour +- **or1-emu.AC3.1 Success:** READ on FULL cell returns data immediately via result token +- **or1-emu.AC3.2 Success:** READ on EMPTY cell with empty deferred register stashes return route, sets WAITING +- **or1-emu.AC3.3 Success:** WRITE on WAITING cell satisfies deferred read — emits result token to stashed return route +- **or1-emu.AC3.4 Success:** WRITE on EMPTY/RESERVED sets cell to FULL +- **or1-emu.AC3.5 Success:** CLEAR sets cell to EMPTY, cancels deferred read if targeting that cell +- **or1-emu.AC3.6 Success:** READ_INC/READ_DEC atomically modify and return value (lower 256 cells only) +- **or1-emu.AC3.7 Failure:** Depth-1 constraint: second blocking READ on different empty cell stalls until first deferred read is satisfied +- **or1-emu.AC3.8 Edge:** WRITE on FULL cell overwrites data (diagnostic flag set if modelled) +- **or1-emu.AC3.9 Success:** CAS on FULL cell: if current value == expected (SMToken.flags), writes new value (SMToken.data) and returns old value; if mismatch, cell unchanged and returns old value (lower 256 cells only) + +--- + + + +### Task 1: Implement StructureMemory class + +**Verifies:** or1-emu.AC3.1, or1-emu.AC3.2, or1-emu.AC3.3, or1-emu.AC3.4, or1-emu.AC3.5, or1-emu.AC3.6, or1-emu.AC3.7, or1-emu.AC3.8, or1-emu.AC3.9 + +**Files:** +- Create: `emu/sm.py` + +**Implementation:** + +```python +import logging +from typing import Optional + +import simpy + +from emu.types import DeferredRead +from sm_mod import Presence, SMCell +from tokens import CMToken, MemOp, MonadToken, SMToken + +logger = logging.getLogger(__name__) + +ATOMIC_CELL_LIMIT = 256 + + +class StructureMemory: + def __init__( + self, + env: simpy.Environment, + sm_id: int, + cell_count: int = 512, + fifo_capacity: int = 8, + ): + self.env = env + self.sm_id = sm_id + self.cells: list[SMCell] = [SMCell(Presence.EMPTY, None, None) for _ in range(cell_count)] + self.deferred_read: Optional[DeferredRead] = None + self._deferred_satisfied: Optional[simpy.Event] = None + self._deferred_cancelled: bool = False + self.input_store: simpy.Store = simpy.Store(env, capacity=fifo_capacity) + self.route_table: dict[int, simpy.Store] = {} + self.process = env.process(self._run()) + + def _run(self): + while True: + token = yield self.input_store.get() + + if not isinstance(token, SMToken): + logger.warning("SM%d: unexpected token type: %s", self.sm_id, type(token)) + continue + + addr = token.target + op = token.op + + match op: + case MemOp.READ: + yield from self._handle_read(addr, token) + case MemOp.WRITE: + yield from self._handle_write(addr, token) + case MemOp.CLEAR: + self._handle_clear(addr) + case MemOp.RD_INC: + yield from self._handle_atomic(addr, token, delta=1) + case MemOp.RD_DEC: + yield from self._handle_atomic(addr, token, delta=-1) + case MemOp.CMP_SW: + yield from self._handle_cas(addr, token) + case MemOp.ALLOC: + self._handle_alloc(addr) + case MemOp.FREE: + self._handle_clear(addr) + case _: + logger.warning("SM%d: unknown op %s", self.sm_id, op) + + def _handle_read(self, addr: int, token: SMToken): + cell = self.cells[addr] + + if cell.pres == Presence.FULL: + yield from self._send_result(token.ret, cell.data_l) + return + + if self.deferred_read is not None: + self._deferred_satisfied = self.env.event() + yield self._deferred_satisfied + self._deferred_satisfied = None + if self._deferred_cancelled: + self._deferred_cancelled = False + return + yield from self._handle_read(addr, token) + return + + self.deferred_read = DeferredRead(cell_addr=addr, return_route=token.ret) + cell.pres = Presence.WAITING + + def _handle_write(self, addr: int, token: SMToken): + cell = self.cells[addr] + + if cell.pres == Presence.WAITING and self.deferred_read is not None and self.deferred_read.cell_addr == addr: + return_route = self.deferred_read.return_route + self.deferred_read = None + cell.pres = Presence.FULL + cell.data_l = token.data + if self._deferred_satisfied is not None: + self._deferred_satisfied.succeed() + yield from self._send_result(return_route, token.data) + return + + cell.pres = Presence.FULL + cell.data_l = token.data + + def _handle_clear(self, addr: int): + cell = self.cells[addr] + cell.pres = Presence.EMPTY + cell.data_l = None + cell.data_r = None + + if self.deferred_read is not None and self.deferred_read.cell_addr == addr: + self.deferred_read = None + self._deferred_cancelled = True + if self._deferred_satisfied is not None: + self._deferred_satisfied.succeed() + + def _handle_alloc(self, addr: int): + cell = self.cells[addr] + if cell.pres == Presence.EMPTY: + cell.pres = Presence.RESERVED + + def _handle_atomic(self, addr: int, token: SMToken, delta: int): + if addr >= ATOMIC_CELL_LIMIT: + logger.warning("SM%d: atomic op on cell %d >= %d", self.sm_id, addr, ATOMIC_CELL_LIMIT) + return + + cell = self.cells[addr] + if cell.pres != Presence.FULL: + logger.warning("SM%d: atomic op on non-FULL cell %d", self.sm_id, addr) + return + + old_value = cell.data_l if cell.data_l is not None else 0 + cell.data_l = (old_value + delta) & 0xFFFF + yield from self._send_result(token.ret, old_value) + + def _handle_cas(self, addr: int, token: SMToken): + if addr >= ATOMIC_CELL_LIMIT: + logger.warning("SM%d: CAS on cell %d >= %d", self.sm_id, addr, ATOMIC_CELL_LIMIT) + return + + cell = self.cells[addr] + if cell.pres != Presence.FULL: + logger.warning("SM%d: CAS on non-FULL cell %d", self.sm_id, addr) + return + + old_value = cell.data_l if cell.data_l is not None else 0 + expected = token.flags if token.flags is not None else 0 + if old_value == expected: + cell.data_l = token.data + yield from self._send_result(token.ret, old_value) + + def _send_result(self, return_route: CMToken, data: int): + result = MonadToken( + target=return_route.target, + offset=return_route.offset, + ctx=return_route.ctx, + data=data, + inline=False, + ) + yield self.route_table[return_route.target].put(result) +``` + +Key design decisions: +- Independent SMCell instances (list comprehension, not `[SMCell(...)] * n`) +- `deferred_read` is a single register — depth-1 constraint +- When deferred register is occupied, SM yields on `_deferred_satisfied` event until the existing deferred is satisfied, then retries recursively +- `_send_result` creates a MonadToken using the return route's target/offset/ctx +- WRITE on FULL cell silently overwrites (per AC3.8 — diagnostic flag is optional) +- ALLOC transitions EMPTY → RESERVED +- FREE delegates to CLEAR +- Atomic ops (RD_INC, RD_DEC) return old value, modify cell, restricted to lower 256 cells +- CAS has full compare-and-swap semantics: if cell value matches expected (flags), writes new value and returns old; if mismatch, cell unchanged, returns old value + +**Verification:** + +Run: `python -c "import simpy; from emu.sm import StructureMemory; env = simpy.Environment(); sm = StructureMemory(env, 0); print('OK')"` +Expected: `OK` + +**Commit:** `feat: implement StructureMemory with I-structure semantics and deferred reads` + + + +### Task 2: Add SM strategies to conftest.py + +**Files:** +- Modify: `tests/conftest.py` + +**Implementation:** + +Append SM-specific strategies: + +```python +from sm_mod import Presence +from tokens import MemOp, SMToken, CMToken + +sm_read_write_ops = st.sampled_from([MemOp.READ, MemOp.WRITE]) +sm_all_ops = st.sampled_from(list(MemOp)) + +@st.composite +def sm_token(draw, addr=None, op=None, data=None): + _addr = draw(st.integers(min_value=0, max_value=511)) if addr is None else addr + _op = draw(sm_all_ops) if op is None else op + _data = draw(uint16) if data is None else data + ret = CMToken(target=0, offset=0, ctx=0, data=0) + return SMToken( + target=_addr, + op=_op, + flags=None, + data=_data, + ret=ret, + ) + +@st.composite +def sm_return_route(draw, target=0): + return CMToken( + target=target, + offset=draw(st.integers(min_value=0, max_value=63)), + ctx=draw(st.integers(min_value=0, max_value=3)), + data=0, + ) +``` + +**Verification:** + +Run: `python -c "from tests.conftest import sm_token; print('OK')"` +Expected: `OK` + +**Commit:** `feat: add SM token strategies to test conftest` + + + +### Task 3: Structure Memory tests + +**Verifies:** or1-emu.AC3.1, or1-emu.AC3.2, or1-emu.AC3.3, or1-emu.AC3.4, or1-emu.AC3.5, or1-emu.AC3.6, or1-emu.AC3.7, or1-emu.AC3.8, or1-emu.AC3.9 + +**Files:** +- Create: `tests/test_sm.py` + +**Testing:** + +Tests must verify each AC listed above. Mix of SimPy functional tests and hypothesis property tests: + +**State machine tests (SimPy):** +- **or1-emu.AC3.1:** READ on FULL — pre-populate cell to FULL with known data. Send READ token with return route. Verify result token appears in collector store with correct data. +- **or1-emu.AC3.2:** READ on EMPTY — cell starts EMPTY, deferred register empty. Send READ token. Verify cell transitions to WAITING and no result token emitted yet. +- **or1-emu.AC3.3:** Deferred read satisfaction — cell is WAITING with stashed return route. Send WRITE token. Verify result token emitted to stashed return route with written data, cell becomes FULL. +- **or1-emu.AC3.4:** WRITE on EMPTY/RESERVED — send WRITE to EMPTY cell. Verify cell becomes FULL with correct data_l. Repeat for RESERVED. +- **or1-emu.AC3.5:** CLEAR — set cell to FULL with data. Send CLEAR. Verify cell is EMPTY, data_l is None. Also: cell in WAITING with deferred read targeting it → CLEAR cancels the deferred read. +- **or1-emu.AC3.8:** WRITE on FULL — cell already FULL with data X. WRITE with data Y. Verify cell still FULL, data_l is now Y (overwrite). + +**Deferred read depth-1 tests (SimPy):** +- **or1-emu.AC3.7:** Two blocking READs — send READ to cell A (EMPTY, deferred register fills). Send READ to cell B (EMPTY, deferred register occupied → SM stalls). Then send WRITE to cell A (satisfies first deferred). Verify SM unblocks and processes second READ (cell B becomes WAITING). Then send WRITE to cell B. Verify both result tokens eventually arrive. + +**Atomic operation tests (SimPy):** +- **or1-emu.AC3.6:** RD_INC — cell 100 is FULL with value 42. Send RD_INC. Verify result token has data 42 (old value), cell now has data_l 43. RD_DEC similarly. Test wrap: RD_INC on 0xFFFF → cell becomes 0, returns 0xFFFF. Test addr >= 256 is rejected. +- **or1-emu.AC3.9:** CAS — two sub-tests: + - Match case: cell 50 is FULL with value 10. Send CMP_SW with flags=10 (expected), data=99 (new). Verify result token has data 10 (old value), cell now has data_l 99 (swap happened). + - Mismatch case: cell 50 is FULL with value 10. Send CMP_SW with flags=20 (expected), data=99 (new). Verify result token has data 10 (old value), cell still has data_l 10 (no swap). + - Test addr >= 256 is rejected. + +**Property-based (hypothesis):** +- Presence state machine invariant: any valid sequence of READ/WRITE/CLEAR operations always leaves cells in a valid Presence state (EMPTY, RESERVED, FULL, or WAITING — never an invalid combination). +- WRITE always sets data_l to the written value regardless of prior cell state. + +Each SimPy test should: +1. Create `simpy.Environment()` +2. Create SM with appropriate initial cell state +3. Wire collector `simpy.Store` as route_table entry +4. Inject token(s) via `sm.input_store.put(token)` (no yield needed outside SimPy process — use `Store.put()` directly on items list for test setup, or wrap in a helper process) +5. `env.run(until=100)` (bounded) +6. Assert on collector store contents and cell state + +**Verification:** + +Run: `python -m pytest tests/test_sm.py -v` +Expected: All tests pass + +**Commit:** `test: add SM state machine, deferred read, and atomic operation tests` + + diff --git a/docs/implementation-plans/2026-02-22-or1-emu/phase_04.md b/docs/implementation-plans/2026-02-22-or1-emu/phase_04.md new file mode 100644 --- /dev/null +++ b/docs/implementation-plans/2026-02-22-or1-emu/phase_04.md @@ -0,0 +1,204 @@ +# OR1 Dataflow CPU Behavioural Emulator — Phase 4: Network and Topology + +**Goal:** Route table wiring, System object, token routing, and backpressure. + +**Architecture:** `build_topology()` creates all PE and SM instances, wires route_tables bidirectionally (PE→PE, PE→SM, SM→PE). Route tables are split into `pe_routes` and `sm_routes` to prevent ID collisions between PE and SM namespaces. No central router — modules resolve destinations and put directly. System object is the top-level handle providing `inject()`, module accessors, and `env`. + +**Tech Stack:** Python 3.12, SimPy 4.1, pytest + +**Scope:** 6 phases from original design (phase 4 of 6) + +**Codebase verified:** 2026-02-22 + +--- + +## Acceptance Criteria Coverage + +This phase implements and tests: + +### or1-emu.AC4: Network and Routing +- **or1-emu.AC4.1 Success:** Token with dest PE_id N arrives at PE N's input Store +- **or1-emu.AC4.2 Success:** SM token (type 10) routes to correct SM by SM_id +- **or1-emu.AC4.3 Success:** Backpressure: PE blocks on put() when destination Store is at capacity +- **or1-emu.AC4.4 Success:** Backpressure releases when consumer drains destination Store + +--- + +## Design Notes + +**Type-based routing:** The hardware uses token-type-based routing — the CM can emit any token type. CM tokens (type 00/01) route to PEs, SM tokens (type 10) route to SMs. The emulator mirrors this: `ALUInst` instructions produce CM tokens (routed via PE `route_table`), `SMInst` instructions produce SM tokens (routed via PE `sm_routes`). Each PE has both `route_table: dict[int, Store]` (PE→PE) and `sm_routes: dict[int, Store]` (PE→SM). SM modules have `route_table: dict[int, Store]` (SM→PE, for result tokens). + +**Route table separation:** PE and SM modules use separate route dicts to avoid ID namespace collisions. A system with PE 0 and SM 0 routes correctly because they're in different dicts. + +**Unified inject:** `System.inject()` accepts `CMToken` (the base class for DyadToken and MonadToken) and routes to PEs by `token.target`. SMTokens require `inject_sm(sm_id, token)` since `SMToken.target` is the cell address, not the SM module ID — the caller must specify which SM to target. + +--- + + + +### Task 1: Implement build_topology() and System class + +**Verifies:** or1-emu.AC4.1, or1-emu.AC4.2 + +**Files:** +- Create: `emu/network.py` + +**Implementation:** + +```python +import simpy + +from emu.pe import ProcessingElement +from emu.sm import StructureMemory +from emu.types import PEConfig, SMConfig +from sm_mod import Presence, SMCell +from tokens import CMToken, SMToken + + +class System: + def __init__( + self, + env: simpy.Environment, + pes: dict[int, ProcessingElement], + sms: dict[int, StructureMemory], + ): + self.env = env + self.pes = pes + self.sms = sms + + def inject(self, token: CMToken) -> None: + """Inject a seed CM token into the target PE's input store.""" + self.pes[token.target].input_store.items.append(token) + + def inject_sm(self, sm_id: int, token: SMToken) -> None: + """Inject a seed SM token into a specific SM's input store.""" + self.sms[sm_id].input_store.items.append(token) + + +def build_topology( + env: simpy.Environment, + pe_configs: list[PEConfig], + sm_configs: list[SMConfig], + fifo_capacity: int = 8, +) -> System: + pes: dict[int, ProcessingElement] = {} + sms: dict[int, StructureMemory] = {} + + for cfg in pe_configs: + pe = ProcessingElement( + env=env, + pe_id=cfg.pe_id, + iram=cfg.iram, + ctx_slots=cfg.ctx_slots, + offsets=cfg.offsets, + fifo_capacity=fifo_capacity, + ) + if cfg.gen_counters is not None: + pe.gen_counters = list(cfg.gen_counters) + pes[cfg.pe_id] = pe + + for cfg in sm_configs: + sm = StructureMemory( + env=env, + sm_id=cfg.sm_id, + cell_count=cfg.cell_count, + fifo_capacity=fifo_capacity, + ) + if cfg.initial_cells is not None: + for addr, (pres, data) in cfg.initial_cells.items(): + sm.cells[addr] = SMCell(pres, data, None) + sms[cfg.sm_id] = sm + + pe_stores: dict[int, simpy.Store] = {pe_id: pe.input_store for pe_id, pe in pes.items()} + sm_stores: dict[int, simpy.Store] = {sm_id: sm.input_store for sm_id, sm in sms.items()} + + for pe in pes.values(): + pe.route_table.update(pe_stores) + pe.sm_routes.update(sm_stores) + + for sm in sms.values(): + sm.route_table.update(pe_stores) + + return System(env, pes, sms) +``` + +Key design decisions: +- `inject()` takes a `CMToken` (base class for DyadToken and MonadToken) and routes to PEs by `token.target`. Appends directly to items list (bypasses SimPy event system — for seed token insertion before simulation runs). +- `inject_sm()` takes an explicit `sm_id` parameter plus an SMToken. This avoids the problem of SMToken.target being the cell address (not SM module ID). The caller specifies which SM module to target. +- PE `route_table` maps PE IDs → PE input Stores (PE→PE routing). PE `sm_routes` maps SM IDs → SM input Stores (PE→SM routing). This mirrors the hardware's type-based routing where different token types route to different module types. +- SM `route_table` maps PE IDs → PE input Stores (SM→PE routing for result tokens). +- PE and SM ID spaces are naturally separated — PE uses `route_table` for PE destinations and `sm_routes` for SM destinations. + +**Verification:** + +Run: +```python +python -c " +import simpy +from emu.network import build_topology +from emu.types import PEConfig, SMConfig +env = simpy.Environment() +sys = build_topology(env, [PEConfig(0, {}), PEConfig(1, {})], [SMConfig(0)]) +print('PEs:', list(sys.pes.keys())) +print('SMs:', list(sys.sms.keys())) +print('OK') +" +``` +Expected: `PEs: [0, 1]`, `SMs: [0]`, `OK` + +**Commit:** `feat: implement build_topology() and System class for network wiring` + + + +### Task 2: Update emu/__init__.py with public API + +**Files:** +- Modify: `emu/__init__.py` + +**Implementation:** + +```python +from emu.network import System, build_topology +from emu.types import PEConfig, SMConfig +``` + +**Verification:** + +Run: `python -c "from emu import build_topology, System, PEConfig, SMConfig; print('OK')"` +Expected: `OK` + +**Commit:** `feat: expose public API from emu package` + + + +### Task 3: Network routing and backpressure tests + +**Verifies:** or1-emu.AC4.1, or1-emu.AC4.2, or1-emu.AC4.3, or1-emu.AC4.4 + +**Files:** +- Create: `tests/test_network.py` + +**Testing:** + +Tests must verify each AC listed above: + +- **or1-emu.AC4.1:** PE routing — build topology with PE0 and PE1. Configure PE0 with PASS instruction at offset 0, dest_l targeting PE1. Inject MonadToken to PE0 offset 0. Run sim. Verify PE1's input_store receives a token (PE0 processed PASS and routed result to PE1). + +- **or1-emu.AC4.2:** SM routing — two sub-tests: + - **Direct injection:** Build topology with PE0 and SM0. Use `sys.inject_sm(0, sm_token)` to place a READ token for a FULL cell. Run sim. Verify the result token arrives at the PE specified in the SMToken's return route. + - **PE emission via SMInst:** Build topology with PE0 and SM0. Configure PE0 with an `SMInst(op=MemOp.WRITE, sm_id=0, const=5)` at offset 0. Inject MonadToken with data=42 to PE0. Run sim. Verify SM0's cell 5 becomes FULL with data 42. + +- **or1-emu.AC4.3:** Backpressure blocking — build topology with fifo_capacity=2. PE0 has CONST instruction (monadic, emits DyadToken to PE1). PE1 has no IRAM (tokens accumulate in input_store). Inject 4+ MonadTokens to PE0. After PE1's input_store fills (2 items), PE0 blocks on `put()`. + + Verify: run sim with `env.run(until=100)`. Check PE1.input_store has exactly `fifo_capacity` items. PE0 still has unprocessed tokens in its input_store. + +- **or1-emu.AC4.4:** Backpressure release — continuation of AC4.3. Add a consumer process that drains PE1's store. Run sim further. Verify PE0 unblocks and processes all remaining tokens. + +**Verification:** + +Run: `python -m pytest tests/test_network.py -v` +Expected: All tests pass + +**Commit:** `test: add network routing and backpressure tests` + + diff --git a/docs/implementation-plans/2026-02-22-or1-emu/phase_05.md b/docs/implementation-plans/2026-02-22-or1-emu/phase_05.md new file mode 100644 --- /dev/null +++ b/docs/implementation-plans/2026-02-22-or1-emu/phase_05.md @@ -0,0 +1,73 @@ +# OR1 Dataflow CPU Behavioural Emulator — Phase 5: Direct Initialization API + +**Goal:** Python-native setup of IRAM, SM contents, and routes without CfgTokens. + +**Architecture:** `build_topology()` (implemented in Phase 4) already accepts PEConfig/SMConfig and initialises IRAM and SM cells at construction time. This phase validates that the initialization API works correctly end-to-end and adds any convenience helpers needed for testing. + +**Tech Stack:** Python 3.12, SimPy 4.1, pytest + +**Scope:** 6 phases from original design (phase 5 of 6) + +**Codebase verified:** 2026-02-22 + +--- + +## Acceptance Criteria Coverage + +This phase implements and tests: + +### or1-emu.AC5: Direct Initialization API +- **or1-emu.AC5.1 Success:** System constructed from PEConfig with IRAM contents — PE has expected instructions at expected offsets +- **or1-emu.AC5.2 Success:** System constructed from SMConfig with initial cell data — SM cells match config +- **or1-emu.AC5.3 Success:** inject(token) delivers seed CM token to correct PE's input Store; inject_sm(sm_id, token) delivers seed SM token to correct SM's input Store. SM injection requires explicit sm_id because SMToken.target is the cell address, not the module ID. + +--- + + + +### Task 1: Initialization API smoke tests + +**Verifies:** or1-emu.AC5.1, or1-emu.AC5.2, or1-emu.AC5.3 + +**Files:** +- Create: `tests/test_integration.py` + +**Testing:** + +Tests must verify each AC listed above: + +- **or1-emu.AC5.1:** IRAM initialization — create PEConfig with IRAM containing ALUInst entries at specific offsets (e.g., offset 0: ADD, offset 5: CONST). Build topology. Verify `sys.pes[pe_id].iram[0].op == ArithOp.ADD` and `sys.pes[pe_id].iram[5].op == RoutingOp.CONST`. Verify offsets NOT in config are NOT in IRAM (`3 not in sys.pes[pe_id].iram`). + +- **or1-emu.AC5.2:** SM cell initialization — create SMConfig with initial_cells mapping: `{0: (Presence.FULL, 42), 10: (Presence.RESERVED, None)}`. Build topology. Verify `sys.sms[sm_id].cells[0].pres == Presence.FULL` and `sys.sms[sm_id].cells[0].data_l == 42`. Verify `sys.sms[sm_id].cells[10].pres == Presence.RESERVED`. Verify uninitialized cells are EMPTY. + +- **or1-emu.AC5.3:** Token injection — build topology with PE0 and SM0. Inject a MonadToken targeting PE0 via `sys.inject(token)`. Verify `sys.pes[0].input_store.items` contains the token. Inject an SMToken to SM0 via `sys.inject_sm(0, sm_token)`. Verify `sys.sms[0].input_store.items` contains the token. + +**Verification:** + +Run: `python -m pytest tests/test_integration.py -v -k "init or inject"` +Expected: All initialization tests pass + +**Commit:** `test: add initialization API smoke tests` + + + +### Task 2: Verify gen_counter initialization + +**Verifies:** or1-emu.AC5.1 (extended — gen_counters are part of PE configuration) + +**Files:** +- Modify: `tests/test_integration.py` (append test) + +**Testing:** + +- PEConfig with `gen_counters=[1, 0, 2, 3]` → verify `sys.pes[pe_id].gen_counters == [1, 0, 2, 3]` +- PEConfig with `gen_counters=None` (default) → verify all gen_counters are 0 + +**Verification:** + +Run: `python -m pytest tests/test_integration.py -v -k "gen_counter"` +Expected: Tests pass + +**Commit:** `test: add gen_counter initialization verification` + + diff --git a/docs/implementation-plans/2026-02-22-or1-emu/phase_06.md b/docs/implementation-plans/2026-02-22-or1-emu/phase_06.md new file mode 100644 --- /dev/null +++ b/docs/implementation-plans/2026-02-22-or1-emu/phase_06.md @@ -0,0 +1,160 @@ +# OR1 Dataflow CPU Behavioural Emulator — Phase 6: End-to-End Integration + +**Goal:** Run small hand-constructed programs across 1-2 PEs and an SM, verifying correct token flow, matching, execution, and output routing. + +**Architecture:** Tests use the direct initialization API from Phase 5 to set up multi-PE/SM topologies with pre-loaded IRAM and SM state. IRAM entries use `ALUInst` for PE→PE computation and `SMInst` for PE→SM operations. Seed tokens are injected via `System.inject()` (CM tokens) and `System.inject_sm()` (SM tokens for direct SM setup). Simulation runs until quiescence or timeout. Collector stores capture final output for assertion. + +**Tech Stack:** Python 3.12, SimPy 4.1, pytest + +**Scope:** 6 phases from original design (phase 6 of 6) + +**Codebase verified:** 2026-02-22 + +--- + +## Acceptance Criteria Coverage + +This phase implements and tests: + +### or1-emu.AC6: End-to-End Execution +- **or1-emu.AC6.1 Success:** CONST on PE0 emits token that arrives at PE1, triggers ADD, produces correct result +- **or1-emu.AC6.2 Success:** PE writes to SM, different PE reads from SM, receives correct data +- **or1-emu.AC6.3 Success:** DUAL mode fan-out delivers same result to two different consumers +- **or1-emu.AC6.4 Success:** SWITCH mode routes data and trigger to correct destinations based on comparison result + +--- + + +### Task 1: E2E test — CONST on PE0 feeds ADD on PE1 + +**Verifies:** or1-emu.AC6.1 + +**Files:** +- Modify: `tests/test_integration.py` (append test) + +**Testing:** + +Program: +- PE0 offset 0: `CONST` with `const=7`, dest_l → PE1 offset 0 port L (SINGLE mode) +- PE0 offset 1: `CONST` with `const=3`, dest_l → PE1 offset 0 port R (SINGLE mode) +- PE1 offset 0: `ADD`, dest_l → collector PE (or a sink PE2 with no IRAM) + +Seed tokens: +- Inject two MonadTokens to PE0: one targeting offset 0, one targeting offset 1 + +Expected: +- PE0 processes both CONSTs, emitting two tokens to PE1 offset 0 (one L, one R) +- PE1 matches both tokens at offset 0, fires ADD(7, 3) = 10 +- Result token (data=10) arrives at destination + +Verify: collector store receives exactly one token with data=10. + +**Verification:** + +Run: `python -m pytest tests/test_integration.py -v -k "const_feeds_add"` +Expected: Test passes + +**Commit:** `test: add e2e test for CONST→ADD across two PEs` + + + +### Task 2: E2E test — SM round-trip (PE writes, PE reads) + +**Verifies:** or1-emu.AC6.2 + +**Files:** +- Modify: `tests/test_integration.py` (append test) + +**Testing:** + +Program design: PE0 writes to SM0 via SMInst, then PE0 triggers a read from SM0 that returns to PE1. + +Setup: +- PE0 offset 0: `SMInst(op=MemOp.WRITE, sm_id=0, const=0)` — monadic SM write. Token data becomes write value, const=0 is cell address. +- PE0 offset 1: `SMInst(op=MemOp.READ, sm_id=0, const=0, ret=Addr(a=0, port=Port.L, pe=1))` — monadic SM read. const=0 is cell address. Result routes to PE1 offset 0. +- SM0: cell 0 starts EMPTY +- PE1: no IRAM (collects incoming tokens) + +Seed tokens (injected in order — PE0's input Store is FIFO, so WRITE processes before READ): +1. Inject MonadToken to PE0 offset 0 with data=42 (triggers SM WRITE to cell 0 with data=42) +2. Inject MonadToken to PE0 offset 1 with data=0 (triggers SM READ from cell 0, result → PE1) + +Note: This test relies on SimPy Store FIFO ordering — token 1 (WRITE) is consumed before token 2 (READ). This is guaranteed by `simpy.Store.get()` returning items in insertion order. + +Expected: +- PE0 processes SMInst(WRITE) → emits SMToken(target=0, op=WRITE, data=42) to SM0 +- SM0 receives WRITE → cell 0 becomes FULL with data 42 +- PE0 processes SMInst(READ) → emits SMToken(target=0, op=READ, ret=CMToken(target=1, offset=0, ctx=0, data=0)) to SM0 +- SM0 receives READ on FULL cell → emits result token (data=42) to PE1 +- PE1 receives MonadToken with data=42 + +Verify: PE1 input_store receives a token with data=42. + +**Verification:** + +Run: `python -m pytest tests/test_integration.py -v -k "sm_round_trip"` +Expected: Test passes + +**Commit:** `test: add e2e test for SM write/read round-trip` + + + +### Task 3: E2E test — DUAL mode fan-out + +**Verifies:** or1-emu.AC6.3 + +**Files:** +- Modify: `tests/test_integration.py` (append test) + +**Testing:** + +Program: +- PE0 offset 0: `PASS` with dest_l → PE1 offset 0 port L, dest_r → PE2 offset 0 port L (DUAL mode — both dests set, non-SWITCH op) + +Seed token: +- Inject MonadToken to PE0 offset 0 with data=99 + +Expected: +- PE0 processes PASS, emits two tokens (DUAL mode): same data to PE1 and PE2 +- Both PE1 and PE2 receive tokens with data=99 + +Verify: PE1 and PE2 input_stores each contain exactly one token with data=99. + +**Verification:** + +Run: `python -m pytest tests/test_integration.py -v -k "dual_fanout"` +Expected: Test passes + +**Commit:** `test: add e2e test for DUAL mode fan-out to two consumers` + + + +### Task 4: E2E test — SWITCH mode conditional routing + +**Verifies:** or1-emu.AC6.4 + +**Files:** +- Modify: `tests/test_integration.py` (append test) + +**Testing:** + +Program: +- PE0 offset 0: `SWEQ` (switch-on-equal) with dest_l → PE1 offset 0, dest_r → PE2 offset 0 (SWITCH mode) + +Test case A — condition TRUE (equal operands): +- Inject two DyadTokens to PE0 offset 0 with same data (e.g., both data=5) +- SWEQ(5, 5) → bool_out=True → data token to dest_l (PE1), inline trigger to dest_r (PE2) +- Verify: PE1 receives data token (data=5), PE2 receives inline MonadToken trigger (data=0, inline=True) + +Test case B — condition FALSE (unequal operands): +- Inject two DyadTokens to PE0 offset 0 with different data (e.g., 5 and 10) +- SWEQ(5, 10) → bool_out=False → data token to dest_r (PE2), inline trigger to dest_l (PE1) +- Verify: PE2 receives data token, PE1 receives inline trigger + +**Verification:** + +Run: `python -m pytest tests/test_integration.py -v -k "switch_routing"` +Expected: Both test cases pass + +**Commit:** `test: add e2e test for SWITCH mode conditional routing` + -- tangled.sh