OR-1 dataflow CPU sketch
at a8ec18c41dee4e0e31246e4d5becf68b6e828e90 356 lines 13 kB view raw
1"""Code generation for OR1 assembly. 2 3Converts fully allocated IRGraphs to emulator-ready configuration objects and 4token streams. Two output modes: 51. Direct mode: Produces PEConfig/SMConfig lists + seed tokens (for direct setup) 62. Token stream mode: Produces bootstrap sequence (SM init → IRAM writes → seeds) 7 8Reference: Phase 6 design doc, Tasks 1-2. 9""" 10 11from dataclasses import dataclass 12from collections import defaultdict 13 14from asm.errors import AssemblyError, ErrorCategory 15from asm.ir import ( 16 IRGraph, IRNode, IREdge, ResolvedDest, collect_all_nodes_and_edges, collect_all_data_defs, 17 DEFAULT_IRAM_CAPACITY, DEFAULT_CTX_SLOTS 18) 19from asm.opcodes import is_dyadic 20from cm_inst import ALUInst, MemOp, Port, RoutingOp, SMInst 21from emu.types import PEConfig, SMConfig 22from tokens import DyadToken, IRAMWriteToken, MonadToken, SMToken 23from sm_mod import Presence 24 25 26@dataclass(frozen=True) 27class AssemblyResult: 28 """Result of code generation in direct mode. 29 30 Attributes: 31 pe_configs: List of PEConfig objects, one per PE 32 sm_configs: List of SMConfig objects, one per SM with data_defs 33 seed_tokens: List of MonadTokens for const nodes with no incoming edges 34 """ 35 pe_configs: list[PEConfig] 36 sm_configs: list[SMConfig] 37 seed_tokens: list[MonadToken] 38 39 40 41 42def _build_iram_for_pe( 43 nodes_on_pe: list[IRNode], 44 all_nodes: dict[str, IRNode], 45 all_edges: list[IREdge], 46) -> dict[int, ALUInst | SMInst]: 47 """Build IRAM instruction dict for a single PE. 48 49 Args: 50 nodes_on_pe: List of IRNodes on this PE 51 all_nodes: All nodes in graph (for lookups) 52 all_edges: All edges in graph (for ctx_override detection) 53 54 Returns: 55 Dict mapping IRAM offset to ALUInst or SMInst 56 """ 57 iram = {} 58 59 # Build edge map for quick lookup: node name -> list of outgoing edges 60 edges_by_source = defaultdict(list) 61 for edge in all_edges: 62 edges_by_source[edge.source].append(edge) 63 64 for node in nodes_on_pe: 65 if node.iram_offset is None: 66 # Node not allocated, skip 67 continue 68 69 if isinstance(node.opcode, MemOp): 70 # Memory operation -> SMInst 71 ret_addr = node.dest_l.addr if isinstance(node.dest_l, ResolvedDest) else None 72 ret_dyadic = False 73 if isinstance(node.dest_l, ResolvedDest): 74 dest_node = all_nodes.get(node.dest_l.name) 75 if dest_node is not None: 76 ret_dyadic = is_dyadic(dest_node.opcode, dest_node.const) 77 inst = SMInst( 78 op=node.opcode, 79 sm_id=node.sm_id, 80 const=node.const, 81 ret=ret_addr, 82 ret_dyadic=ret_dyadic, 83 ) 84 else: 85 # ALU operation -> ALUInst 86 # Extract Addr from ResolvedDest or keep None 87 dest_l_addr = None 88 dest_r_addr = None 89 90 if node.dest_l is not None and isinstance(node.dest_l, ResolvedDest): 91 dest_l_addr = node.dest_l.addr 92 93 if node.dest_r is not None and isinstance(node.dest_r, ResolvedDest): 94 dest_r_addr = node.dest_r.addr 95 96 # Check if this node has ctx_override edges (AC5.2, AC5.3) 97 ctx_mode = 0 98 packed_const = node.const 99 node_edges = edges_by_source.get(node.name, []) 100 has_ctx_override = any(edge.ctx_override for edge in node_edges) 101 102 if has_ctx_override: 103 # AC5.3: Conflict detection - node with both const and ctx_override 104 if node.const is not None: 105 error = AssemblyError( 106 category=ErrorCategory.VALUE, 107 message=f"Node '{node.name}' requires both const operand and CTX_OVRD — expected expand pass to insert trampoline", 108 loc=node.loc, 109 ) 110 raise ValueError(f"Codegen error: {error.message}") 111 112 ctx_mode = 1 113 # Pack const field: target_ctx and target_gen 114 # Find the first ctx_override edge to get the target context 115 # The allocator should have set ctx on destination nodes 116 for edge in node_edges: 117 if edge.ctx_override: 118 dest_node = all_nodes.get(edge.dest) 119 if dest_node is not None and dest_node.ctx is not None: 120 target_ctx = dest_node.ctx 121 target_gen = 0 # Initial generation 122 # Pack: ((target_ctx & 0xF) << 4) | ((target_gen & 0x3) << 2) 123 # Upper 8 bits must be zero (reserved) 124 packed_const = ((target_ctx & 0xF) << 4) | ((target_gen & 0x3) << 2) 125 break 126 127 # Defensive guard: ensure packed_const is set for CTX_OVRD 128 if packed_const is None: 129 raise ValueError( 130 f"Codegen error: Node '{node.name}' has ctx_override edge but destination context is not resolved. " 131 f"Allocator should have assigned ctx to destination nodes." 132 ) 133 134 inst = ALUInst( 135 op=node.opcode, 136 dest_l=dest_l_addr, 137 dest_r=dest_r_addr, 138 const=packed_const, 139 ctx_mode=ctx_mode, 140 ) 141 142 iram[node.iram_offset] = inst 143 144 return iram 145 146 147def _compute_route_restrictions( 148 nodes_by_pe: dict[int, list[IRNode]], 149 all_edges: list[IREdge], 150 all_nodes: dict[str, IRNode], 151 pe_id: int, 152) -> tuple[set[int], set[int]]: 153 """Compute allowed PE and SM routes for a given PE. 154 155 Analyzes all edges involving nodes on this PE to determine which other 156 PEs and SMs it can route to. Includes self-routes. 157 158 Args: 159 nodes_by_pe: Dict mapping PE ID to list of nodes on that PE 160 all_edges: List of all edges in graph 161 all_nodes: Dict of all nodes 162 pe_id: The PE we're computing routes for 163 164 Returns: 165 Tuple of (allowed_pe_routes set, allowed_sm_routes set) 166 """ 167 nodes_on_pe_set = {node.name for node in nodes_by_pe.get(pe_id, [])} 168 169 pe_routes = {pe_id} # Always include self-route 170 sm_routes = set() 171 172 # Scan all edges for those sourced from this PE 173 for edge in all_edges: 174 if edge.source in nodes_on_pe_set: 175 # This edge originates from our PE 176 dest_node = all_nodes.get(edge.dest) 177 if dest_node is not None: 178 if dest_node.pe is not None: 179 pe_routes.add(dest_node.pe) 180 181 # Scan all nodes on this PE for SM instructions 182 for node in nodes_by_pe.get(pe_id, []): 183 if isinstance(node.opcode, MemOp) and node.sm_id is not None: 184 sm_routes.add(node.sm_id) 185 186 return pe_routes, sm_routes 187 188 189def generate_direct(graph: IRGraph) -> AssemblyResult: 190 """Generate PEConfig, SMConfig, and seed tokens from an allocated IRGraph. 191 192 Args: 193 graph: A fully allocated IRGraph (after allocate pass) 194 195 Returns: 196 AssemblyResult with pe_configs, sm_configs, and seed_tokens 197 """ 198 all_nodes, all_edges = collect_all_nodes_and_edges(graph) 199 all_data_defs = collect_all_data_defs(graph) 200 201 # Group nodes by PE 202 nodes_by_pe: dict[int, list[IRNode]] = defaultdict(list) 203 for node in all_nodes.values(): 204 if node.pe is not None: 205 nodes_by_pe[node.pe].append(node) 206 207 # Build PEConfigs 208 pe_configs = [] 209 for pe_id in sorted(nodes_by_pe.keys()): 210 nodes_on_pe = nodes_by_pe[pe_id] 211 212 # Build IRAM for this PE 213 iram = _build_iram_for_pe(nodes_on_pe, all_nodes, all_edges) 214 215 # Compute route restrictions 216 allowed_pe_routes, allowed_sm_routes = _compute_route_restrictions( 217 nodes_by_pe, all_edges, all_nodes, pe_id 218 ) 219 220 # Create PEConfig 221 config = PEConfig( 222 pe_id=pe_id, 223 iram=iram, 224 ctx_slots=graph.system.ctx_slots if graph.system else DEFAULT_CTX_SLOTS, 225 offsets=graph.system.iram_capacity if graph.system else DEFAULT_IRAM_CAPACITY, 226 allowed_pe_routes=allowed_pe_routes, 227 allowed_sm_routes=allowed_sm_routes, 228 ) 229 pe_configs.append(config) 230 231 # Build SMConfigs from data_defs 232 sm_configs_by_id: dict[int, dict[int, tuple[Presence, int]]] = defaultdict(dict) 233 for data_def in all_data_defs: 234 if data_def.sm_id is not None and data_def.cell_addr is not None: 235 sm_configs_by_id[data_def.sm_id][data_def.cell_addr] = ( 236 Presence.FULL, data_def.value 237 ) 238 239 sm_count = max(1, graph.system.sm_count if graph.system else 1) 240 for sm_id in range(sm_count): 241 if sm_id not in sm_configs_by_id: 242 sm_configs_by_id[sm_id] = {} 243 244 sm_configs = [] 245 for sm_id in sorted(sm_configs_by_id.keys()): 246 initial_cells = sm_configs_by_id[sm_id] 247 config = SMConfig( 248 sm_id=sm_id, 249 initial_cells=initial_cells if initial_cells else None, 250 ) 251 sm_configs.append(config) 252 253 # Detect seed tokens 254 seed_tokens = [] 255 256 # Build edge indices 257 edges_by_dest = defaultdict(list) 258 edges_by_source = defaultdict(list) 259 for edge in all_edges: 260 edges_by_dest[edge.dest].append(edge) 261 edges_by_source[edge.source].append(edge) 262 263 for node in all_nodes.values(): 264 if node.seed: 265 # Seed node: generate token(s) targeted at destination(s) 266 out_edges = edges_by_source.get(node.name, []) 267 for edge in out_edges: 268 dest_node = all_nodes.get(edge.dest) 269 if dest_node is None or dest_node.pe is None: 270 continue 271 dest_is_dyadic = is_dyadic(dest_node.opcode, dest_node.const) 272 if dest_is_dyadic: 273 token = DyadToken( 274 target=dest_node.pe, 275 offset=dest_node.iram_offset if dest_node.iram_offset is not None else 0, 276 ctx=dest_node.ctx if dest_node.ctx is not None else 0, 277 data=node.const if node.const is not None else 0, 278 port=edge.port, 279 gen=0, 280 wide=False, 281 ) 282 else: 283 token = MonadToken( 284 target=dest_node.pe, 285 offset=dest_node.iram_offset if dest_node.iram_offset is not None else 0, 286 ctx=dest_node.ctx if dest_node.ctx is not None else 0, 287 data=node.const if node.const is not None else 0, 288 inline=False, 289 ) 290 seed_tokens.append(token) 291 elif node.opcode == RoutingOp.CONST: 292 # Triggerable constant: CONST node in IRAM with no incoming edges 293 if node.name not in edges_by_dest: 294 token = MonadToken( 295 target=node.pe if node.pe is not None else 0, 296 offset=node.iram_offset if node.iram_offset is not None else 0, 297 ctx=node.ctx if node.ctx is not None else 0, 298 data=node.const if node.const is not None else 0, 299 inline=False, 300 ) 301 seed_tokens.append(token) 302 303 return AssemblyResult( 304 pe_configs=pe_configs, 305 sm_configs=sm_configs, 306 seed_tokens=seed_tokens, 307 ) 308 309 310def generate_tokens(graph: IRGraph) -> list: 311 """Generate bootstrap token sequence from an allocated IRGraph. 312 313 Produces tokens in order: SM init → IRAM writes → seeds 314 315 Args: 316 graph: A fully allocated IRGraph (after allocate pass) 317 318 Returns: 319 List of tokens (SMToken, IRAMWriteToken, MonadToken) in bootstrap order 320 """ 321 # Use direct mode to get configs and seeds 322 result = generate_direct(graph) 323 324 tokens = [] 325 326 # 1. SM init tokens 327 all_data_defs = collect_all_data_defs(graph) 328 for data_def in all_data_defs: 329 if data_def.sm_id is not None and data_def.cell_addr is not None: 330 token = SMToken( 331 target=data_def.sm_id, 332 addr=data_def.cell_addr, 333 op=MemOp.WRITE, 334 flags=None, 335 data=data_def.value, 336 ret=None, 337 ) 338 tokens.append(token) 339 340 # 2. IRAM write tokens 341 for pe_config in result.pe_configs: 342 offsets = sorted(pe_config.iram.keys()) 343 iram_instructions = [pe_config.iram[offset] for offset in offsets] 344 token = IRAMWriteToken( 345 target=pe_config.pe_id, 346 offset=0, 347 ctx=0, 348 data=0, 349 instructions=tuple(iram_instructions), 350 ) 351 tokens.append(token) 352 353 # 3. Seed tokens 354 tokens.extend(result.seed_tokens) 355 356 return tokens