Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion python/assassyn/builder/type_oriented_namer.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def _symbol_to_name():
"""Convert operator symbols to descriptive names."""
return {
'+': 'add', '-': 'sub', '*': 'mul', '/': 'div', '%': 'mod',
'&': 'and', '|': 'or', '^': 'xor',
'&': 'and_', '|': 'or_', '^': 'xor_',
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The operator symbol mapping has been changed from '&': 'and' to '&': 'and_' (and similarly for '|' and '^'). This change adds trailing underscores to avoid conflicts with Python keywords. However, this is a potentially breaking change if any code depends on the old naming convention.

Verify that:

  1. All existing code that references these operator names has been updated
  2. This change is intentional and documented in the PR or changelog
  3. The new names align with Python's operator module naming (e.g., operator.and_, operator.or_)

Copilot uses AI. Check for mistakes.
'<': 'lt', '>': 'gt', '<=': 'le', '>=': 'ge', '==': 'eq', '!=': 'neq',
'<<': 'shl', '>>': 'shr',
'!': 'not',
Expand Down
2 changes: 1 addition & 1 deletion python/assassyn/codegen/verilog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ Keeping these definitions in a runtime module ensures generated designs and user

## Handshake & Scheduling

- `executed_wire` gates side‑effects each cycle (built through `_format_reduction_expr` so OR / AND reductions share the same formatting):
- `executed_wire` gates side‑effects each cycle (built through `predicates.reduce_predicates` so OR / AND reductions share the same formatting):
- Drivers: `trigger_counter_pop_valid [& WAIT_UNTIL]`
- Downstreams: OR of upstream `inst_<dep>.executed`
- FIFO push (producer of `<C>.<p>`):
Expand Down
26 changes: 12 additions & 14 deletions python/assassyn/codegen/verilog/_expr/arith.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,17 +164,15 @@ def codegen_select1hot(dumper, expr: Select1Hot) -> Optional[str]:
num_values = len(values)
selector_bits = max((num_values - 1).bit_length(), 1)
if num_values == 2:
body = f"{cond}.as_bits()[1]"
else:
dumper.append_code(f"{cond}_res = Bits({selector_bits})(0)")
for i in range(num_values):
dumper.append_code(
f"{cond}_res = Mux({cond}[{i}] ,"
f" {cond}_res , Bits({selector_bits})({i}))")

values_str = ", ".join(values)
mux_code = f"{rval} = Mux({cond}_res, {values_str})"
dumper.append_code(mux_code)
return None

return body
return f"{rval} = Mux({cond}.as_bits()[1], {values[0]}, {values[1]})"

dumper.append_code(f"{cond}_res = Bits({selector_bits})(0)")
Comment on lines +167 to +169
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two-value case now returns an assignment expression directly, which should be consistent with the multi-value case that also appends code. However, this creates an inconsistency: when num_values == 2, the function returns a string (to be assigned), while for other cases it returns None after appending code. This mixed return behavior could be confusing.

Consider making the two-value case consistent by always appending code and returning None:

dumper.append_code(f"{rval} = Mux({cond}.as_bits()[1], {values[0]}, {values[1]})")
return None
Suggested change
return f"{rval} = Mux({cond}.as_bits()[1], {values[0]}, {values[1]})"
dumper.append_code(f"{cond}_res = Bits({selector_bits})(0)")
dumper.append_code(f"{rval} = Mux({cond}.as_bits()[1], {values[0]}, {values[1]})")
return None

Copilot uses AI. Check for mistakes.
for i in range(num_values):
dumper.append_code(
f"{cond}_res = Mux({cond}[{i}] ,"
f" {cond}_res , Bits({selector_bits})({i}))")

values_str = ", ".join(values)
mux_code = f"{rval} = Mux({cond}_res, {values_str})"
dumper.append_code(mux_code)
return None
2 changes: 1 addition & 1 deletion python/assassyn/codegen/verilog/_expr/intrinsics.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ The module uses several utility functions:

- `dump_rval()` from [rval module](/python/assassyn/codegen/verilog/rval.md) for generating signal references
- `unwrap_operand()` and `namify()` from [utils module](/python/assassyn/utils.md) for operand processing and name generation
- `get_pred()` from [CIRCTDumper](/python/assassyn/codegen/verilog/design.md) for getting current execution predicate
- `format_predicate()` from [CIRCTDumper](/python/assassyn/codegen/verilog/design.md) which consumes `expr.meta_cond` captured on each IR node to produce `Bits(1)` guards recorded alongside external exposures
- `external_instance_names` / `external_wrapper_names` maps on the dumper to coordinate `ExternalIntrinsic` handling across passes

The intrinsic expression generation is integrated into the main expression dispatch system through the [__init__.py](/python/assassyn/codegen/verilog/_expr/__init__.md) module, which routes different expression types to their appropriate code generation functions.
Expand Down
56 changes: 22 additions & 34 deletions python/assassyn/codegen/verilog/_expr/intrinsics.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,47 +157,36 @@ def _handle_external_output(dumper, expr, intrinsic, rval):
port_name = port_operand.value if hasattr(port_operand, 'value') else port_operand
index_operand = expr.args[2] if len(expr.args) > 2 else None

result = None
instance_owner = dumper.external_metadata.owner_for(instance)
if instance_owner and instance_owner != dumper.current_module:
# Cross-module access: use the exposed port value provided on inputs.
port_name_for_read = dumper.get_external_port_name(expr)
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The refactored code at line 163 returns early for cross-module access cases. However, the original code had logic to populate dumper.external_wire_assignment_keys and dumper.external_wire_assignments for cross-module cases. While these have been removed (based on system.py changes), the code should ensure that the external metadata registry is still properly populated elsewhere.

Verify that the ExternalRegistry is being updated to track these cross-module reads, or document why this tracking is no longer needed.

Suggested change
port_name_for_read = dumper.get_external_port_name(expr)
port_name_for_read = dumper.get_external_port_name(expr)
# Register this cross-module read in the external metadata registry
if hasattr(dumper, "external_metadata") and hasattr(dumper.external_metadata, "register_read"):
dumper.external_metadata.register_read(instance, port_name, expr)

Copilot uses AI. Check for mistakes.
wire_key = dumper.get_external_wire_key(instance, port_name, index_operand)
assignment_key = (dumper.current_module, wire_key)
if assignment_key not in dumper.external_wire_assignment_keys:
dumper.external_wire_assignment_keys.add(assignment_key)
dumper.external_wire_assignments.append({
'consumer': dumper.current_module,
'producer': instance_owner,
'expr': expr,
'wire': wire_key,
})
result = f"{rval} = self.{port_name_for_read}"
else:
inst_name = dumper.external_instance_names.get(instance)
if inst_name is None:
inst_name = dumper.dump_rval(instance, False)
dumper.external_instance_names[instance] = inst_name
return f"{rval} = self.{port_name_for_read}"

port_specs = instance.external_class.port_specs()
wire_spec = port_specs.get(port_name)
result = None
inst_name = dumper.external_instance_names.get(instance)
if inst_name is None:
inst_name = dumper.dump_rval(instance, False)
dumper.external_instance_names[instance] = inst_name

if wire_spec is not None and wire_spec.kind == 'reg':
if index_operand is None:
result = f"{rval} = {inst_name}.{port_name}"
else:
idx_operand = unwrap_operand(index_operand)
if isinstance(idx_operand, Const) and idx_operand.value == 0:
result = f"{rval} = {inst_name}.{port_name}"
else:
index_code = dumper.dump_rval(index_operand, False)
result = f"{rval} = {inst_name}.{port_name}[{index_code}]"
port_specs = instance.external_class.port_specs()
wire_spec = port_specs.get(port_name)

if wire_spec is not None and wire_spec.kind == 'reg':
if index_operand is None:
result = f"{rval} = {inst_name}.{port_name}"
else:
if index_operand is None:
idx_operand = unwrap_operand(index_operand)
if isinstance(idx_operand, Const) and idx_operand.value == 0:
result = f"{rval} = {inst_name}.{port_name}"
else:
index_code = dumper.dump_rval(index_operand, False)
result = f"{rval} = {inst_name}.{port_name}[{index_code}]"
else:
if index_operand is None:
result = f"{rval} = {inst_name}.{port_name}"
else:
index_code = dumper.dump_rval(index_operand, False)
result = f"{rval} = {inst_name}.{port_name}[{index_code}]"

return result

Expand Down Expand Up @@ -259,16 +248,15 @@ def codegen_external_intrinsic(dumper, expr: ExternalIntrinsic) -> Optional[str]
continue
seen_keys.add(wire_key)
output_name = f"{rval}_{entry.port_name}"
dumper.external_wire_outputs[wire_key] = output_name
current_pred = dumper.get_pred(expr)
meta_cond = expr.meta_cond if hasattr(expr, "meta_cond") else None
exposures.setdefault(wire_key, {
'output_name': output_name,
'dtype': entry.expr.dtype,
'instance_name': rval,
'port_name': entry.port_name,
'index_operand': entry.index_operand,
'index_key': wire_key[2],
'condition': current_pred,
'meta_cond': meta_cond,
})


Expand Down
20 changes: 13 additions & 7 deletions python/assassyn/codegen/verilog/cleanup.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ This module provides post-generation cleanup utilities for Verilog code generati

The cleanup module is responsible for generating the final control signals and interconnections after the main Verilog code generation is complete. It handles complex signal routing for arrays, ports, modules, and memory interfaces, ensuring proper connectivity between generated modules according to the credit-based pipeline architecture. As part of the external module flow, it also materialises producer-side `expose_*`/`valid_*` ports for any external register outputs that are consumed by another module, so cross-module reads can be wired up without duplicating logic. FIFO wiring now consumes the metadata snapshot produced by the pre-pass instead of mutating registries during emission.

Following the 2024-03 refresh the module now depends on
`python.assassyn.codegen.verilog.predicates` for all predicate reductions and mux-chain
construction. Shared helpers (`reduce_predicates`, `emit_predicate_mux_chain`) replace
the previous private `predicates.reduce_predicates` / `predicates.emit_predicate_mux_chain` pair, so the
cleanup logic mirrors the top-level harness and keeps ordering semantics in one place.
Comment on lines +10 to +13
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The documentation references predicates.reduce_predicates and predicates.emit_predicate_mux_chain without the full module path. For consistency with the rest of the documentation and clarity, these should reference the full module path: python.assassyn.codegen.verilog.predicates.reduce_predicates and python.assassyn.codegen.verilog.predicates.emit_predicate_mux_chain.

Copilot uses AI. Check for mistakes.

Metadata consumed here (`ModuleMetadata`, `ModuleInteractionView`, `ArrayMetadata`,
and `FIFOInteractionView`) is provided by the `python.assassyn.codegen.verilog.metadata`
package. Implementations live in the `metadata.module`, `metadata.array`, and
Expand All @@ -25,7 +31,7 @@ def cleanup_post_generation(dumper):
This is the main cleanup function that generates all the necessary control signals and interconnections after the primary Verilog code generation is complete. It performs the following steps:

1. **Execution Signal Generation**: Creates the `executed_wire` signal that determines when a module should execute:
- For downstream modules: Gathers upstream dependencies with `analysis.get_upstreams(module)` and ORs their `executed` flags via `_format_reduction_expr(..., op="or_", default_literal="Bits(1)(0)")`.
- For downstream modules: Gathers upstream dependencies with `analysis.get_upstreams(module)` and ORs their `executed` flags via `predicates.reduce_predicates(..., op="or_", default_literal="Bits(1)(0)")`.
- For regular modules: ANDs the trigger-counter pop-valid input with any active `wait_until` predicate recorded during expression lowering using the same helper with `op="and_"` and a `Bits(1)(1)` default.

2. **Finish Signal Generation**: Reduces every FINISH site captured in
Expand All @@ -38,13 +44,13 @@ This is the main cleanup function that generates all the necessary control signa
`module_metadata.interactions.writes`:
- Filters out arrays whose owner is a memory instance and satisfy `array.is_payload(owner)`, because those are handled by dedicated memory logic.
- Uses the module view’s `writes(array)` tuples (which mirror the global array view maintained by the `InteractionMatrix`) to map interactions onto the precomputed port indices stored in the `ArrayMetadataRegistry`.
- Emits write-enable, write-data, and write-index signals per port, formatting each write’s `expr.meta_cond` with `dumper.format_predicate`. Multi-writer modules rely on `_emit_predicate_mux_chain` to collapse predicates and thread prioritised mux chains for data and indices, guaranteeing consistent selection semantics.
- Emits write-enable, write-data, and write-index signals per port, formatting each write’s `expr.meta_cond` with `dumper.format_predicate`. Multi-writer modules rely on `predicates.emit_predicate_mux_chain` to collapse predicates and thread prioritised mux chains for data and indices, guaranteeing consistent selection semantics.

5. **FIFO Signal Generation**: Walks `module_metadata.interactions.fifo_ports` to visit each FIFO touched by the module:
- Pulls the per-port `FIFOInteractionView` directly from the shared matrix so the recorded `FIFOPush` / `FIFOPop` expressions stay in sync across consumers—predicates come from each expression’s `meta_cond`, push data from `expr.val`, and module ownership from the metadata view that registered the expression.
- Applies backpressure via the parent module's `fifo_*_push_ready` signals and emits valid/data assignments driven purely from metadata captured during the pre-pass.
- Produces the module-local `*_pop_ready` backpressure signal without consulting dumper internals.
- Reuses `_emit_predicate_mux_chain` so the push-valid reduction and push-data mux mirror the prioritisation used for array writes.
- Reuses `predicates.emit_predicate_mux_chain` so the push-valid reduction and push-data mux mirror the prioritisation used for array writes.

6. **Module Trigger Signal Generation**: Reads async trigger exposures from `dumper.interactions.async_ledger.calls_for_module(current_module)`, sums all predicates (each taken from the call’s `meta_cond` and converted to an 8-bit increment), and routes the result into `<callee>_trigger`.

Expand Down Expand Up @@ -86,10 +92,10 @@ This function generates the control signals specifically for SRAM memory interfa
- Understanding of [SRAM memory model](/python/assassyn/ir/memory/sram.md)
- Knowledge of [array read/write operations](/python/assassyn/ir/expr/array.md)

### `_emit_predicate_mux_chain`
### `emit_predicate_mux_chain`

```python
def _emit_predicate_mux_chain(entries, *, render_predicate, render_value, default_value, aggregate_predicates):
def emit_predicate_mux_chain(entries, *, render_predicate, render_value, default_value, aggregate_predicates):
"""Return both the mux chain and aggregate predicate for *entries*."""
```

Expand All @@ -113,7 +119,7 @@ The module uses several internal helper functions and imports utilities from oth
- `dump_type()` and `dump_type_cast()` from [utils](/python/assassyn/codegen/verilog/utils.md) for type handling
- `get_sram_info()` from [utils](/python/assassyn/codegen/verilog/utils.md) for SRAM information extraction
- `namify()` and `unwrap_operand()` from [utils](/python/assassyn/utils.md) for name generation and operand handling
- `_format_reduction_expr(predicates, *, default_literal, op="or_")` canonicalises OR/AND-style predicate reductions, emitting caller-provided defaults for empty sequences while allowing any reducer supported by the dumper runtime.
- `_emit_predicate_mux_chain()` centralises predicate-driven mux construction so callers reuse ordering and reduction semantics.
- `predicates.reduce_predicates(predicates, *, default_literal, op="or_")` canonicalises OR/AND-style predicate reductions, emitting caller-provided defaults for empty sequences while allowing any reducer supported by the dumper runtime.
- `predicates.emit_predicate_mux_chain()` centralises predicate-driven mux construction so callers reuse ordering and reduction semantics.

The cleanup process is tightly integrated with the [CIRCTDumper](/python/assassyn/codegen/verilog/design.md) class and is called as the final step in module generation to ensure all interconnections are properly established.
Loading
Loading