From 6a796454b038fef56a7c712ffc3f0d9153ef659b Mon Sep 17 00:00:00 2001 From: Orual Date: Mon, 23 Feb 2026 14:43:37 -0500 Subject: [PATCH] bunch of design docs fixes --- design-notes/alu-and-output-design.md | 11 + design-notes/architecture-overview.md | 45 +- design-notes/assembler-architecture.md | 409 ++++++++++++++ .../bus-architecture-and-width-decoupling.md | 64 +-- design-notes/design-alternatives.md | 58 ++ design-notes/dfasm-primer.md | 500 ++++++++++++++++++ design-notes/io-and-bootstrap.md | 6 +- design-notes/network-and-communication.md | 9 +- design-notes/pe-design.md | 79 +-- design-notes/sm-design.md | 28 +- design-notes/syntax-sketch.md | 8 +- tokens.py | 1 - 12 files changed, 1072 insertions(+), 146 deletions(-) create mode 100644 design-notes/assembler-architecture.md create mode 100644 design-notes/dfasm-primer.md diff --git a/design-notes/alu-and-output-design.md b/design-notes/alu-and-output-design.md index 44b2ae6..f8d02c9 100644 --- a/design-notes/alu-and-output-design.md +++ b/design-notes/alu-and-output-design.md @@ -161,6 +161,11 @@ FREE_CTX Deallocate context slot (monadic). Clears the slot's occupied ### Operation Set Summary +> **⚠ Preliminary:** The binary opcode encodings below are a draft layout, +> not a committed hardware encoding. The Python emulator and assembler use +> IntEnum ordinal values that do NOT correspond to these bit patterns. +> Final hardware encoding will be determined during physical build. + | Opcode | Mnemonic | Arity | Output Mode | Description | |--------|----------|-------|-------------|-------------| | 00000 | ADD | dyadic | DUAL or SINGLE | A + B | @@ -245,6 +250,12 @@ stage at 5-10 MHz). ## ALU Hardware +> **⚠ Tentative:** The 8-bit vs 16-bit ALU datapath decision is not fully +> resolved. The emulator operates at 16-bit. The 8-bit design below is the +> current hardware sketch for minimising chip count; the 16-bit variant +> (see below) roughly doubles the datapath. Final decision depends on +> transistor budget during physical build. + ### 8-bit Datapath (v0) ``` diff --git a/design-notes/architecture-overview.md b/design-notes/architecture-overview.md index 3c6e080..2142c0d 100644 --- a/design-notes/architecture-overview.md +++ b/design-notes/architecture-overview.md @@ -48,7 +48,7 @@ see `design-alternatives.md`. for bootstrap - **Yang et al. (DDR SDRAM IP lookup)**: hash + small CAM for collision overflow -### Data Width (Resolved) +### Data Width (Tentative) - **16-bit** data words within PEs and SM (see `bus-architecture-and-width-decoupling.md`) - **16-bit external bus**, with multi-flit token encoding (2 flits standard) - Three independent width domains: external bus (16-bit), IRAM (32-48 bits, @@ -56,6 +56,13 @@ see `design-alternatives.md`. and control paths) - Width conversion at FIFO boundaries via serialisers/deserialisers +> **⚠ Tentative:** 16-bit is the working assumption for the emulator and +> assembler, but the 8-bit vs 16-bit ALU datapath decision is not fully +> resolved. The hardware design notes (`alu-and-output-design.md`) detail +> an 8-bit v0 datapath with 16-bit as a future upgrade. The emulator +> operates at 16-bit. Final decision depends on transistor budget tradeoffs +> during physical build. + ## Token Format (type-tagged, flit-based) **Supersedes the 32-bit monolithic token format.** See @@ -232,10 +239,9 @@ See `network-and-communication.md` for routing, clocking, and scaling details. | **Total logic** | **~25-35K** | | SRAM chips (instruction mem, matching stores, token queues) | 8-16 chips | -Note: bootstrap microsequencer removed from budget — bootstrap is handled -by the I/O controller + type-11 config writes, or by an external -microcontroller during early prototyping. No dedicated bootstrap hardware -in the final architecture. +Bootstrap is handled by the I/O controller via type-11 config writes, +or by an external microcontroller during early prototyping. No dedicated +bootstrap hardware in the architecture. ## IPC / Performance Expectations @@ -299,26 +305,31 @@ in the final architecture. 1. **SM internal design** — banking scheme, operation set, interface protocol (partially specified, see `sm-design.md`) -2. **Matching store SRAM addressing** — detailed direct-index scheme with - possible hash fallback to external/SM RAM via small TLB/LUT -3. **Context slot count per CM** — 4 bits = 16 slots vs 5 bits = 32 slots; +2. **Context slot count per CM** — 4 bits = 16 slots vs 5 bits = 32 slots; drives flit-1 bit allocation tradeoffs -4. **Flit-1 bit allocation** — how to divide the 12 available bits (after +3. **Flit-1 bit allocation** — how to divide the 12 available bits (after type + PE_id) among instr_addr, ctx_slot, port, and gen. Smaller instr_addr is reasonable given small IRAM; gen bits only needed for dyadic. Multi-flit extended tokens provide escape hatch for wider addressing. See `bus-architecture-and-width-decoupling.md` open questions. -5. **Instruction encoding** — operation set, format, IRAM width (32 or 48 +4. **Instruction encoding** — operation set, format, IRAM width (32 or 48 bits). Decoupled from bus width, driven by opcode + destination fields. -6. **I/O controller internal design** — state machine, UART bridge, + > **Partially resolved:** the emulator and assembler use Python IntEnum + > values as opcode placeholders. These do NOT represent final hardware + > bit encodings — a hardware encoding pass is still needed. The 5-bit + > opcode table in `alu-and-output-design.md` is a preliminary draft. +5. **I/O controller internal design** — state machine, UART bridge, unsolicited token generation, flit-based I/O token format -7. **Compiler / assembler** — hand-written dataflow asm for v0, assembler - that packs token fields (flit-aware) -8. **Monadic/dyadic optimisation** — deferred, revisit after v0 matching - store works -9. **Mode B clock ratio** — exactly 2x, or design for arbitrary integer +6. ~~**Compiler / assembler**~~ — **Partially Resolved.** The `asm/` package + implements a 6-stage assembler pipeline (parse → lower → resolve → + place → allocate → codegen). Produces PEConfig/SMConfig + seed tokens + or a bootstrap token stream. See `assembler-architecture.md` for architecture. + Grammar is `dfasm.lark` (Lark/Earley parser). Auto-placement via + greedy bin-packing with locality heuristic. Remaining work: further + optimisation passes, macro expansion, binary output. +7. **Mode B clock ratio** — exactly 2x, or design for arbitrary integer ratios? See `bus-architecture-and-width-decoupling.md`. -10. **Instruction residency** — small IRAM per PE means programs larger +8. **Instruction residency** — small IRAM per PE means programs larger than IRAM need runtime code loading. Code storage hierarchy: external storage → SM → IRAM. Identity detection (is the *right* code loaded?) is harder than presence detection. See `pe-design.md` Instruction diff --git a/design-notes/assembler-architecture.md b/design-notes/assembler-architecture.md new file mode 100644 index 0000000..8fdf65e --- /dev/null +++ b/design-notes/assembler-architecture.md @@ -0,0 +1,409 @@ +# Dynamic Dataflow CPU — Assembler Architecture + +Covers the `asm/` package: pipeline structure, IR design, pass +architecture, code generation modes, and key implementation decisions. + +See `architecture-overview.md` for the target hardware model. +See `dfasm-primer.md` for the language itself. +See `asm/CLAUDE.md` for the contract-level summary. + +## Role + +The assembler translates dfasm source into emulator-ready configuration +objects (`PEConfig`, `SMConfig`, seed tokens) or a hardware-faithful +bootstrap token sequence. It bridges the gap between human-authored +dataflow graph programs and the structures the emulator (and eventually +hardware) consumes. + +The assembler does NOT optimise. It does not reorder instructions for +performance, fuse operations, or eliminate redundant subgraphs. It is a +faithful translator: the graph you write is the graph you get. Future +optimisation passes may be inserted between resolve and place, but the +current pipeline is intentionally thin — correctness first, cleverness +later. + +## Pipeline Overview + +Six stages, each a pure function from `IRGraph → IRGraph` (or +`IRGraph → output`). The pipeline is: + +``` +dfasm source + │ + ▼ + Parse Lark/Earley parser, dfasm.lark grammar + │ → concrete syntax tree (CST) + ▼ + Lower CST → IRGraph (nodes, edges, regions, data defs) + │ → name qualification, scope creation + ▼ + Resolve validate edge endpoints, detect scope violations + │ → "did you mean" suggestions via Levenshtein distance + ▼ + Place assign PEs to unplaced nodes + │ → greedy bin-packing with locality heuristic + ▼ + Allocate assign IRAM offsets, context slots, resolve addresses + │ → dyadic-first layout, per-PE context scoping + ▼ + Codegen emit PEConfig/SMConfig + seeds (direct mode) + or SM init → ROUTE_SET → LOAD_INST → seeds (token mode) +``` + +Each pass returns a new `IRGraph`. Graphs are never mutated after +construction — each pass produces a fresh copy with the new information +filled in. Errors accumulate in `IRGraph.errors` rather than failing +fast, so the assembler reports all problems in a single pass rather +than forcing the programmer to fix them one at a time. + +The public API orchestrates the pipeline and raises `ValueError` if any +stage produces errors: + +```python +assemble(source: str) -> AssemblyResult # direct mode +assemble_to_tokens(source: str) -> list # token stream mode +round_trip(source: str) -> str # parse → lower → serialize +serialize_graph(graph: IRGraph) -> str # IRGraph → dfasm at any stage +``` + +## IR Types (`ir.py`) + +All IR types are frozen dataclasses, following the conventions of +`tokens.py` and `cm_inst.py`. + +### Core Types + +| Type | Fields | Purpose | +|------|--------|---------| +| `IRNode` | name, opcode, dest_l, dest_r, const, pe, iram_offset, ctx, loc, args, sm_id | Single instruction in the dataflow graph | +| `IREdge` | source, dest, port, source_port, loc | Connection between two nodes | +| `IRGraph` | nodes, edges, regions, data_defs, system, errors | Complete program representation | +| `IRRegion` | tag, kind, body (IRGraph), loc | Nested scope (FUNCTION or LOCATION) | +| `IRDataDef` | name, sm_id, cell_addr, value, loc | SM cell initialisation | +| `SystemConfig` | pe_count, sm_count, iram_capacity, ctx_slots, loc | Hardware configuration from `@system` pragma | + +### Destination Representation + +Destinations evolve through the pipeline: + +1. **After lower**: `NameRef(name, port)` — symbolic, unresolved +2. **After allocate**: `ResolvedDest(name, addr)` — concrete + `Addr(a, port, pe)` with IRAM offset, port, and target PE + +This two-stage resolution means early passes can work with symbolic +names while later passes have concrete hardware addresses. + +### Graph Traversal Utilities + +`IRGraph` provides recursive traversal functions for working with nested +regions: + +- `iter_all_subgraphs()` — depth-first traversal of graph + all region + bodies +- `collect_all_nodes()` — flatten nodes from graph and all nested regions +- `collect_all_nodes_and_edges()` — flatten both +- `collect_all_data_defs()` — flatten data definitions +- `update_graph_nodes()` — recursively update nodes while preserving + region structure + +These are necessary because function definitions create nested +`IRRegion` objects with their own `IRGraph` bodies. + +## Pass Details + +### Parse + +Uses the Lark library with the Earley parser algorithm. The grammar is +in `dfasm.lark`. Earley is required (not LALR) because the grammar has +ambiguities between `location_dir` (a bare qualified reference) and +`weak_edge` (outputs before opcode) that require context-sensitive +resolution. + +The parser produces a concrete syntax tree (CST) — Lark `Tree` objects +with `Token` terminals. No semantic processing happens here. + +### Lower (`lower.py`) + +A Lark `Transformer` that walks the CST bottom-up, converting each +grammar rule into IR types. + +**Key transformations:** + +- `inst_def` → `IRNode` with opcode, const, PE placement, named args +- `plain_edge` → `IREdge` with source, dest, port qualifiers +- `strong_edge` / `weak_edge` → anonymous `IRNode` + input/output + `IREdge` set (creates a `CompositeResult`) +- `func_def` → `IRRegion(kind=FUNCTION)` with a nested `IRGraph` body +- `location_dir` → `IRRegion(kind=LOCATION)` — subsequent statements + are collected into its body during post-processing +- `data_def` → `IRDataDef` with SM placement and cell address +- `system_pragma` → `SystemConfig` (stored on the transformer, attached + to the final `IRGraph`) + +**Name qualification:** + +Labels (`&name`) inside function regions are qualified with the function +scope: `&add` inside `$main` becomes `$main.&add`. This scoping is +transparent to the programmer — dfasm source uses bare `&label` names +within functions, and the assembler qualifies them internally. + +Node references (`@name`) and function references (`$name`) are +top-level and are not qualified. + +**Opcode mapping (`opcodes.py`):** + +Mnemonic strings from the grammar are mapped to `ALUOp`, `MemOp`, or +`CfgOp` enum values via `MNEMONIC_TO_OP`. A complication: Python +`IntEnum` subclasses can share numeric values across types +(`ArithOp.ADD == 0 == MemOp.READ`), so the reverse mapping and set +membership tests use type-aware collections +(`TypeAwareOpToMnemonicDict`, `TypeAwareMonadicOpsSet`) that key on +`(type, value)` tuples internally. + +### Resolve (`resolve.py`) + +Validates that all edge endpoints exist in the flattened namespace. + +**Process:** + +1. Flatten all nodes from the graph and all nested regions into a single + namespace +2. Build a scope map: qualified name → defining scope +3. For each edge, check that both source and dest exist +4. Detect cross-function label references (a `&label` in `$main` cannot + reference a `&label` in `$other`) and flag as SCOPE errors +5. For undefined references, compute Levenshtein distance against all + known names and suggest the closest match ("did you mean `&addr`?") + +Resolve does not modify nodes — it only appends errors. + +### Place (`place.py`) + +Assigns PE IDs to nodes that don't have explicit placement. + +**Explicit placements** (from `|pe0` qualifiers in source) are validated +first: reject any `pe >= pe_count`. + +**Auto-placement algorithm** for unplaced nodes (greedy, insertion +order): + +1. For each unplaced node, find its connected neighbours via edges +2. Count PE occurrences among placed neighbours (locality heuristic) +3. Sort candidate PEs by: most neighbours (descending), then most + remaining IRAM capacity (tie-break) +4. Place on the first PE with room for both IRAM and context slots +5. If no PE fits, record a placement error with per-PE utilisation + breakdown + +**IRAM cost model:** + +- Dyadic instruction: 2 slots (instruction + matching store entry) +- Monadic instruction: 1 slot + +**System config inference:** + +If no `@system` pragma is provided, the placer infers `pe_count` from +the highest explicit PE ID and uses defaults for IRAM capacity (64) and +context slots (4). + +### Allocate (`allocate.py`) + +Assigns three things per node: IRAM offset, context slot, and resolved +destinations. + +**IRAM layout (per PE):** + +Dyadic instructions are packed at low offsets (0..D-1), monadic at +higher offsets (D..D+M-1). This matches the hardware contract in +`pe-design.md`: the token's offset field doubles as the matching store +entry for dyadic instructions, so they must occupy the dense low range. + +**Context slot assignment (per PE):** + +Each function scope gets a distinct context slot. The root scope +(top-level) always gets slot 0. Additional scopes get slots 1, 2, ... +in order of first appearance. Overflow beyond `ctx_slots` is a RESOURCE +error. + +**Destination resolution:** + +For each node, outgoing edges are resolved to `ResolvedDest` objects +containing concrete `Addr(a=iram_offset, port=Port.L|R, pe=target_pe)`. +The allocator handles: + +- Single outgoing edge → `dest_l` +- Two outgoing edges → `dest_l` + `dest_r` (distinguished by + `source_port` qualifier or positional order) +- Port conflicts (duplicate ports, mixed explicit/implicit) → PORT error + +**SM ID assignment:** + +For `MemOp` nodes, the allocator assigns the target SM ID. Single-SM +systems default to `sm_id=0`. Multi-SM systems with ambiguous targets +produce a RESOURCE error. + +### Codegen (`codegen.py`) + +Two output modes from the same allocated `IRGraph`: + +**Direct mode** (`generate_direct() → AssemblyResult`): + +Produces immediately usable emulator configuration: + +- `pe_configs`: list of `PEConfig` with populated IRAM (ALUInst/SMInst + by offset), context slot count, and route restrictions +- `sm_configs`: list of `SMConfig` with initial cell values from data + definitions +- `seed_tokens`: `MonadToken` for each `CONST` node with no incoming + edges (these kick off execution) + +Route restrictions are computed by scanning all edges from each PE to +determine which other PEs and SMs it can reach. Self-routes are always +included. + +**Token stream mode** (`generate_tokens() → list`): + +Produces a hardware-faithful bootstrap sequence: + +1. **SM init tokens** — `SMToken(op=WRITE)` for each data definition +2. **ROUTE_SET tokens** — `RouteSetToken` per PE with route restrictions +3. **LOAD_INST tokens** — `LoadInstToken` per PE with full IRAM contents +4. **Seed tokens** — same `MonadToken` list as direct mode + +This ordering mirrors what the hardware bootstrap would do: initialise +structure memory, configure routing, load instruction memory, then +inject the initial tokens that start execution. + +Both modes reuse the same internal logic — token stream mode calls +`generate_direct()` internally and then reformats the result. + +## Error Handling (`errors.py`) + +Errors are structured with category, source location, message, and +optional suggestions. Categories: + +| Category | Stage | Examples | +|----------|-------|---------| +| PARSE | lower | unknown opcode, malformed grammar | +| NAME | resolve | undefined node reference | +| SCOPE | resolve, lower | cross-function label reference, duplicate definition | +| PLACEMENT | place | PE ID out of range, capacity exceeded | +| RESOURCE | allocate | IRAM overflow, context slot overflow, missing SM target | +| ARITY | lower | wrong operand count | +| PORT | allocate | port conflicts, missing destinations | +| UNREACHABLE | (future) | unused nodes | +| VALUE | lower | out-of-range literals | + +Error formatting follows a Rust-style pattern: + +``` +error[SCOPE]: Duplicate label '&add' in function '$main' + --> line 5, column 3 + | +5 | &add <| sub + | ^^^ + = help: First defined at line 2 +``` + +## Key Design Decisions + +### Immutable Pass Pattern + +Each pass returns a new `IRGraph`. This simplifies debugging (you can +inspect intermediate representations), enables future caching, and +follows functional programming discipline. The tradeoff is allocation +overhead from copying, but for assembler-scale programs this is +negligible. + +### Dyadic-First IRAM Layout + +Dyadic instructions are packed at low IRAM offsets so the token's offset +field doubles as the matching store entry index. This is a hardware +constraint from `pe-design.md` (option b) — no extra bits or lookup +tables needed. The compiler must cooperate, and it does. + +### Greedy Placement + +The placer is intentionally simple: greedy bin-packing with a locality +heuristic. No simulated annealing, no ILP solver, no graph partitioning +library. For the target scale (4 PEs, tens of instructions), greedy is +adequate. The locality heuristic (prefer the PE where connected +neighbours already live) naturally minimises cross-PE token traffic. + +More sophisticated placement is a future concern — the pass interface +(`IRGraph → IRGraph`) means the placer is swappable without touching +anything else. + +### Error Accumulation + +All phases append errors to `IRGraph.errors` rather than raising +immediately. This means the programmer sees all problems at once, not +a frustrating one-at-a-time reveal. The pipeline orchestrator +(`__init__.py`) raises `ValueError` after each stage if errors are +present. + +### Type-Aware Opcode Collections + +Python `IntEnum` values collide across subclasses (`ArithOp.ADD = 0 = +MemOp.READ`). Plain dicts and sets lose type information. The +`TypeAwareOpToMnemonicDict` and `TypeAwareMonadicOpsSet` in +`opcodes.py` key on `(type(op), op.value)` tuples to avoid this. + +## Module Dependency Graph + +``` +dfasm.lark (grammar) + │ + ▼ +lower.py ──→ ir.py (types) ──→ opcodes.py (mnemonic mapping) + │ │ + ▼ ▼ +resolve.py errors.py (error types) + │ + ▼ +place.py + │ + ▼ +allocate.py + │ + ▼ +codegen.py ──→ cm_inst (ALUInst, SMInst, Addr) + │ ──→ tokens (MonadToken, SMToken, CfgToken variants) + │ ──→ emu/types (PEConfig, SMConfig) + │ ──→ sm_mod (Presence) + ▼ +__init__.py (pipeline orchestration, public API) +``` + +**Boundary rule**: `emu/` and root-level modules never import from +`asm/`. The assembler depends on the emulator's types, not the other +way around. + +## Serialization and Round-Tripping (`serialize.py`) + +The serializer emits valid dfasm source from an `IRGraph` at any +pipeline stage. This enables: + +- **Round-trip testing**: `source → parse → lower → serialize → source'` + verifies that the parser and lowering pass preserve the program +- **IR inspection**: dump the graph after any pass to see what the + assembler is doing +- **Code generation from IR**: future tools could construct `IRGraph` + objects programmatically and serialize them to dfasm + +The serializer unqualifies names inside function regions (strips the +`$func.` prefix), preserves port and placement qualifiers, and formats +values as hex (if > 255) or decimal. + +## Future Work + +- **Optimisation passes** between resolve and place: dead node + elimination, constant folding, subgraph deduplication +- **Macro expansion**: the grammar already supports `#macro` syntax in + data definitions; the expansion pass is not yet implemented +- **Wider placement heuristics**: graph partitioning, min-cut + algorithms, or profile-guided placement for larger programs +- **Incremental reassembly**: modify part of the graph and re-run only + affected passes +- **Hardware encoding pass**: translate ALUInst/SMInst to bit-level + instruction words for actual IRAM loading diff --git a/design-notes/bus-architecture-and-width-decoupling.md b/design-notes/bus-architecture-and-width-decoupling.md index fddf348..131cd93 100644 --- a/design-notes/bus-architecture-and-width-decoupling.md +++ b/design-notes/bus-architecture-and-width-decoupling.md @@ -475,9 +475,8 @@ encoded per instruction, with no pressure from bus width. | Immediate | small constant for immediate-mode ops | 0-8 | This sums to roughly **32-48 bits** depending on address space size and -how aggressively fields are packed. The destination address fields are -narrower than the previous estimate because IRAM per PE is now 128 entries -(7-bit address), not 1024. +how aggressively fields are packed. IRAM per PE is 128 entries (7-bit +address), keeping destination address fields compact. Note: the IRAM instruction word includes a **width flag** for each output destination, determining whether the output token is formed as narrow @@ -679,53 +678,7 @@ clock with handshake signals. Neither transition changes any module interface. This is the payoff of the FIFO-based decoupling discipline. ---- - -## Impact on Existing Design Documents - -### `architecture-overview.md` -- Token format section needs rewrite for the four compute token formats -- Flit-1 bit allocation now concrete (type:2, PE:2, mode:1, then - type-dependent fields) -- Variable-length token table updated (1-4 flits) -- "14-bit dyadic data" problem eliminated -- "bump to 36-bit bus?" question retired - -### `network-and-communication.md` -- Physical bus is 16-bit, not 32-bit -- Routing nodes inspect flit 1 bits [15:12] only (type + dest ID) -- 1-flit inline monadic tokens are a new packet length the network must - handle (determined by type=01, bit[11]=1) -- Backpressure unchanged — still FIFO-based -- Handshake/ready signal reusable for IRAM swap quiesce - -### `pe-design.md` -- Pipeline stage 2 now includes IRAM valid-bit check (parallel with - matching store read) -- Matching store entry format: 8-bit or 16-bit data depending on PE - width mode configuration -- IRAM: 128 entries, 7-bit address space for monadic, 4-5 bit for dyadic -- Diagnostic LED on valid-bit discard flag -- Instruction encoding: width flag per output destination controls narrow - vs wide output token formation - -### `design-alternatives.md` -- 36-bit bus alternative retired -- Variable-width token alternative adopted (flit-based, type-dependent - field layouts) -- 8-bit bus remains rejected (even with inline monadic, minimum is 1 flit - = 16 bits on the wire) -- 8-bit data mode documented as first-class option alongside 16-bit - -### `sm-design.md` -- SM internal datapath confirmed at 16-bit -- SM result tokens: formatter must know whether to produce narrow or wide - compute tokens (return routing in request specifies width, or SM always - returns wide and the receiving CM's instruction handles width conversion) - ---- - -## Open Questions (Updated) +## Open Questions 1. **IRAM width** — 32 or 48 bits? Depends on destination address field sizes (now 7-10 bits given smaller IRAM), opcode count, immediate @@ -750,14 +703,3 @@ the FIFO-based decoupling discipline. 8. **Drain protocol specifics** — quiesce via type-11 subop, or via handshake backpressure, or both? Needs to be defined before IRAM swap can be implemented. - -### Resolved Questions (from previous version) - -- **Flit 1 bit allocation**: now concrete across four compute token formats. - Type-dependent field layouts maximise use of available bits. -- **Extended data tokens**: deferred. 8-bit narrow mode addresses the - "data is often small" case. True 32-bit values can use two tokens with - software multi-word arithmetic. -- **Instruction encoding interaction with bus**: fully decoupled. IRAM - width is independent. Instruction words control output token width via - a per-destination width flag. diff --git a/design-notes/design-alternatives.md b/design-notes/design-alternatives.md index 3f8db1c..629c175 100644 --- a/design-notes/design-alternatives.md +++ b/design-notes/design-alternatives.md @@ -449,6 +449,64 @@ Explored in the context of the EM-4 analysis. How to engage multiple PEs on a da **Decision**: Option A (SM scatter/gather) is the primary approach. Option B (broadcast) is a natural extension when prefix routing is implemented. Options C and D are rejected or subsumed. **Key implication**: dedicated SM-CM paths become more valuable when vector operations are common, because the per-element SM traffic is the bottleneck. This is a strong argument for eventually separating AN/DN from the main CN, even at small scale. +--- + +## 11. Matching Store Entry Addressing + +### Chosen: Unified Offset (dyadic-first IRAM layout) + +The token's offset field serves as both the IRAM instruction address +and (for dyadic instructions at offsets < M) the matching store entry +within the context slot. The compiler packs dyadic instructions at low +IRAM offsets and monadic above. Single cycle, no extra token bits, no +lookup table. + +See `pe-design.md` §Instruction Address vs Matching Store Address for +the chosen design. + +### Alternative A: Separate Token Fields + +The token carries both an instruction offset AND a separate match entry +index. Costs token bits — the offset field would need to be split or +the match_entry packed into spare bits. Rejected because the unified +offset approach achieves the same result with zero bit cost. + +### Alternative B: Instruction Word Contains Match Entry + +The instruction word fetched from IRAM in Stage 3 would contain the +match_entry field. Initially considered as the "simplest v0 approach." +**Rejected** because Stage 2 (matching) happens BEFORE Stage 3 +(instruction fetch) — the match_entry must be known before the +instruction word is available. This ordering constraint makes the +approach impossible without adding a pipeline stall or lookup table. + +### Alternative C: Lookup ROM/SRAM + +A small lookup ROM/SRAM alongside the matching store maps instruction +offset → match_entry. Adds either a serial read before the match SRAM +access (extra latency) or requires a second SRAM port (extra hardware). +Rejected as unnecessary given that the compiler can enforce the +dyadic-first layout constraint. + +--- + +## 12. Matching Store Overflow: CAM-Based Buffer + +An early design considered using National Semiconductor 100142 CAM chips +(4×4-bit, content-addressable memory) for a small overflow buffer +alongside the matching store. Tokens that couldn't fit in the direct- +indexed store would spill to the CAM for associative lookup. + +**Why abandoned:** the 100142 is tiny (4 words × 4 bits per chip) and +expensive. Building a useful overflow buffer requires many chips for +minimal capacity. The TLB/LUT fallback to external RAM or SM RAM +(described in `pe-design.md` §Overflow) is more scalable and uses +commodity SRAM. The 100142 chips remain potentially useful for other +small associative lookups (routing tables, free-list management) but +are not a good fit for matching store overflow. + +--- + ### Instruction Memory as Write-Back Cache - Future idea: if instruction memory is writable at runtime, could it function as a write-back cache for a larger backing store? PE fetches diff --git a/design-notes/dfasm-primer.md b/design-notes/dfasm-primer.md new file mode 100644 index 0000000..1e56c48 --- /dev/null +++ b/design-notes/dfasm-primer.md @@ -0,0 +1,500 @@ +# dfasm — Dataflow Graph Assembly Language + +A primer on the dfasm dialect: syntax, semantics, naming conventions, +and the mapping from source to executable configuration. + +See `assembler-architecture.md` for the assembler's internal pipeline. +See `architecture-overview.md` for the hardware model dfasm targets. + +## What dfasm Is + +dfasm is a textual representation of dataflow graphs. Each instruction +is a **node** with zero, one, or two inputs and up to two outputs. +Connections between nodes are **edges**. Execution is data-driven: +a node fires when all its required operands have arrived as tokens on +the network. + +dfasm is not sequential assembly. There is no program counter, no +implicit instruction ordering. The order of statements in the source +file has no effect on execution — only the graph topology matters. + +## Syntax Overview + +### Comments + +Semicolons start line comments (traditional assembler convention): + +```vhdl +; This is a comment +&add <| add ; inline comment +``` + +### Names and Sigils + +dfasm uses three sigil-prefixed naming conventions: + +| Sigil | Scope | Use | +|-------|-------|-----| +| `@name` | Global (top-level) | Node references, data definitions | +| `&name` | Local (within enclosing function) | Labels for instructions | +| `$name` | Global | Function / subgraph definitions | + +Names are composed of `[a-zA-Z_][a-zA-Z0-9_]*`. Sigils are part of the +reference syntax, not the name itself. + +### Qualifier Chains + +Names can be chained with placement and port qualifiers. No spaces +are allowed within a chain: + +```vhdl +&sum|pe0:L ; label "sum", placed on PE 0, left port +@data|sm0:5 ; node "data", placed on SM 0, cell address 5 +&branch|pe1:R ; label "branch", PE 1, right port +``` + +| Qualifier | Syntax | Meaning | +|-----------|--------|---------| +| Placement | `\|peN` or `\|smN` | Assign to a specific PE or SM | +| Port | `:L` or `:R` | Left or right input port (for edges) | +| Cell address | `:N` | SM cell address (for data definitions) | + +Placement is optional — the assembler auto-places unplaced nodes using +a greedy locality heuristic. + +## Statement Types + +### System Pragma + +Declares hardware configuration. Required for programs that need +specific PE/SM counts: + +```vhdl +@system pe=4, sm=1, iram=128, ctx=4 +``` + +| Parameter | Required | Default | Meaning | +|-----------|----------|---------|---------| +| `pe` | yes | — | Number of processing elements | +| `sm` | yes | — | Number of structure memory modules | +| `iram` | no | 64 | IRAM capacity per PE (instruction slots) | +| `ctx` | no | 4 | Context slots per PE | + +At most one `@system` pragma per program. + +### Instruction Definition + +Defines a named node with an opcode and optional arguments: + +```vhdl +&label <| opcode [, arg ...] +``` + +The `<|` operator reads as "receives from" — the node receives data +from whatever edges point to it. + +**Examples:** + +```vhdl +&c1|pe0 <| const, 42 ; constant node, value 42, placed on PE 0 +&adder <| add ; dyadic add, auto-placed +&reader|pe1 <| read, 5 ; SM read at cell 5, placed on PE 1 +&branch <| sweq ; switch-on-equal (routing op) +``` + +Named arguments are supported for clarity: + +```asm +@serial <| ior dest=0x45, addr=0x91, data=0x43 +``` + +### Plain Edge + +Wires a named source to one or more named destinations: + +```vhdl +&source |> &dest:L ; single edge, left port +&source |> &dest1:L, &dest2:R ; fan-out to two destinations +``` + +The `|>` operator reads as "flows to" — data flows from source to +destination. Port qualifiers on the destination specify which input +the data arrives on. Port qualifiers on the source specify which +output slot it leaves from (relevant for dual-output nodes like +switch operations). + +**Default port is L (left)** when no port is specified. + +### Strong Edge (Inline Anonymous Node) + +Creates an anonymous node with explicit input wiring: + +```vhdl +opcode inputs... |> outputs... +``` + +```vhdl +add &a, &b |> &result:L ; anonymous add of &a and &b → &result +``` + +This is shorthand. The assembler creates a hidden node (named +`&__anon_N`) and wires the inputs and outputs. Useful for small, +one-off operations that don't need a label. + +### Weak Edge (Reverse Inline) + +Same as strong edge but with reversed syntax: + +```asm +outputs... opcode <| inputs... +``` + +```asm +&result:L add <| &a, &b ; same as: add &a, &b |> &result:L +``` + +The distinction between strong and weak edges is purely syntactic — +they produce identical IR. + +### Function Definition + +Groups instructions into a named scope: + +```vhdl +$fib |> { + &c_n <| const, 10 + &sub1 <| sub + &branch <| sweq + + &c_n |> &branch:L + &c_n |> &sub1:L + ; ... +} +``` + +Labels (`&name`) inside a function are scoped to that function. You +cannot reference `&sub1` from outside `$fib`. Internally, the assembler +qualifies the name as `$fib.&sub1`. + +Node references (`@name`) are always global — they can be referenced +from anywhere. + +### Data Definition + +Initialises a structure memory cell before execution begins: + +```vhdl +@data|sm0:5 = 0x42 ; SM 0, cell 5, value 0x42 +@pair|sm0:0 = 'h', 'i' ; two chars packed big-endian → 0x6869 +@msg|sm1:10 = "hello" ; string chars as packed 16-bit words +``` + +Data definitions require SM placement (`|smN`) and a cell address +(`:N`). The assembler translates these into SM write tokens during +bootstrap. + +### Location Directive + +Sets a location context for subsequent definitions: + +```vhdl +@compute_region +&a <| const, 5 ; these nodes are inside @compute_region +&b <| add +``` + +Statements following a location directive are collected into that +location's scope until the next function or location directive. + +## Opcodes + +### Arithmetic (dyadic unless noted) + +| Mnemonic | Arity | Description | +|----------|-------|-------------| +| `add` | dyadic | L + R | +| `sub` | dyadic | L − R | +| `inc` | monadic | data + 1 | +| `dec` | monadic | data − 1 | +| `shiftl` | monadic | shift left by const+1 bits | +| `shiftr` | monadic | logical shift right by const+1 bits | +| `ashiftr` | monadic | arithmetic shift right by const bits | + +### Logical + +| Mnemonic | Arity | Description | +|----------|-------|-------------| +| `and` | dyadic | bitwise AND | +| `or` | dyadic | bitwise OR | +| `xor` | dyadic | bitwise XOR | +| `not` | monadic | bitwise NOT | + +### Comparison (dyadic, produce bool_out) + +| Mnemonic | Description | +|----------|-------------| +| `eq` | L == R | +| `lt` | L < R (signed) | +| `lte` | L ≤ R (signed) | +| `gt` | L > R (signed) | +| `gte` | L ≥ R (signed) | + +Comparison results are signed 2's complement interpretation of 16-bit +values. + +### Routing / Switching / Branching (dyadic) + +These operations route tokens based on a comparison result. They are +all dyadic — they compare L and R, then route accordingly. + +**Branch operations** (`br*`): emit data to `dest_l` (taken) or +`dest_r` (not taken) based on comparison: + +| Mnemonic | Condition | +|----------|-----------| +| `breq` | L == R | +| `brgt` | L > R | +| `brge` | L ≥ R | +| `brof` | overflow | +| `brty` | type match | + +NOTE: `br*` ops use predicate register and internal-to-PE loopback route +if that hardware is implemented. + +**Switch operations** (`sw*`): like branch, but when the condition is +true, data goes to `dest_l` and a trigger token (value 0) goes to +`dest_r`. When false, trigger goes to `dest_l` and data goes to +`dest_r`: + +| Mnemonic | Condition | +|----------|-----------| +| `sweq` | L == R | +| `swgt` | L > R | +| `swge` | L ≥ R | +| `swof` | overflow | +| `swty` | type match | + +**Other routing:** + +| Mnemonic | Arity | Description | +|----------|-------|-------------| +| `gate` | dyadic | pass data through if bool_out is true, suppress if false | +| `sel` | dyadic | select between inputs | +| `merge` | dyadic | merge two inputs | + +### Data (monadic) + +| Mnemonic | Description | +|----------|-------------| +| `pass` | pass data through unchanged | +| `const` | emit constant value (from const field) | +| `free_ctx` | deallocate context slot, no data output | + +- `free_ctx` in particular is a special token used to handle function body and loop exits. + +### Structure Memory + +| Mnemonic | Arity | Description | +|----------|-------|-------------| +| `read` | monadic | read from SM cell (const = cell address) | +| `write` | context-dependent | write to SM cell — monadic if const is set (cell addr from const), dyadic if const is None (cell addr from L operand) | +| `clear` | monadic | clear SM cell | +| `alloc` | monadic | allocate SM cell | +| `free` | monadic | free SM cell | +| `rd_inc` | monadic | atomic read-and-increment | +| `rd_dec` | monadic | atomic read-and-decrement | +| `cmp_sw` | monadic | compare-and-swap | + +Note: `free_ctx` (ALU context deallocation) and `free` (SM cell free) +are disambiguated by mnemonic — `free_ctx` maps to `RoutingOp.FREE_CTX` +while `free` maps to `MemOp.FREE`. + +### Configuration / System + +| Mnemonic | Description | +|----------|-------------| +| `load_inst` | load instruction into PE IRAM | +| `route_set` | configure PE routing table | +| `ior` | I/O read | +| `iow` | I/O write | +| `iorw` | I/O read-write | + +These are rarely written by hand — `load_inst` and `route_set` are +generated by the assembler's token stream mode during bootstrap. + +## Literals + +| Syntax | Example | Description | +|--------|---------|-------------| +| Decimal | `42` | Decimal integer | +| Hex | `0xFF` | Hexadecimal integer | +| Char | `'A'` | Single character (ASCII value) | +| String | `"hello"` | String of char codes | +| Raw string | `r"no\escapes"` | No escape processing | +| Byte string | `b"\x00\xFF"` | Explicit byte values | + +**Escape sequences** (in regular strings and char literals): +`\n`, `\t`, `\r`, `\0`, `\\`, `\'`, `\"`, `\xHH` + +**Multi-char packing:** when multiple char values appear in a data +definition, they are packed big-endian into 16-bit words: + +```vhdl +@data|sm0:0 = 'h', 'i' ; → 0x6869 (h=0x68 in high byte, i=0x69 in low) +``` + +All data values are 16-bit unsigned. + +## Complete Example + +A simple program that adds two constants and routes the result +across PEs: + +```vhdl +; Hardware: 2 PEs, no structure memory +@system pe=2, sm=0 + +; Define nodes +&c1|pe0 <| const, 3 ; constant 3 on PE 0 +&c2|pe0 <| const, 7 ; constant 7 on PE 0 +&result|pe0 <| add ; adder on PE 0 +&output|pe1 <| pass ; output relay on PE 1 + +; Wire the graph +&c1 |> &result:L ; const 3 → adder left input +&c2 |> &result:R ; const 7 → adder right input +&result |> &output:L ; sum → output relay +``` + +**What happens at runtime:** + +1. The assembler emits two seed tokens (for `&c1` and `&c2`) since + they are `CONST` nodes with no incoming edges. +2. Both tokens arrive at PE 0's matching store. `&result` is a dyadic + instruction — it waits for both operands. +3. When both arrive, the matching store pairs them. The left operand + (3) and right operand (7) feed the ALU. +4. The ALU computes `3 + 7 = 10` and emits a token to `&output` on + PE 1. +5. `&output` is monadic (`pass`) — it bypasses the matching store and + immediately emits the value 10. + +## Structure Memory Example + +Write a value to SM, then read it back: + +```vhdl +@system pe=3, sm=1 + +; Pre-initialise SM cell 5 with value 0x42 +@val|sm0:5 = 0x42 + +; Trigger a read of cell 5 +&trigger|pe0 <| const, 1 +&reader|pe0 <| read, 5 ; read SM 0, cell 5 +&relay|pe1 <| pass +&sink|pe2 <| pass + +&trigger |> &reader:L ; trigger the read operation +&reader |> &relay:L ; SM result → relay +&relay |> &sink:L ; relay → final sink +``` + +The `read` instruction is monadic — it takes a trigger token and issues +a read request to SM. The `const` argument (`5`) specifies the cell +address. The SM returns the stored value (0x42 = 66 decimal) as a +token routed back to the `read` node's destination. + +## Switch Routing Example + +Branch on equality, routing data to the taken or not-taken path: + +```vhdl +@system pe=3, sm=0 + +&val|pe0 <| const, 5 ; value to test +&cmp|pe0 <| const, 5 ; comparison target +&branch|pe0 <| sweq ; switch-on-equal + +&taken|pe1 <| pass +¬_taken|pe1 <| pass +&output|pe2 <| pass + +; Wire inputs +&val |> &branch:L +&cmp |> &branch:R + +; Wire outputs — source port qualifiers select output slot +&branch:L |> &taken:L ; taken path (data) +&branch:R |> ¬_taken:L ; not-taken path (trigger) + +; Merge for downstream +&taken |> &output:L +¬_taken |> &output:R +``` + +Since `val == cmp` (both 5), `sweq` evaluates to true: data (5) goes +to `dest_l` (taken) and a trigger token (0) goes to `dest_r` +(not_taken). + +## Auto-Placement + +Nodes without explicit `|peN` qualifiers are automatically placed by +the assembler: + +```vhdl +@system pe=3, sm=0 + +&c1 <| const, 5 +&c2 <| const, 3 +&result <| add +&output <| pass + +&c1 |> &result:L +&c2 |> &result:R +&result |> &output:L +``` + +The assembler's greedy placer assigns PEs based on connectivity — nodes +connected by edges prefer to share a PE (minimising cross-PE traffic). +The result is functionally identical to explicit placement. + +## From Source to Execution + +### Lowering and Resolution + +After parsing, the assembler lowers the CST to an intermediate +representation (`IRGraph`). Names are qualified, scopes are created, +and edges are validated. The resolve pass checks that every edge +endpoint exists and produces suggestions for typos. + +### Placement and Allocation + +Unplaced nodes get PE assignments. Then the allocator assigns each node +an IRAM offset and context slot. Dyadic instructions are packed at low +IRAM offsets (0..D-1), monadic above (D..D+M-1). This layout matches +the hardware contract: the token's offset field doubles as the matching +store entry for dyadic instructions. + +Context slots are assigned per function scope per PE — each function +body sharing a PE gets its own context slot, enabling concurrent +activations to coexist without operand interference. + +### Code Generation + +The assembler offers two output modes: + +**Direct mode** produces `PEConfig` objects (IRAM contents, route +restrictions, context slot count) and `SMConfig` objects (initial cell +values), plus seed tokens. This is the fast path for the emulator — +configuration is applied directly. + +**Token stream mode** produces a bootstrap sequence: SM initialisation +writes, route configuration tokens, instruction load tokens, then seed +tokens. This mirrors the hardware bootstrap protocol — the same +sequence that the I/O controller would emit over the network to +configure a physical system. + +Both modes produce identical execution results. The token stream mode +exists because it validates the end-to-end bootstrap path that real +hardware will use. diff --git a/design-notes/io-and-bootstrap.md b/design-notes/io-and-bootstrap.md index 0f57a18..497bade 100644 --- a/design-notes/io-and-bootstrap.md +++ b/design-notes/io-and-bootstrap.md @@ -29,9 +29,9 @@ acceptable for decode and handling to take extra cycles. ### What It Is -A fixed-function device on the network. NOT a PE — no matching store, no -instruction memory, no ALU. it receives type-11 subtype-00 packets, -interprets them as I/O commands, and responds. +A fixed-function device on the network (not a PE): it has no matching +store, instruction memory, or ALU. it receives type-11 subtype-00 +packets, interprets them as I/O commands, and responds. it is also the only network participant that can **spontaneously generate tokens** without first receiving one. this is how external events (UART diff --git a/design-notes/network-and-communication.md b/design-notes/network-and-communication.md index 72f43ee..a219c3a 100644 --- a/design-notes/network-and-communication.md +++ b/design-notes/network-and-communication.md @@ -101,9 +101,9 @@ multiple levels. for v0, "flat addressing" is just how shared buses work. ### Hierarchical Prefix-Based Routing -NOT Manchester-style omega network. prefix routing gives variable latency: -local = 1 hop, cross-cluster = 2-3 hops. average latency depends on -program locality, which the compiler can optimise. +Prefix-based hierarchical routing gives variable latency: local = 1 hop, +cross-cluster = 2-3 hops. average latency depends on program locality, +which the compiler can optimise. - Top bits of PE_id select cluster, lower bits select within cluster - Each routing node has a small prefix lookup table, configured at @@ -255,8 +255,7 @@ resolves it. 1. Exact routing node logic — 16-bit latch + comparator on flit 1 + flit counter + mux, or something more sophisticated? 2. Bus arbitration policy — round-robin vs priority? priority for type-11 - config traffic during bootstrap? arbitration granularity: per-flit or - per-packet (once flit 1 wins arbitration, hold path for remaining flits)? + config traffic during bootstrap? 3. FIFO depth at each boundary — 8-deep at PE input is the current sketch, what about routing node latches and SM FIFOs? FIFO entries are 16-bit (one flit) or 32-bit (one reassembled token)? diff --git a/design-notes/pe-design.md b/design-notes/pe-design.md index 7d9d006..692bbe1 100644 --- a/design-notes/pe-design.md +++ b/design-notes/pe-design.md @@ -149,9 +149,9 @@ Control logic (state machine, handshaking): ~500-1000 transistors **Per-PE total: ~3-5K transistors of logic + SRAM chips + ser/deser** -(Revised down from earlier 5-8K estimate. The matching stage is -dramatically simpler than originally sketched now that hash fallback is -removed from the primary pipeline. See matching store section.) +The matching stage is the main cost saver — pure direct indexing with +no hash fallback keeps it to ~200-300 transistors. See matching store +section below. ## Instruction Memory @@ -179,6 +179,11 @@ independent of bus width** — it is sized for encoding needs, not bus constraints. See `bus-architecture-and-width-decoupling.md` for the full rationale. +> **⚠ Preliminary:** IRAM width and field allocation are estimates, not +> committed. The emulator uses Python dataclasses (`ALUInst`, `SMInst`) +> that don't reflect bit-level encoding. Hardware encoding is deferred +> to physical build. + #### IRAM Width | Field | Purpose | Bits (est.) | @@ -358,6 +363,12 @@ power-of-two addresses. No awkward 18-bit word widths. The metadata registers alongside the SRAM. The presence bit array for 256 entries is just 32 bytes. +> **⚠ Preliminary:** These configurations are candidates, not commitments. +> The emulator defaults to 16 context slots and 128 IRAM entries (matching +> Config B sizing). Final sizing depends on compiling real programs and +> measuring actual concurrent activation counts and dyadic instruction +> density per PE. + **Recommendation for v0**: start with Config B (16 slots x 32 entries = 1KB) to match the current 4-bit ctx_slot token field. Upgrade to Config C (32 x 32 = 2KB, needs 5-bit ctx_slot) if 16 concurrent activations proves too tight. The physical SRAM chip doesn't change between these configs — just the address generation logic. ### Instruction Address vs Matching Store Address @@ -367,27 +378,26 @@ These are NOT the same thing, and this distinction matters: - **Instruction address** (used in Stage 3): indexes into instruction memory SRAM. 7-8 bits (128-256 instructions per PE). Used by ALL token types. This is the "offset" field in the token. - **Matching store address** (used in Stage 2): indexes into matching store SRAM. Composed of [ctx_slot : match_entry]. Only used by dyadic tokens. -The compiler maintains the mapping. For dyadic instructions, the instruction word in IM includes a "match_entry" field that tells the hardware which matching store entry corresponds to this instruction. - -This means the matching store is dense with respect to dyadic instructions — no gaps for monadic instructions. A function chunk with 20 instructions, 8 of which are dyadic, uses 8 matching store entries, not 20. - -**Simplest v0 approach:** the token carries the instruction memory offset (for Stage 3). The instruction word fetched in Stage 3 contains the match_entry index (for Stage 2, which already happened on the previous pipeline cycle for the previous token — or, more precisely, the match stage reads match_entry from a lookup table indexed by offset, stored alongside the instruction memory). - -Actually, wait — Stage 2 happens BEFORE Stage 3. So the match_entry must come from the token, not the instruction word. This means either: - -(a) The token carries both an instruction offset AND a match entry index. Costs token bits. May require the offset field to be split or the match_entry to be packed into unused bits. - -(b) The match_entry IS the instruction offset, and the instruction memory is laid out so that the offset of a dyadic instruction is also its matching store entry within the slot. This works if the compiler assigns offsets such that dyadic instruction offsets are dense (0, 1, 2, ...) and monadic instruction offsets are in a separate range. - -(c) A small lookup ROM/SRAM alongside the matching store maps instruction offset -> match_entry. This adds a read before the match SRAM access (serial, adds latency) or requires a second SRAM port (parallel, adds hardware). +The matching store is dense with respect to dyadic instructions — no +gaps for monadic instructions. A function chunk with 20 instructions, +8 of which are dyadic, uses 8 matching store entries, not 20. -Option (b) is the simplest if the compiler can make it work. The instruction memory layout would be: dyadic instructions at offsets 0..M-1, monadic instructions at offsets M..N-1. The token's offset field directly indexes both the matching store (for dyadic) and the instruction memory (for everything). The matching store just doesn't get accessed for offsets +### Unified Offset: Instruction Address = Matching Store Entry -> = M (monadic range). +The token's offset field serves double duty. Because Stage 2 (matching) +happens before Stage 3 (instruction fetch), the match_entry must come +from the token itself, not from the instruction word. The solution: +the token's offset field IS the matching store entry for dyadic +instructions, and also indexes into IRAM for all instructions. -This constrains instruction memory layout — dyadic instructions must be packed at the low end. But the compiler controls the layout, so this is achievable. +This works because the compiler packs IRAM with dyadic instructions at +low offsets (0..M-1) and monadic instructions above (M..N-1). The +matching store is addressed by `[ctx_slot : offset]`, and offsets < M +correspond to dyadic instructions that use the matching store. Offsets +≥ M are monadic — they bypass matching entirely. -**Recommendation for v0:** option (b). Dyadic instructions packed at offsets 0..M-1 in instruction memory, monadic at M..N-1. Token offset directly serves as both instruction address and (for offsets < M) matching store entry within the context slot. Clean, no extra bits, no extra lookup, single cycle. Constraint on the compiler, not on the hardware. +> **Note:** See `design-alternatives.md` §11 for the other +> approaches considered and why unified offset was chosen. ### What About Overflow? @@ -409,8 +419,6 @@ If the matching store is full (all slots occupied) or a function body exceeds M - If runtime overflow becomes a real issue (genuinely unpredictable recursion depth), a fallback path could spill overflow tokens to external RAM or SM RAM with address translation via a small TLB/LUT -- More scalable than the earlier CAM-based overflow concept (100142 - chips are tiny and expensive) - The TLB/LUT maps overflow context slots to external memory addresses; on overflow, the PE issues a memory write to spill the token and a read to recover it when a local slot frees @@ -429,8 +437,8 @@ If the matching store is full (all slots occupied) or a function body exceeds M ### Deallocation -- Compiler inserts explicit "free" instruction on every exit path -- Free instruction clears the slot's occupied bits (all entries in the slot) and returns the slot ID to the free pool +- Compiler inserts explicit FREE_CTX instruction on every exit path +- FREE_CTX clears the slot's occupied bits (all entries in the slot) and returns the slot ID to the free pool - Multiple frees are idempotent / harmless - Freed slots are immediately available for reallocation @@ -459,34 +467,31 @@ If the matching store is full (all slots occupied) or a function body exceeds M 2. **Matching store metadata storage**: flip-flop register file for presence/port/gen, or tiny SRAM? Depends on slot count and available chip count budget per PE. -3. **Instruction memory layout**: dyadic-first packing (option b) seems - clean. Any cases where this constraint causes the compiler grief? -4. **Free slot tracking**: bump allocator + bitmap + priority encoder? Or +3. **Free slot tracking**: bump allocator + bitmap + priority encoder? Or free-slot FIFO? -5. **Instruction encoding**: operation set, format, IRAM width (32 or 48 +4. **Instruction encoding**: operation set, format, IRAM width (32 or 48 bits). Decoupled from bus width — driven by opcode + destination fields. See field table in IRAM section above. -6. **Function splitting heuristics**: how does the compiler decide where +5. **Function splitting heuristics**: how does the compiler decide where to split? Minimise cross-PE traffic? Balance slot usage across PEs? Hardware constraints (slot count, entry count) drive it. -7. **Flit-1 bit allocation**: how to divide the 12 bits after type + PE_id +6. **Flit-1 bit allocation**: how to divide the 12 bits after type + PE_id among instr_addr, ctx_slot, port, and gen. Drives context slot count and instruction address space simultaneously. See `bus-architecture-and-width-decoupling.md` for tradeoff analysis. -8. **Matching store overflow fallback**: possible hash fallback to - external RAM or SM RAM with address translation via small TLB/LUT, - rather than the earlier CAM-based overflow buffer concept. -9. **Instruction identity detection**: how does the PE know loaded code +7. **Matching store overflow fallback**: possible fallback to external + RAM or SM RAM with address translation via small TLB/LUT. +8. **Instruction identity detection**: how does the PE know loaded code matches what an arriving token expects? Fragment ID register vs entry gate instruction vs software-only guarantee. The identity problem is harder than simple presence — a validity bit doesn't distinguish "right instruction" from "wrong instruction loaded over the top." See Instruction Residency section. -10. **Miss handling mechanism**: stall + fetch request vs recirculate + +9. **Miss handling mechanism**: stall + fetch request vs recirculate + fetch request. Depends on expected miss frequency and tolerance for PE stalls. v0 may start with software-only invariant (no hardware miss path). -11. **Code storage in SM**: format and addressing for code pages stored +10. **Code storage in SM**: format and addressing for code pages stored in SM. How does the loader PE locate and fetch code pages? SM address space partitioning between code and data. @@ -701,4 +706,4 @@ activations on PEs that don't yet have the right code loaded. - `17407_17358.pdf` — DFM evaluation: OM structure (1024 CAM blocks, 32 words each, 8 entries of 4 words, 4-way set-associative within entry). Function activation via CCU requesting least-loaded PE, then getting instance name from target PE's free instance table. IM is 8KW/PE, identical across all PEs. Critical for understanding why Amamiya's OM is so large and why ours can be much smaller. - `gurd1985.pdf` — Manchester matching unit: 16 parallel hash banks, 64K tokens each, 54-bit comparators, 180ns clock period. Overflow unit emulated in software. Shows the cost of general-purpose matching. - `Dataflow_Machine_Architecture.pdf` — Veen survey: matching store analysis, tag space management, overflow handling across multiple architectures. -- `amamiya1982.pdf` — Original DFM paper: semi-CAM concept, IM/OM split, execution control mechanism with associative IM fetch. Partial function body execution (begin executing when first argument arrives, don't wait for all arguments). \ No newline at end of file +- `amamiya1982.pdf` — Original DFM paper: semi-CAM concept, IM/OM split, execution control mechanism with associative IM fetch. Partial function body execution (begin executing when first argument arrives, don't wait for all arguments). diff --git a/design-notes/sm-design.md b/design-notes/sm-design.md index e6159ac..c5d5f89 100644 --- a/design-notes/sm-design.md +++ b/design-notes/sm-design.md @@ -577,42 +577,28 @@ The 9-bit address in the compact structure token (type 10) gives only ## Open Design Questions -1. **Result token type** — always monadic (type 01)? or sometimes dyadic? - see "open question" in interface protocol section above. for deferred - reads, the result is constructed later — does it still use the return - routing from the original request, or does it need additional info? - (answer: yes, the deferred read register holds the full return routing, - so the result is constructed identically to an immediate result.) -2. **CAS multi-flit handling** — how does the request FIFO handle 3-flit +1. **CAS multi-flit handling** — how does the request FIFO handle 3-flit ops? does it buffer all flits before dispatching, or pipeline them? CAS needs expected_value, new_value, AND return routing — that's 48 bits of payload beyond the opcode/address. 3 flits minimum. -3. **Page register per-CM or global?** — if multiple CMs access the same +2. **Page register per-CM or global?** — if multiple CMs access the same SM, do they share a page register (contention) or each have their own (more hardware, more config)? probably global for v0. -4. **Banking vs pipeline depth** — with 2 banks, can we overlap a read to +3. **Banking vs pipeline depth** — with 2 banks, can we overlap a read to bank 0 with a write to bank 1? worth the control complexity for v0? presence state SRAM complicates this — is presence per-bank or shared? if shared, it serialises cross-bank operations. if per-bank, each bank needs its own presence SRAM. probably shared for v0 (simpler). -5. **SRAM chip selection** — specific part numbers, speed grades, package. +4. **SRAM chip selection** — specific part numbers, speed grades, package. needs to match the target clock frequency. presence state SRAM needs to be at least as fast as data SRAM (same access path). -6. **Code storage in SM** — SM can hold instruction pages as well as data. - Addressing and format for code pages stored in SM, partitioning between - code and data regions, and the protocol for a loader PE to read code - from SM and reformat as type-11 config writes to target PEs. See - `pe-design.md` Instruction Residency section. -7. **WRITE-on-FULL semantics** — overwrite-with-diagnostic (option c) is - recommended for v0. revisit if write-once enforcement is needed for - specific use cases. -8. **RAW_READ** — non-blocking read that returns data or empty indicator +5. **RAW_READ** — non-blocking read that returns data or empty indicator without registering a deferred read. useful for polling and diagnostics. opcode slot reserved (internal 0101) but not committed. implement if needed during testing. -9. **Atomic ops on non-FULL cells** — READ_INC/READ_DEC/CAS on EMPTY or +6. **Atomic ops on non-FULL cells** — READ_INC/READ_DEC/CAS on EMPTY or WAITING cells is currently undefined. options: error, stall, or treat as zero. error is safest for v0. -10. **Direct path input mux** — when direct path is added, the SM needs a +7. **Direct path input mux** — when direct path is added, the SM needs a mux between bus input and direct input feeding the internal command bus. arbitration policy TBD (direct path priority? round-robin?). diff --git a/design-notes/syntax-sketch.md b/design-notes/syntax-sketch.md index 7e640b4..fa9bb43 100644 --- a/design-notes/syntax-sketch.md +++ b/design-notes/syntax-sketch.md @@ -1,3 +1,8 @@ +> **⚠ SUPERSEDED** — This was an early syntax sketch. The canonical grammar +> is `dfasm.lark` (Lark/Earley parser, 38+ tests in `tests/test_parser.py`). +> Mnemonics, naming conventions, and some semantics have diverged from this +> sketch. Preserved for historical context and design rationale only. + ## Ops ### CM tokens @@ -32,6 +37,7 @@ everything about them is essentially the same except for dyadic/monadic - shiftl - Shift A left by N+1 bits. N from IRAM immediate field (1-8 allow for shifting of a full byte's width) - shiftr - Logical shift A right by N+1 bits. Zero-fill. N from immediate. - ashftr - Arithmetic shift A right by N bits. Sign-extend. N from immediate. + (NOTE: implementation uses `shl`/`shr`/`asr` mnemonics — see `alu-and-output-design.md`) #### Logical - and @@ -59,7 +65,7 @@ All of these have perhaps odd wire semantics (see alu-and-output-design.md) #### Data (all monadic) - pass - const - constant load -- free - dealloc context slot, no data output +- free_ctx - dealloc context slot, no data output #### System (Type 4) diff --git a/tokens.py b/tokens.py index a619d87..5ad6cef 100644 --- a/tokens.py +++ b/tokens.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from typing import List, Optional from cm_inst import ALUInst, CfgOp, MemOp, Port, SMInst -- 2.51.2