diff --git a/.github/configs/mlc_config.json b/.github/configs/mlc_config.json index 0b6ff56de4..3cbaed9f60 100644 --- a/.github/configs/mlc_config.json +++ b/.github/configs/mlc_config.json @@ -18,6 +18,12 @@ { "pattern":"^https://intel\\.github\\.io(?:/|$)" }, + { + "pattern":"^https://marketplace\\.visualstudio\\.com" + }, + { + "pattern":"^https://pub\\.dev" + }, { "pattern":"^https://pymtl3.readthedocs.io/en/latest/$" }, diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c1a5e3fe3..ed2890553f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,37 @@ - Netlist JSON format is now `0.1.0`, preserving enum types and encodings on retained netnames using Yosys-style attributes, including constant-driven enum uses (). - Refined the typed-array hierarchy so `LogicArray` directly specializes `TypedLogicArray`, and strengthened custom `TypedLogicArray` cloning to preserve runtime types, codecs, unpacked dimensions, and constructor-only naming prefixes without exposing them as public axis metadata (). - Expanded `TypedLogicArray` support for recursively nested typed and ordinary arrays, structures containing array fields, mixed packed/unpacked dimensions, structured net and inout ports, and recursive netlist type metadata. Fixed generated SystemVerilog for array-valued elements to use packed offsets consistent with declarations and module-port connections, avoiding invalid chained selections in Icarus and Verilator (). +- Preserved configured SystemVerilog output object types for nested arrays and added the opt-in `iverilogWorkaroundForUnpackedArrayVariables` setting for child-driven unpacked array variables that remain unknown in Icarus Verilog 12.0 (). +- `LogicArray` remains source-compatible for construction and operations. Its `value` and `previousValue` now return shape-carrying `LogicValueArray` instances, which remain `LogicValue` subtypes; code relying on their exact concrete runtime types may observe this behavior change (). +- Added support for structures with constant leaves when their `clone()` implementation produces driveable fields, preserving domain-specific constant structures through generic operations. +- Integrated `LogicEnum` with generic operations. Enum mappings are preserved through typed operation results, enum members can be used as `cases` keys, and incompatible enum mappings are rejected. +- Fixed generic operations to reject incompatible `LogicEnum` mappings inside structures and typed arrays, and to preserve mapped enum constant reset values in netlist register cells. +- `Pipeline.get`, `PipelineStageInfo.get`, and `PipelineStageInfo.getAbs` now preserve each identifier's concrete type through stage reconstruction and SSA, allowing structures, typed arrays, enums, and scalar signals in one pipeline. `ReadyValidPipeline` retains its established multi-signal API and gains the same per-identifier typed access without separate payload pipeline classes. +- Preserved aggregate structure assignment metadata through synthesis and netlist generation. +- Fixed SystemVerilog synthesis of internally computed packed logic assigned to packed-array outputs, retaining required intermediate declarations. +- Upgraded `Mux`, `FlipFlop`, `Passthrough`, `mux`, `flop`, `cases`, + `selectIndex`, and `selectFrom` to preserve compatible concrete `Logic` + subtypes, including nested `LogicArray` and `TypedLogicArray` boundaries. + Removed the parallel structure-only operation APIs. +- **Migration:** plain `Const` and `LogicNet` sources no longer infer + driveable outputs for operations that construct hardware. Request + ``, for example `flop(clk, Const(1))`, to normalize them. + Use an operation `outputGenerator` when an explicit output representation is + required, including mixed packed and structured `cases` branches. +- **Migration:** type-preserving operations on `TypedLogicArray` operands now + require the identical `valueCodec` instance. Reuse a shared codec or cross + an explicit `packed` boundary to reinterpret compatible bits. +- **Breaking:** Removed compatibility APIs deprecated before `0.6.11`. Use the + canonical `ExternalSystemVerilogModule`, `FiniteStateMachine`, `Sequential`, + `If.block`, `Logic.port`, `LogicValue` `of...` constructors, `SystemVerilog`, + `SynthFileContents`, and instance `clone()` APIs. Apply interface port names + at connection time with `uniquify`; use canonical operation outputs such as + `out` and `sum`; and access signal values through `value`. Compatibility APIs + first deprecated in `0.6.11` remain available, including `generateSynth`, + `WaveDumper`, `hierarchyJSON`, and `contentsByName`. +- Added public `cloneTyped()` and `namedTyped()` conveniences for retaining a + receiver's static type. `Const.cloneTyped()` remains a literal clone; + `Const.namedTyped()` is rejected because a named alias must be driveable. - Added configurable waveform timescales, recording windows, and output overwrite policies (). - Added opt-in `DebugSession` setup for source tracing, netlist and FLC services, and live waveform recording (). - Expanded reusable `rohd_devtools_widgets` components for hierarchy browsing, signal formatting, bit expansion, overlays, help, capture, and cross-probing (). diff --git a/README.md b/README.md index 97543d0419..6c7c57b239 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ You can also open this repository in a GitHub Codespace to run the example in yo - Makes **validation collateral** simpler to develop and debug. The [ROHD Verification Framework](https://github.com/intel/rohd-vf) helps build well-structured testbenches - Powerful capabilities like the [ROHD Schematic Viewer](https://github.com/rohd-schematic-viewer) for high-fidelity rendering of ROHD designs as well as [ROHD Wave Viewer](https://github.com/rohd-wave-viewer) for viewing simulation waveforms even for complex ROHD structures - Debug your design with [ROHD Live Debugger](https://github.com/intel/rohd/rohd_devtools_extension), an integrated debugger with cross-probing into ROHD source and synthesized RTL output from schematics rendering and waveform display all connected to live RTL simulation - - Support for **cosimulation with verilog modules** (via [ROHD Cosim](https://github.com/intel/rohd-cosim)) and **instantiation of verilog modules** in generated SystemVerilog code + - Support for **cosimulation with Verilog modules** (via [ROHD Cosim](https://github.com/intel/rohd-cosim)) and **instantiation of Verilog modules** in generated SystemVerilog code ROHD is *not* a new language, it is *not* a hardware description language (HDL), and it is *not* a version of High-Level Synthesis (HLS). ROHD can be classified as a generator framework, extending a powerful software language into the hardware domain and providing a full suite of tools and capabilities to accelerate hardware design and validation. diff --git a/doc/logic_enum_netlist_metadata.md b/doc/logic_enum_netlist_metadata.md new file mode 100644 index 0000000000..b197c5b4a6 --- /dev/null +++ b/doc/logic_enum_netlist_metadata.md @@ -0,0 +1,179 @@ +# LogicEnum JSON Netlist Metadata + +## Purpose + +This note defines the proposed JSON-netlist work needed after `LogicEnum` from #599 is merged. + +The JSON netlist must always preserve hardware behavior as packed bits. The +proposal adds optional metadata so ROHD-aware tools can also preserve the +source-level enum domain, member names, and explicit encodings. + +## Current behavior and gap + +An enum member can lower to a mapped packed constant, so a generic netlist can +represent its hardware value correctly. For example, a member mapped to +`2'b10` remains two constant bits with that value. + +That is not enough for source-aware consumers. Without additional metadata, +the netlist cannot distinguish: + +```text +2'b10 +``` + +from: + +```text +Mode.execute, where Mode.execute is encoded as 2'b10 +``` + +The SystemVerilog backend can emit enum typedefs and symbolic values, but the +JSON netlist backend currently has no equivalent enum-definition or +enum-reference serialization. + +## Design principles + +1. `LogicEnum` remains a driveable signal, not a `Const` subtype. +2. Enum member literals continue to lower to ordinary packed constant bits. +3. Metadata supplements bit-accurate netlist output; it must not change + ordinary netlist semantics or require generic netlist consumers to + understand enums. +4. Metadata is emitted only for typed ports and retained named nets, not every + anonymous temporary signal. +5. Explicit conversion to packed `Logic` erases enum-domain metadata. +6. Incompatible enum mappings must never acquire equivalence solely from + matching width. + +## Proposed representation + +Declare enum definitions once per generated module in module attributes: + +```json +{ + "attributes": { + "rohd_enum_definitions": { + "Mode": { + "width": 2, + "members": { + "idle": "00", + "execute": "10", + "halt": "11" + } + } + } + } +} +``` + +Reference the definition from typed ports and retained named nets: + +```json +{ + "ports": { + "state": { + "direction": "input", + "bits": [1, 2], + "attributes": { + "rohd_enum": "Mode" + } + } + }, + "netnames": { + "next_state": { + "bits": [3, 4], + "attributes": { + "rohd_enum": "Mode" + } + } + } +} +``` + +The exact attribute spelling may change, but the schema must be stable, +versioned, and documented. Member encodings should use an unambiguous +most-significant-bit-first binary representation with no implied ordinal +meaning. + +## Synthesis integration + +The shared synthesis model should carry an optional enum-domain descriptor +containing: + +- enum definition identity; +- width; +- member-to-encoding mapping; +- preferred/reserved definition-name policy; and +- compatibility identity based on Dart enum type and exact mapping. + +The netlist translator should consume that descriptor when emitting: + +- module-level enum definitions; +- input, output, and inout port attributes; +- retained named internal net attributes; and +- hierarchy boundary metadata where the child and parent domains remain + compatible. + +Constant folding must continue to emit raw bit constants. A named enum signal +driven by a legal enum constant may retain its enum attribute; a standalone +constant has no independent enum domain. + +## Configuration and versioning + +Add a configuration option similar to: + +```dart +const NetlistSynthesizerConfiguration( + preserveEnumMetadata: true, +); +``` + +When disabled, the generated netlist must omit all enum metadata while +preserving exactly the same ports, cells, wire IDs, and packed constant bits. + +Adding this schema requires a `NetlistSynthesizer.formatVersion` update and +documentation describing compatibility expectations for consumers. + +## Validation + +Extend netlist validation to check: + +- each enum reference names a definition in its module; +- referenced width matches the associated port or net width; +- member names are unique and valid identifiers for the metadata schema; +- member encodings have the declared width and are unique; +- disabled metadata emits no enum references; and +- aliases and hierarchy transformations do not leave dangling references. + +## Required tests + +| Scenario | Required assertion | +| --- | --- | +| Enum input and output | Raw bit IDs remain correct and both port attributes reference the expected definition. | +| Named internal enum net | Retained net has the expected enum attribute. | +| Sparse mapping | Metadata preserves non-ordinal member encodings exactly. | +| Enum constant assignment | Netlist retains raw constant bits; the typed destination retains its enum metadata. | +| FSM | State ports/nets retain their mapping, including reset-state encoding. | +| Hierarchy | Compatible enum domains remain annotated across a parent/child boundary. | +| Type mismatch | Incompatible mapping/type connections fail before netlist emission or are explicitly lowered through a packed boundary. | +| Packed conversion | An explicit `Logic` conversion drops enum metadata. | +| Disabled metadata | No enum metadata is emitted and packed netlist behavior is unchanged. | +| Validation | Malformed or dangling enum attributes are rejected. | + +Tests should compare both parsed JSON structure and behaviorally relevant bit +connections. They should not rely only on pretty-printed JSON strings. + +## Non-goals + +- Emitting SystemVerilog typedef syntax in JSON netlists. +- Making a `LogicEnum` signal a constant object. +- Attaching domain metadata to anonymous temporary expressions. +- Treating a whole `LogicStructure`, such as a floating-point value, as an + enum. Structured literals and codecs are a separate abstraction. + +## Acceptance criteria + +The work is complete when a ROHD-aware netlist consumer can recover the enum +domain and exact member encodings for retained enum signals, while a generic +consumer can ignore the metadata and still consume a correct packed-bit +netlist. diff --git a/doc/logic_enum_typed_operations.md b/doc/logic_enum_typed_operations.md new file mode 100644 index 0000000000..2d2cc4836e --- /dev/null +++ b/doc/logic_enum_typed_operations.md @@ -0,0 +1,155 @@ +# LogicEnum Integration with Generic Typed Operations + +## Purpose + +This note records the work required in #704 after `LogicEnum` +from #599 is merged and rebased onto the current codebase. + +The goal is to make `LogicEnum` a first-class scalar domain type for the +existing generic operation APIs. This work must extend `mux`, `flop`, `cases`, +selection, and passthrough rather than introducing parallel typed-operation +APIs. + +## Preconditions + +Before implementing this work: + +1. Rebase #599 onto current `main`. +2. Preserve the existing `LogicEnum` mapping, clone, assignment, case, FSM, + and SystemVerilog contracts. +3. Keep the generic operation APIs introduced by #704 as the only public + operation spellings. + +## Domain and literal contract + +`LogicEnum` is a driveable domain signal, not a `Const` subtype. + +- A `LogicEnum` clone must retain its Dart enum type, mapping, width, + `definitionName`, and definition-name reservation policy while creating a + driveable signal. +- A Dart enum member is a domain literal only in the context of a + `LogicEnum` mapping. It must never silently mean the member ordinal. +- `getsEnum`, `put`, `inject`, and conditional assignment map a member through + the receiving enum's explicit mapping. +- A packed `Const` may drive an enum only when its exact value is represented + in the enum mapping. +- Raw packed `Logic` may cross into an enum domain under the enum's existing + runtime validation policy. It is an explicit bit-level boundary. +- Requesting `` is the explicit domain-erasure path for an operation + result. + +These rules match the existing distinction between a literal source and a +driveable operation output. + +## Operation behavior + +### Mux + +`mux` and `Mux` must infer `LogicEnum` from enum operands. + +- The default result schema is `d0`, including its mapping and configuration. +- An output generator can intentionally select another compatible enum schema. +- Inputs with different Dart enum types or incompatible mappings must be + rejected rather than reinterpreted by width. +- A constant control without an output generator returns the selected source by + identity. +- A supplied output generator is invoked once and materializes a fresh, + driveable result even for a constant control. +- `mux` deliberately drops enum-domain information and operates on + packed bits. + +### Flop + +`flop` and `FlipFlop` must preserve `LogicEnum`. + +- The default result schema is `d`. +- A reset by enum member must use the output mapping. +- A packed reset constant is legal only when its encoding belongs to the + output mapping. +- An unmapped reset value must fail with a mapping-specific diagnostic. +- A reset signal with a compatible enum domain must preserve that domain across + the register boundary. +- `flop` is the explicit packed, driveable normalization path. + +### Cases + +`cases` must distinguish enum selector literals from enum result literals. + +- An enum expression may use members of its mapping as case keys. +- Keys not present in the expression mapping must be rejected. +- A bare enum member cannot be a result value because it has no independent + output mapping. +- Result values must be enum signals, or callers must supply an enum output + generator. +- The default result prototype remains the first typed branch unless an output + generator is supplied. +- Mixed packed and enum result branches require an explicit output schema. + +### Selection and passthrough + +`selectFrom`, `selectIndex`, and `Passthrough` must preserve concrete +`LogicEnum` types and their mappings for homogeneous inputs. Explicit +`Logic` output requests intentionally erase the domain. + +## Required test matrix + +Add real `LogicEnum` coverage to `test/typed_operations_test.dart`; the +existing scalar-domain test double is not sufficient proof of enum behavior. + +| Surface | Required coverage | +| --- | --- | +| Clone and naming | Static type, mapping, width, definition name, reserved definition name, and driveability survive `cloneTyped` and `namedTyped`. | +| Mux | Inferred type, `d0` schema, output generator, constant-control identity, generator materialization, sparse mappings, mapping mismatch rejection, and `` erasure. | +| Flop | Inferred type, enable, synchronous/asynchronous reset, enum-member reset, legal packed reset, rejected unmapped reset, and `` normalization. | +| Cases | Enum expression/member keys, typed result signals, rejected bare result members, default behavior, mixed packed/domain branches, and output generators. | +| Selection | `selectFrom` and `selectIndex` retain enum type and reject incompatible mappings. | +| Passthrough | Default schema preservation and explicit output generator behavior. | +| Arrays | `TypedLogicArray, LogicValue>` works through the relevant operations and preserves its element builder and dimensions. | +| Structures | Structures with enum leaves work through mux, flop, cases, selection, and module hierarchy. | +| Backends | ROHD functional simulation, SystemVerilog with enums enabled and disabled, Icarus, and Verilator agree. | + +Every synthesized scenario should have a representative nested-composition +test, not only a root-module test. + +## FSM coverage + +`FiniteStateMachine` is a primary user of `LogicEnum`. + +Add or retain tests for: + +- reset to an enum state; +- legal transitions and default transitions; +- sparse and non-ordinal mappings; +- direct enum-member case labels; +- typed current and next state access; +- generated SystemVerilog enum typedefs and symbolic state members; and +- functional and external-simulator agreement. + +Constraining FSM state identifiers to `Enum` is a breaking API change and +needs a release migration note. + +## Structures and structured literals + +`LogicEnum` should support enum leaves in a `LogicStructure`, but it should +not become a general structured-value abstraction. + +A type such as `FloatingPoint` should remain a `LogicStructure` with a +separate literal/codec API, for example `FloatingPoint.one()` or +`FloatingPoint.fromDouble(1.0)`. Such a literal may use constant leaves, but +its `clone()` must construct driveable leaves so generic operations can create +a driveable output while retaining the `FloatingPoint` static type. + +This follows the existing structured-constant operation contract and keeps +finite scalar enum domains distinct from arbitrary structured encodings. + +## Acceptance criteria + +The work is complete when: + +1. No parallel typed operation API is introduced. +2. Enum type and mapping information is retained by every applicable generic + operation unless the caller explicitly requests `Logic`. +3. Invalid mappings and unmapped literals fail explicitly. +4. The test matrix above passes in ROHD simulation and emitted-SystemVerilog + simulation. +5. The migration and domain-erasure rules are documented for users. diff --git a/doc/user_guide/_docs/A19-logic-structures.md b/doc/user_guide/_docs/A19-logic-structures.md index aa2d53971a..3f87a047a9 100644 --- a/doc/user_guide/_docs/A19-logic-structures.md +++ b/doc/user_guide/_docs/A19-logic-structures.md @@ -1,7 +1,7 @@ --- title: "Logic Structures" permalink: /docs/logic-structures/ -last_modified_at: 2025-7-23 +last_modified_at: 2026-9-4 toc: true --- @@ -17,6 +17,100 @@ Ports with matching types to the original `LogicStructure` can be created using `LogicArray`s are a type of `LogicStructure` and thus inherit these behavioral traits. +## Type-preserving operations + +`Mux`, `FlipFlop`, and `Passthrough` preserve a concrete `Logic` subtype when +their operands establish a compatible output representation: + +```dart +final selected = Mux(select, packet1, packet0).out; +final registered = FlipFlop(clk, selected, reset: reset).q; +final forwarded = Passthrough(registered).out; +final observedValid = forwarded.valid; + +// All three values have type Packet. +assert(observedValid.width == 1); +``` + +The same behavior applies to `LogicArray` and `TypedLogicArray`, including +nested typed arrays. A mux uses its `d0` operand as the default output +prototype. Both operands must have matching concrete types and recursive +geometry: field widths, array dimensions, packing hints, and leaf structure. +Pass an `outputGenerator` to `Mux`, `FlipFlop`, `mux`, `flop`, or `cases` to +select a different result representation. `Passthrough` always preserves its +input representation; create and connect an explicit destination when another +representation is needed. + +These operations can consume a structure containing `Const` leaves when its +`clone()` implementation returns the same concrete structure type with +driveable `Logic` leaves. The operation preserves the structure type while +normalizing its input port and output to driveable logic. This supports +domain-specific constant structures, such as a floating-point structure +assembled from constant sign, exponent, and mantissa fields. Plain `Const` and +`LogicNet` sources cannot infer a driveable result type, so normalize them +explicitly: + +```dart +final Logic registered = flop(clk, Const(0x5a, width: 8)); +``` + +For a valid constant mux control without an `outputGenerator`, `mux` returns +the selected source directly. Supplying an `outputGenerator` always constructs +a fresh driveable output, including for a constant control. + +### Case selection + +`Case` can assign structures directly because its branches contain ordinary +conditional assignments. The destination determines the result type: + +```dart +final selected = packet0.cloneTyped(name: 'selected'); + +Combinational([ + Case(selector, [ + CaseItem(Const(0, width: selector.width), [selected < packet0]), + CaseItem(Const(1, width: selector.width), [selected < packet1]), + ]), +]); +``` + +Direct structure assignments require matching total widths and map bits in +packed leaf order. They do not require the source and destination to have the +same concrete structure type. + +Use generic `cases` when the operation should construct and return a value +while preserving its concrete type: + +```dart +final selected = cases( + selector, + {0: packet0, 1: packet1}, + defaultValue: fallback, + outputGenerator: packet0.clone, +); +``` + +The output generator is also how a structured result can accept deliberately +packed branch values. A packed source is interpreted using the generated +output's representation. Different structured domain types should be converted +to `packed` explicitly before that bit reinterpretation. + +Use `selectIndex` and `selectFrom` for structure-preserving indexed selection. +`Pipeline.get`, `PipelineStageInfo.get`, and `PipelineStageInfo.getAbs` +preserve the concrete type of each supplied identifier. A single `Pipeline` +can therefore carry structures alongside unrelated arrays, enums, and scalar +signals. + +### Migrating type-preserving operations + +The former `StructureMux`, `StructureFlipFlop`, `StructurePassthrough`, +`typedCases`, `typedMux`, `typedFlop`, `selectIndexTyped`, and +`selectFromTyped` entry points are replaced by the corresponding established +generic APIs. Use `cloneTyped()` and `namedTyped()` when the receiver's static +type should be retained without a cast. `Const.cloneTyped()` remains a literal +clone, while `Const.namedTyped()` is rejected because a named alias is +driveable; use `Const.named()` when an ordinary `Logic` alias is intended. + ## Using `LogicStructure` to group signals The simplest way to use a `LogicStructure` is to just use its constructor, which requires a collection of `Logic`s. diff --git a/doc/user_guide/_docs/A20-logic-arrays.md b/doc/user_guide/_docs/A20-logic-arrays.md index 889db3b827..b1636a8ce2 100644 --- a/doc/user_guide/_docs/A20-logic-arrays.md +++ b/doc/user_guide/_docs/A20-logic-arrays.md @@ -27,7 +27,44 @@ LogicArray([5, 5, 5], 128); As long as the total width of a `LogicArray` and another type of `Logic` (including `Logic`, `LogicStructure`, and another `LogicArray`) are the same, assignments and bitwise operations will work in per-element order. This means you can assign two `LogicArray`s of different dimensions to each other as long as the total width matches. -## Typed arrays +## Assigning arrays + +Use `<=` for continuous assignments outside conditional blocks. You can assign +an entire array or an individual element: + +```dart +final source = LogicArray([4], 8, name: 'source'); +final copied = LogicArray([4], 8, name: 'copied'); +final updated = LogicArray([4], 8, name: 'updated'); +final replacement = Logic(name: 'replacement', width: 8); + +copied <= source; +updated.elements[0] <= replacement; +``` + +Use `<` for assignments inside `Combinational`, just as with ordinary +`Logic` signals. Whole-array conditional assignments preserve the array's +element ordering: + +```dart +final select = Logic(name: 'select'); +final sourceA = LogicArray([4], 8, name: 'sourceA'); +final sourceB = LogicArray([4], 8, name: 'sourceB'); +final selected = LogicArray([4], 8, name: 'selected'); + +Combinational([ + If( + select, + then: [selected < sourceA], + orElse: [selected < sourceB], + ), +]); +``` + +The `elements` list follows array index order, so `array.elements[0]` +corresponds to `array[0]` in generated SystemVerilog. + +## Typed and value-domain arrays Use `TypedLogicArray` when every array position has the same specialized hardware type and associated semantic value type. `LogicArray` is @@ -85,6 +122,9 @@ representations. `valueCodec` is optional only when `TValue` is `LogicValue`. A custom codec should decode every four-state value that can appear in its hardware. +Type-preserving operations require compatible arrays to use the identical +`valueCodec` instance; reuse a shared codec instance or pass `packed` +explicitly when intentionally reinterpreting the bits with a different codec. `dimensionNames` controls child naming during construction and cloning; it is not public axis metadata. See the [`TypedLogicArray` API documentation](https://intel.github.io/rohd/rohd/TypedLogicArray-class.html) @@ -189,6 +229,25 @@ Array ports in generated SystemVerilog will match dimensions (including unpacked Use the existing `addTypedInput`, `addTypedOutput`, and `addTypedInOut` methods for `TypedLogicArray` ports. Their generic `LogicType` preserves the complete array subtype, including its hardware element type, semantic value type, codec, net kind, dimensions, and unpacked-dimension configuration. This allows the module to access fields such as `samples.at([1, 2]).data` directly. The established `addInputArray`, `addOutputArray`, and `addInOutArray` APIs remain the concrete `LogicArray` helpers. +## Type-preserving operations + +Use generic `Mux`, `FlipFlop`, and `Passthrough` when the output must retain +the array's concrete type, dimensions, and specialized leaf type: + +```dart +final selected = Mux(select, samplesA, samplesB).out; +final delayed = FlipFlop(clk, selected, reset: reset).q; +final forwarded = Passthrough(delayed).out; + +final bottomRightData = forwarded.at([1, 2]).data; +``` + +The mux inputs must have matching concrete array types and geometry, including +dimensions, leaf widths, packed/unpacked configuration, and leaf structure. +Use generic `cases`, `selectIndex`, or `selectFrom` when selecting one complete +typed array from multiple choices. Specify the array type parameter on +`Pipeline` when its stages access the array with `p.get(array)`. + ## Elements of arrays Use `elements` to inspect immediate children, `arrayElements` or `indexedElements` to traverse declared array positions, and `leafElements` only when fully recursive traversal is intended. The normal `[n]` operator selects the `n`th packed bit for both `LogicArray` and `Logic`; use `at` for multidimensional typed element indexing. diff --git a/doc/user_guide/_docs/A21-generation.md b/doc/user_guide/_docs/A21-generation.md index 92d1568d06..e54ebb2b37 100644 --- a/doc/user_guide/_docs/A21-generation.md +++ b/doc/user_guide/_docs/A21-generation.md @@ -205,6 +205,14 @@ The `Naming.unpreferredName` function will modify a signal name to indicate to d ## Services API +### Migrating legacy generation APIs + +`generateSynth` and `WaveDumper` remain available as deprecated compatibility +APIs because their replacements were introduced in `0.6.11`. Prefer +`dumpSystemVerilog`, `SystemVerilogService`, `dumpWaves`, and +`WaveformService` for new code. Their deprecation does not imply removal in +this `0.7.0` release. + `ModuleService` is the shared API for module-scoped generation, capture, and inspection. Services can register with `ModuleServices` for lookup by DevTools and other consumers. `ArtifactProducingService` implementations expose named @@ -285,7 +293,12 @@ With `multiFile: true`, `SystemVerilogService` writes one `.sv` file per generated module definition. For custom synthesis flows, [`SynthBuilder`](https://intel.github.io/rohd/rohd/SynthBuilder-class.html) accepts a `Module` and a `Synthesizer` (usually a -`SystemVerilogSynthesizer`). +`SystemVerilogSynthesizer`). It provides `SynthFileContents` objects through +`getSynthFileContents`; each includes its contents, file name, and context +about the `module` it refers to and its `instanceTypeName`. With these APIs, +you can generate named files, add file headers, ignore generation of some +modules, and generate file lists for other tools. `SynthBuilder.multi` makes +it convenient to generate outputs for multiple independent hierarchies. ## Capturing waveforms diff --git a/lib/src/diagnostics/inspector_service.dart b/lib/src/diagnostics/inspector_service.dart index 53ea49d553..f34926be93 100644 --- a/lib/src/diagnostics/inspector_service.dart +++ b/lib/src/diagnostics/inspector_service.dart @@ -118,7 +118,7 @@ class ModuleTree { json.encode({'status': 'fail', 'reason': 'module not yet build'}); } - /// Returns the module hierarchy as a JSON string. + /// Returns the built module hierarchy as JSON. @Deprecated('Use hierarchyJson instead.') String get hierarchyJSON => hierarchyJson; } diff --git a/lib/src/interfaces/pair_interface.dart b/lib/src/interfaces/pair_interface.dart index 2bbf8cada6..08569737f6 100644 --- a/lib/src/interfaces/pair_interface.dart +++ b/lib/src/interfaces/pair_interface.dart @@ -196,7 +196,7 @@ class PairInterface extends Interface { subInterfaceEntry.value.uniquify ?? (original) => original; String newSubIntfUniquify(String original) => - nonNullUniquify(nonNullSubIntfUniquify(nonNullModify(original))); + nonNullUniquify(nonNullSubIntfUniquify(original)); if (!srcInterface._subInterfaces.containsKey(subInterfaceName)) { throw InterfaceTypeException( diff --git a/lib/src/modules/bus.dart b/lib/src/modules/bus.dart index 7fbaee7bd9..533ce5f4e3 100644 --- a/lib/src/modules/bus.dart +++ b/lib/src/modules/bus.dart @@ -272,6 +272,20 @@ class Swizzle extends Module with InlineSystemVerilog { @override String get resultSignalName => _out; + @override + String instantiationVerilog( + String instanceType, + String instanceName, + Map ports, + ) { + final result = ports[resultSignalName]; + final inputPorts = Map.of(ports)..remove(resultSignalName); + final packedArrayAlias = _packedArrayAlias(inputPorts); + return packedArrayAlias == null + ? super.instantiationVerilog(instanceType, instanceName, ports) + : 'assign $result = $packedArrayAlias;'; + } + @override String inlineVerilog(Map inputs) { assert( @@ -281,6 +295,11 @@ class Swizzle extends Module with InlineSystemVerilog { ' but saw $inputs with ${inputs.length} values.', ); + final packedArrayAlias = _packedArrayAlias(inputs); + if (packedArrayAlias != null) { + return packedArrayAlias; + } + // Calculate all width descriptions upfront to determine alignment final validInputs = _swizzleInputs.reversed.where((e) => e.width > 0).toList(); @@ -351,6 +370,41 @@ ${inputLines.join('\n')} }'''; } + /// Returns the source array for a natural-order packing of a packed 1D array. + String? _packedArrayAlias(Map inputs) { + if (isNet || _swizzleInputs.isEmpty) { + return null; + } + + final firstSource = _swizzleInputs.first.srcConnection; + final array = firstSource?.parentStructure; + if (array is! BaseLogicArray || + array.dimensions.length != 1 || + array.numUnpackedDimensions != 0 || + array.elements.length != _swizzleInputs.length) { + return null; + } + + String? arrayExpression; + for (var index = 0; index < _swizzleInputs.length; index++) { + final input = _swizzleInputs[index]; + final source = input.srcConnection; + if (source?.parentStructure != array || source?.arrayIndex != index) { + return null; + } + + final match = _singleBitSelectRegex.firstMatch(inputs[input.name]!); + if (match == null || + int.parse(match.group(2)!) != index || + (arrayExpression != null && match.group(1) != arrayExpression)) { + return null; + } + arrayExpression ??= match.group(1); + } + + return arrayExpression; + } + /// Rewrites runs of adjacent descending single-bit selects from the same /// packed signal into wider SystemVerilog slices. /// diff --git a/lib/src/modules/conditionals/case.dart b/lib/src/modules/conditionals/case.dart index b50ef2f62c..3d7df8d335 100644 --- a/lib/src/modules/conditionals/case.dart +++ b/lib/src/modules/conditionals/case.dart @@ -11,6 +11,7 @@ import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/modules/conditionals/ssa.dart'; +import 'package:rohd/src/modules/operation_utils.dart'; /// Represents a single case within a [Case] block. class CaseItem { @@ -32,20 +33,28 @@ class CaseItem { /// It is used to assign a signal based on a condition with multiple cases to /// consider. For e.g., this can be used instead of a nested [mux]. /// -/// The result is of type [Logic] and it is determined by conditionaly matching -/// the expression with the values of each item in conditions. If width of the -/// input is not provided, then the width of the result is inferred from the -/// width of the entries. Bare [Enum] values are not supported as results -/// because an untyped result has no hardware encoding for them; use explicitly -/// mapped [LogicEnum] signals instead. -Logic cases(Logic expression, Map conditions, - {int? width, - ConditionalType conditionalType = ConditionalType.none, - dynamic defaultValue}) { +/// The result is of type [LogicType] and is determined by conditionally +/// matching [expression] with values in [conditions]. +/// +/// When [outputGenerator] is omitted, a requested [Logic] result is +/// normalized to ordinary [Logic]. For a more specific [LogicType], the first +/// matching typed branch supplies the output prototype. Supply +/// [outputGenerator] to establish an explicit result representation when +/// sources are mixed or no typed branch is available. +LogicType cases( + Logic expression, + Map conditions, { + int? width, + ConditionalType conditionalType = ConditionalType.none, + dynamic defaultValue, + LogicType Function({String? name})? outputGenerator, + String name = 'result', +}) { final resultValues = [ ...conditions.values, - if (defaultValue != null) defaultValue + if (defaultValue != null) defaultValue, ]; + if (resultValues.any((value) => value is Enum)) { throw ArgumentError.value( resultValues, @@ -66,13 +75,17 @@ Logic cases(Logic expression, Map conditions, if (width != inferredWidth && width != null && inferredWidth != null) { throw SignalWidthMismatchException.forDynamic( - conditionValue, width, inferredWidth); + conditionValue, + width, + inferredWidth, + ); } width ??= inferredWidth; } - if (width == null) { + final prototype = resultValues.whereType().firstOrNull; + if (width == null && prototype == null && outputGenerator == null) { throw SignalWidthMismatchException.forNull(conditions); } @@ -94,33 +107,67 @@ Logic cases(Logic expression, Map conditions, } } + final result = createOperationOutput( + width: width ?? prototype?.width, + name: name, + operation: 'cases<$LogicType>', + naming: Naming.mergeable, + prototype: prototype, + outputGenerator: outputGenerator, + ); + for (final condition in conditions.entries) { if (condition.key is Logic) { if (expression.width != (condition.key as Logic).width) { throw SignalWidthMismatchException.forDynamic( - condition.key, expression.width, (condition.key as Logic).width); + condition.key, + expression.width, + (condition.key as Logic).width, + ); } } if (condition.key is LogicValue) { if (expression.width != (condition.key as LogicValue).width) { - throw SignalWidthMismatchException.forDynamic(condition.key, - expression.width, (condition.key as LogicValue).width); + throw SignalWidthMismatchException.forDynamic( + condition.key, + expression.width, + (condition.key as LogicValue).width, + ); } } } - final result = Logic(name: 'result', width: width, naming: Naming.mergeable); + for (final conditionValue in resultValues) { + if (conditionValue is Logic) { + validateOperationSource( + conditionValue, + result, + operation: 'cases branch', + ); + } else if (conditionValue is LogicValue && + conditionValue.width != result.width) { + throw SignalWidthMismatchException.forDynamic( + conditionValue, + result.width, + conditionValue.width, + ); + } + } Combinational([ Case( - expression, - [ - for (final condition in conditions.entries) - CaseItem(conditionLogic(condition.key), [result < condition.value]) - ], - conditionalType: conditionalType, - defaultItem: defaultValue != null ? [result < defaultValue] : null) + expression, + [ + for (final condition in conditions.entries) + CaseItem( + conditionLogic(condition.key), + [result < condition.value], + ), + ], + conditionalType: conditionalType, + defaultItem: defaultValue != null ? [result < defaultValue] : null, + ), ]); return result; @@ -174,10 +221,12 @@ class Case extends Conditional { /// Whenever an item in [items] matches [expression], it will be executed. /// /// If none of [items] match, then [defaultItem] is executed. - Case(this.expression, this.items, - {List? defaultItem, - this.conditionalType = ConditionalType.none}) - : _defaultItem = defaultItem { + Case( + this.expression, + this.items, { + List? defaultItem, + this.conditionalType = ConditionalType.none, + }) : _defaultItem = defaultItem { for (final item in items) { if (item.value.width != expression.width) { throw PortWidthMismatchException.equalWidth(expression, item.value); @@ -247,13 +296,14 @@ class Case extends Conditional { } @override - late final List conditionals = - UnmodifiableListView(_getConditionals()); + late final List conditionals = UnmodifiableListView( + _getConditionals(), + ); /// Calculates the set of conditionals directly within this. List _getConditionals() => [ ...items.map((item) => item.then).expand((conditional) => conditional), - if (defaultItem != null) ...defaultItem! + if (defaultItem != null) ...defaultItem!, ]; @override @@ -265,16 +315,20 @@ class Case extends Conditional { for (final item in items) { drivers ..add(item.value) - ..addAll(item.then - .map((conditional) => conditional.drivers) - .expand((driver) => driver) - .toList(growable: false)); + ..addAll( + item.then + .map((conditional) => conditional.drivers) + .expand((driver) => driver) + .toList(growable: false), + ); } if (defaultItem != null) { - drivers.addAll(defaultItem! - .map((conditional) => conditional.drivers) - .expand((driver) => driver) - .toList(growable: false)); + drivers.addAll( + defaultItem! + .map((conditional) => conditional.drivers) + .expand((driver) => driver) + .toList(growable: false), + ); } return drivers; } @@ -286,23 +340,31 @@ class Case extends Conditional { List calculateReceivers() { final receivers = []; for (final item in items) { - receivers.addAll(item.then - .map((conditional) => conditional.receivers) - .flattened - .toList(growable: false)); + receivers.addAll( + item.then + .map((conditional) => conditional.receivers) + .flattened + .toList(growable: false), + ); } if (defaultItem != null) { - receivers.addAll(defaultItem! - .map((conditional) => conditional.receivers) - .flattened - .toList(growable: false)); + receivers.addAll( + defaultItem! + .map((conditional) => conditional.receivers) + .flattened + .toList(growable: false), + ); } return receivers; } @override - String verilogContents(int indent, Map inputsNameMap, - Map outputsNameMap, String assignOperator) { + String verilogContents( + int indent, + Map inputsNameMap, + Map outputsNameMap, + String assignOperator, + ) { final padding = Conditional.calcPadding(indent); final expressionName = inputsNameMap[driverInput(expression).name]; var caseHeader = caseType; @@ -316,8 +378,14 @@ class Case extends Conditional { for (final item in items) { final conditionName = inputsNameMap[driverInput(item.value).name]; final caseContents = item.then - .map((conditional) => conditional.verilogContents( - indent + 4, inputsNameMap, outputsNameMap, assignOperator)) + .map( + (conditional) => conditional.verilogContents( + indent + 4, + inputsNameMap, + outputsNameMap, + assignOperator, + ), + ) .join('\n'); verilog.write(''' $subPadding$conditionName : begin @@ -327,8 +395,14 @@ ${subPadding}end } if (defaultItem != null) { final defaultCaseContents = defaultItem! - .map((conditional) => conditional.verilogContents( - indent + 4, inputsNameMap, outputsNameMap, assignOperator)) + .map( + (conditional) => conditional.verilogContents( + indent + 4, + inputsNameMap, + outputsNameMap, + assignOperator, + ), + ) .join('\n'); verilog.write(''' ${subPadding}default : begin @@ -342,25 +416,30 @@ ${subPadding}end } @override - Map processSsa(Map currentMappings, - {required int context}) { + Map processSsa( + Map currentMappings, { + required int context, + }) { // add an empty default if there isn't already one, since we need it for phi _defaultItem ??= []; // first connect direct drivers into the case statement - Conditional.connectSsaDriverFromMappings(expression, currentMappings, - context: context); + Conditional.connectSsaDriverFromMappings( + expression, + currentMappings, + context: context, + ); for (final itemDriver in items.map((e) => e.value)) { - Conditional.connectSsaDriverFromMappings(itemDriver, currentMappings, - context: context); + Conditional.connectSsaDriverFromMappings( + itemDriver, + currentMappings, + context: context, + ); } // calculate mappings locally within each item final phiMappings = {}; - for (final conditionals in [ - ...items.map((e) => e.then), - defaultItem!, - ]) { + for (final conditionals in [...items.map((e) => e.then), defaultItem!]) { var localMappings = {...currentMappings}; for (final conditional in conditionals) { @@ -369,10 +448,10 @@ ${subPadding}end for (final localMapping in localMappings.entries) { if (!phiMappings.containsKey(localMapping.key)) { - phiMappings[localMapping.key] = Logic( + phiMappings[localMapping.key] = SsaLogic.cloneLike( + localMapping.key, + localMapping.value, name: '${localMapping.key.name}_phi', - width: localMapping.key.width, - naming: Naming.mergeable, ); } @@ -387,25 +466,24 @@ ${subPadding}end // an inferred latch final signalsNeedingInit = [ ...items.map((e) => e.then).flattened, - ...defaultItem!, + ...defaultItem! ] .map((e) => e.calculateReceivers()) .flattened - .whereType() + .where((signal) => SsaLogic.isSsaProxy(signal, context: context)) .toSet(); - for (final conditionals in [ - ...items.map((e) => e.then), - defaultItem!, - ]) { + for (final conditionals in [...items.map((e) => e.then), defaultItem!]) { final alreadyDrivenSsaSignals = conditionals .map((e) => e.calculateReceivers()) .flattened - .whereType() + .where((signal) => SsaLogic.isSsaProxy(signal, context: context)) .toSet(); for (final signalNeedingInit in signalsNeedingInit) { if (!alreadyDrivenSsaSignals.contains(signalNeedingInit)) { - conditionals.add(signalNeedingInit < 0); + conditionals.add( + signalNeedingInit < SsaLogic.branchDefaultValue(signalNeedingInit), + ); } } } @@ -447,8 +525,12 @@ class CaseZ extends Case { /// the definition of matches allows for `z` to be a wildcard. /// /// If none of [items] match, then [defaultItem] is executed. - CaseZ(super.expression, super.items, - {super.defaultItem, super.conditionalType}); + CaseZ( + super.expression, + super.items, { + super.defaultItem, + super.conditionalType, + }); @override String get caseType => 'casez'; diff --git a/lib/src/modules/conditionals/combinational.dart b/lib/src/modules/conditionals/combinational.dart index bc3abff54b..4b6f71b2a3 100644 --- a/lib/src/modules/conditionals/combinational.dart +++ b/lib/src/modules/conditionals/combinational.dart @@ -87,16 +87,39 @@ class Combinational extends Always { /// mitigated by constructing the [Combinational.ssa] before connecting /// inputs to the rest of the design, but usually the impact is so small /// that it will not be noticeable. + /// factory Combinational.ssa( - List Function(Logic Function(Logic signal) s) construct, - {String name = 'combinational_ssa'}) { + List Function(Logic Function(Logic signal) s) construct, + {String name = 'combinational_ssa'}) => + Combinational._constructSsa(construct, name: name); + + /// Creates SSA combinational logic with type-preserving proxies. + /// + /// This supports internal users, such as [Pipeline], that need an SSA + /// callback to retain concrete signal types and configuration. + @internal + factory Combinational.ssaWithTypes( + List Function(Logic Function(Logic signal) s) construct, { + String name = 'combinational_ssa', + }) => + Combinational._constructSsa(construct, name: name, preserveTypes: true); + + factory Combinational._constructSsa( + List Function(Logic Function(Logic signal) s) construct, { + required String name, + bool preserveTypes = false, + }) { final context = _ssaContextCounter++; - final ssas = []; + final ssas = {}; Logic getSsa(Logic ref) { - final newSsa = SsaLogic(ref, context); - ssas.add(newSsa); + final newSsa = SsaLogic.create( + ref, + context, + preserveTypes: preserveTypes, + ); + ssas.addAll(SsaLogic.proxyLeaves(newSsa, context: context)); return newSsa; } @@ -112,22 +135,22 @@ class Combinational extends Always { return Combinational(conditionals, name: name); } - /// A map from [SsaLogic]s to signals that they drive. + /// A map from SSA proxies to signals that they drive. /// /// This only stores information temporarily during construction of a /// [Combinational.ssa] and clears afterwards. @internal - static final Map> signalToSsaDrivers = {}; + static final Map> signalToSsaDrivers = {}; /// Tags each downstream [Logic] from [ssaDriver] as such in /// [signalToSsaDrivers]. - static void _updateSsaDriverMap(SsaLogic ssaDriver) { + static void _updateSsaDriverMap(Logic ssaDriver) { final toParse = TraverseableCollection() ..addAll(ssaDriver.dstConnections); for (var i = 0; i < toParse.length; i++) { final tpi = toParse[i]; - signalToSsaDrivers.putIfAbsent(tpi, () => {}).add(ssaDriver); + signalToSsaDrivers.putIfAbsent(tpi, () => {}).add(ssaDriver); if (tpi.isInput && tpi.parentModule! is SystemVerilog) { toParse.addAll(tpi.parentModule!.outputs.values); @@ -145,7 +168,7 @@ class Combinational extends Always { // reference an element of a structure without separately SSA'ing it. // However, leaving this in here just in case (probably negligible perf // impact). - if (tpi is LogicStructure) { + if (tpi is LogicStructure && !SsaLogic.containsTypedProxy(tpi)) { toParse.addAll(tpi.elements); } } diff --git a/lib/src/modules/conditionals/conditional.dart b/lib/src/modules/conditionals/conditional.dart index 091c368442..c478a475ca 100644 --- a/lib/src/modules/conditionals/conditional.dart +++ b/lib/src/modules/conditionals/conditional.dart @@ -216,39 +216,45 @@ abstract class Conditional { // take all the "current" names for these signals for (final ssaDriver in ssaDrivers) { - if (!mappings.containsKey(ssaDriver.ref)) { - throw UninitializedSignalException(ssaDriver.ref.name); + final reference = SsaLogic.referenceOf(ssaDriver, context: context); + if (!mappings.containsKey(reference)) { + throw UninitializedSignalException(reference.name); } // if these are already connected, just skip it, we're fine already if (ssaDriver.srcConnection != null && - ssaDriver.srcConnection == mappings[ssaDriver.ref]!) { + ssaDriver.srcConnection == mappings[reference]!) { continue; } // if these are the same signal, also just skip it - if (ssaDriver == mappings[ssaDriver.ref]!) { + if (ssaDriver == mappings[reference]!) { continue; } // if at this point there's still a srcConnection, then something is wrong // and someone probably reused a signal when they shouldn't have if (ssaDriver.srcConnection != null) { - throw MappedSignalAlreadyAssignedException(ssaDriver.ref.name); + throw MappedSignalAlreadyAssignedException(reference.name); } - ssaDriver <= mappings[ssaDriver.ref]!; + ssaDriver <= mappings[reference]!; } } /// Searches for SSA nodes from a source [driver] which match the [context]. - static List _findSsaDriversFrom(Logic driver, int context) { - if (driver is SsaLogic && driver.context == context) { + static List _findSsaDriversFrom(Logic driver, int context) { + if (SsaLogic.isSsaProxy(driver, context: context)) { return [driver]; } // no need to check for context on this map since it clears each time - return Combinational.signalToSsaDrivers[driver]?.toList() ?? const []; + return Combinational.signalToSsaDrivers[driver] + ?.where( + (signal) => SsaLogic.isSsaProxy(signal, context: context), + ) + .toList() ?? + const []; } /// Given existing [currentMappings], connects [drivers] and [receivers] diff --git a/lib/src/modules/conditionals/conditional_assign.dart b/lib/src/modules/conditionals/conditional_assign.dart index 6e968fd367..424f0ef226 100644 --- a/lib/src/modules/conditionals/conditional_assign.dart +++ b/lib/src/modules/conditionals/conditional_assign.dart @@ -113,9 +113,9 @@ class ConditionalAssign extends Conditional { context: context); final newMappings = {...currentMappings}; - // if the receiver is an ssa node, then update the mapping - if (receiver is SsaLogic) { - newMappings[(receiver as SsaLogic).ref] = receiver; + // If the receiver is an SSA node, update the mapping. + if (SsaLogic.isSsaProxy(receiver, context: context)) { + newMappings[SsaLogic.referenceOf(receiver, context: context)] = receiver; } return newMappings; diff --git a/lib/src/modules/conditionals/conditional_group.dart b/lib/src/modules/conditionals/conditional_group.dart index a8745eac61..15a1aa3b37 100644 --- a/lib/src/modules/conditionals/conditional_group.dart +++ b/lib/src/modules/conditionals/conditional_group.dart @@ -61,3 +61,25 @@ class ConditionalGroup extends Conditional { )) .join('\n'); } + +/// A [ConditionalGroup] originating from one complete [LogicStructure] +/// assignment. +/// +/// The contained leaf assignments preserve the normal ROHD connection graph, +/// while [receiver] and [driver] retain the aggregate operation for backends +/// that can emit it directly. +@internal +class StructureConditionalGroup extends ConditionalGroup { + /// The complete structure assigned by this group. + final LogicStructure receiver; + + /// The source structure or packed value assigned to [receiver]. + final Logic driver; + + /// Creates leaf assignments for one aggregate structure assignment. + StructureConditionalGroup( + this.receiver, + this.driver, + super.conditionals, + ); +} diff --git a/lib/src/modules/conditionals/conditionals.dart b/lib/src/modules/conditionals/conditionals.dart index 69e4c2665b..2e9f74a3f1 100644 --- a/lib/src/modules/conditionals/conditionals.dart +++ b/lib/src/modules/conditionals/conditionals.dart @@ -5,7 +5,7 @@ export 'case.dart'; export 'combinational.dart'; export 'conditional.dart'; export 'conditional_assign.dart'; -export 'conditional_group.dart'; +export 'conditional_group.dart' hide StructureConditionalGroup; export 'flop.dart'; export 'if.dart'; export 'sequential.dart'; diff --git a/lib/src/modules/conditionals/flop.dart b/lib/src/modules/conditionals/flop.dart index 1c2a310320..76639b1e31 100644 --- a/lib/src/modules/conditionals/flop.dart +++ b/lib/src/modules/conditionals/flop.dart @@ -8,125 +8,262 @@ // Author: Max Korbel import 'package:rohd/rohd.dart'; +import 'package:rohd/src/modules/operation_utils.dart'; +import 'package:rohd/src/signals/logic_structure_signature.dart'; -/// Constructs a positive edge triggered flip flop on [clk]. +/// Constructs a positive-edge-triggered flip-flop preserving [LogicType]. /// -/// It returns [FlipFlop.q]. -/// -/// When the optional [en] is provided, an additional input will be created for -/// flop. If optional [en] is high or not provided, output will vary as per -/// input[d]. For low [en], output remains frozen irrespective of input [d]. -/// -/// When the optional [reset] is provided, the flop will be reset (active-high). -/// If no [resetValue] is provided, the reset value is always `0`. Otherwise, -/// it will reset to the provided [resetValue]. -/// -/// If [asyncReset] is true, the [reset] signal (if provided) will be treated -/// as an async reset. If [asyncReset] is false, the reset signal will be -/// treated as synchronous. -Logic flop( +/// The input [d] supplies the output representation when [outputGenerator] is +/// omitted. Request `` explicitly to normalize a plain [Const] or +/// [LogicNet] source into a driveable scalar result. +LogicType flop( Logic clk, - Logic d, { + LogicType d, { Logic? en, Logic? reset, dynamic resetValue, bool asyncReset = false, + LogicType Function({String? name})? outputGenerator, }) => - FlipFlop( + FlipFlop( clk, d, en: en, reset: reset, resetValue: resetValue, asyncReset: asyncReset, + outputGenerator: outputGenerator, ).q; -/// Represents a single flip-flop with no reset. -class FlipFlop extends Module with SystemVerilog { - /// Name for the enable input of this flop - final String _enName = Naming.unpreferredName('en'); +/// A positive-edge-triggered flip-flop preserving `LogicType`. +/// +/// The returned `q` is created from `d`'s clone by default, or from +/// `outputGenerator` when supplied. Dynamic operation outputs must be +/// driveable, so a concrete [Const] or [LogicNet] result type is rejected. +class FlipFlop extends Module { + /// Registered output. + LogicType get q => _scalarQ; - /// Name for the clk of this flop. - final String _clkName = Naming.unpreferredName('clk'); + late final LogicType _scalarQ; - /// Name for the input of this flop. - final String _dName = Naming.unpreferredName('d'); + /// Alias for [q] shared with other single-output operations. + LogicType get out => q; - /// Name for the output of this flop. - final String _qName = Naming.unpreferredName('q'); + /// Indicates whether reset is asynchronous. + bool get asyncReset => _scalarAsyncReset; - /// Name for the reset of this flop. - final String _resetName = Naming.unpreferredName('reset'); + late final bool _scalarAsyncReset; - /// Name for the reset value of this flop. - final String _resetValueName = Naming.unpreferredName('resetValue'); + /// Constant packed reset value, when one is used. + LogicValue? get constantResetValue => _scalarConstantResetValue; - /// The clock, posedge triggered. - late final Logic _clk = input(_clkName); + late final LogicValue? _scalarConstantResetValue; + + /// Constructs a flip-flop preserving [LogicType]. + factory FlipFlop( + Logic clk, + LogicType d, { + Logic? en, + Logic? reset, + dynamic resetValue, + bool asyncReset = false, + LogicType Function({String? name})? outputGenerator, + String name = 'flipflop', + }) { + if (clk.width != 1) { + throw PortWidthMismatchException(clk, 1); + } + if (en != null && en.width != 1) { + throw PortWidthMismatchException(en, 1); + } + if (reset != null && reset.width != 1) { + throw PortWidthMismatchException(reset, 1); + } + + final outputSchema = createOperationOutput( + width: d.width, + name: 'q', + operation: 'FlipFlop<$LogicType>', + prototype: d, + outputGenerator: outputGenerator, + ); + validateOperationSource(d, outputSchema, operation: 'FlipFlop d'); + + if (resetValue is Logic && resetValue.width != outputSchema.width) { + throw PortWidthMismatchException.equalWidth(resetValue, outputSchema); + } + if (resetValue is LogicStructure && outputSchema is LogicStructure) { + validateMatchingOperationStructure( + resetValue, + outputSchema, + operation: 'FlipFlop reset', + ); + } - /// Optional enable input to the flop. + if (LogicType == Logic || d is! LogicStructure) { + return _ScalarFlipFlop( + clk, + d, + outputSchema, + en: en, + reset: reset, + resetValue: resetValue, + asyncReset: asyncReset, + usesOutputGenerator: outputGenerator != null, + name: name, + ); + } + return _StructuredFlipFlop( + clk, + d, + outputSchema, + en: en, + reset: reset, + resetValue: resetValue, + asyncReset: asyncReset, + name: name, + ); + } + + FlipFlop._({super.name, super.definitionName}); + + /// Constructs a scalar sequential flip-flop for subclass implementations. /// - /// If enable is high or enable is not provided then flop output will vary - /// on the basis of clock [_clk] and input [_d]. If enable is low, then - /// output of the flop remains frozen irrespective of the input [_d]. - late final Logic? _en = tryInput(_enName); + /// The public unnamed constructor should be preferred for normal use. This + /// constructor preserves the historical subclassing seam for a custom + /// scalar flip-flop; it must be instantiated as [FlipFlop]<[Logic]>. + FlipFlop.scalar( + Logic clk, + Logic d, { + Logic? en, + Logic? reset, + dynamic resetValue, + bool asyncReset = false, + super.name = 'flipflop', + }) : super() { + if (LogicType != Logic) { + throw LogicConstructionException( + 'FlipFlop.scalar is only valid for FlipFlop.', + ); + } + if (clk.width != 1) { + throw PortWidthMismatchException(clk, 1); + } + if (en != null && en.width != 1) { + throw PortWidthMismatchException(en, 1); + } + if (reset != null && reset.width != 1) { + throw PortWidthMismatchException(reset, 1); + } + if (resetValue is Logic && resetValue.width != d.width) { + throw PortWidthMismatchException.equalWidth(resetValue, d); + } - /// Optional reset input to the flop. - late final Logic? _reset = tryInput(_resetName); + _scalarAsyncReset = asyncReset; + final localClock = addInput('clk', clk); + final localD = addInput('d', d, width: d.width); + final localEnable = en == null ? null : addInput('en', en); + final localReset = reset == null ? null : addInput('reset', reset); + final localResetValue = reset != null && resetValue is Logic + ? addInput('resetValue', resetValue, width: d.width) + : null; + _scalarConstantResetValue = reset == null || resetValue is Logic + ? null + : LogicValue.of(resetValue ?? 0, width: d.width); + _scalarQ = addOutput('q', width: d.width) as LogicType; + + var contents = [_scalarQ < localD]; + if (localEnable case final enable?) { + contents = [If(enable, then: contents)]; + } + Sequential( + localClock, + contents, + reset: localReset, + asyncReset: asyncReset, + resetValues: localReset == null + ? null + : {_scalarQ: localResetValue ?? _scalarConstantResetValue!}, + ); + } +} + +LogicValue? _mappedConstantResetValue( + Logic outputSchema, + Logic? reset, + dynamic resetValue, +) { + if (reset == null || resetValue is Logic) { + return null; + } + if (outputSchema is LogicEnum && resetValue is Enum) { + final encodedValue = outputSchema.mapping[resetValue]; + if (encodedValue == null) { + throw ArgumentError.value( + resetValue, + 'resetValue', + 'Not present in the mapping for ${outputSchema.runtimeType}.', + ); + } + return encodedValue; + } + return LogicValue.of(resetValue ?? 0, width: outputSchema.width); +} + +/// Scalar SystemVerilog implementation of [FlipFlop]. +class _ScalarFlipFlop extends FlipFlop + with SystemVerilog { + final String _enName = Naming.unpreferredName('en'); + final String _clkName = Naming.unpreferredName('clk'); + final String _dName = Naming.unpreferredName('d'); + final String _qName = Naming.unpreferredName('q'); + final String _resetName = Naming.unpreferredName('reset'); + final String _resetValueName = Naming.unpreferredName('resetValue'); - /// The input to the flop. + late final Logic _clk = input(_clkName); + late final Logic? _en = tryInput(_enName); + late final Logic? _reset = tryInput(_resetName); late final Logic _d = input(_dName); - /// The output of the flop. - late final Logic q = output(_qName); + @override + late final LogicType q; - /// The reset value for this flop, if it was a port. Logic? _resetValuePort; + LogicValue? _resetValueConst; - /// The reset value for this flop, if it was a constant. - /// - /// Only initialized if a constant value is provided. - late LogicValue _resetValueConst; - - /// Indicates whether provided `reset` signals should be treated as an async - /// reset. If no `reset` is provided, this will have no effect. + @override final bool asyncReset; - /// The constant reset value, or `null` when reset is absent or data-driven. + @override LogicValue? get constantResetValue => _reset == null || _resetValuePort != null ? null : _resetValueConst; - /// Constructs a flip flop which is positive edge triggered on [clk]. - /// - /// When optional [en] is provided, an additional input will be created for - /// flop. If optional [en] is high or not provided, output will vary as per - /// input[d]. For low [en], output remains frozen irrespective of input [d] - /// - /// When the optional [reset] is provided, the flop will be reset active-high. - /// If no [resetValue] is provided, the reset value is always `0`. Otherwise, - /// it will reset to the provided [resetValue]. The type of [resetValue] must - /// be a valid driver of a [ConditionalAssign] (e.g. [Logic], [LogicValue], - /// [int], etc.). - /// - /// If [asyncReset] is true, the [reset] signal (if provided) will be treated - /// as an async reset. If [asyncReset] is false, the reset signal will be - /// treated as synchronous. - FlipFlop( + _ScalarFlipFlop( Logic clk, - Logic d, { + LogicType d, + LogicType outputSchema, { + required this.asyncReset, + required bool usesOutputGenerator, Logic? en, Logic? reset, dynamic resetValue, - this.asyncReset = false, super.name = 'flipflop', - }) { - if (clk.width != 1) { - throw Exception('clk must be 1 bit'); - } - + }) : super._() { addInput(_clkName, clk); - addInput(_dName, d, width: d.width); - addOutput(_qName, width: d.width); + if (LogicType == Logic) { + addInput(_dName, d, width: d.width); + } else { + addTypedInput(_dName, d); + } + q = (LogicType != Logic || usesOutputGenerator + ? addTypedOutput( + _qName, + operationOutputClone(outputSchema), + ) + : addOutput( + _qName, + width: d.width, + ) as LogicType); if (en != null) { addInput(_enName, en); @@ -134,23 +271,20 @@ class FlipFlop extends Module with SystemVerilog { if (reset != null) { addInput(_resetName, reset); - if (resetValue != null && resetValue is Logic) { _resetValuePort = addInput(_resetValueName, resetValue, width: d.width); } else { - _resetValueConst = LogicValue.of(resetValue ?? 0, width: d.width); + _resetValueConst = + _mappedConstantResetValue(outputSchema, reset, resetValue); } } _setup(); } - /// Performs setup for custom functional behavior. void _setup() { var contents = [q < _d]; - - final en = _en; - if (en != null) { + if (_en case final en?) { contents = [If(en, then: contents)]; } @@ -160,13 +294,16 @@ class FlipFlop extends Module with SystemVerilog { reset: _reset, asyncReset: asyncReset, resetValues: - _reset != null ? {q: _resetValuePort ?? _resetValueConst} : null, + _reset != null ? {q: _resetValuePort ?? _resetValueConst!} : null, ); } @override String instantiationVerilog( - String instanceType, String instanceName, Map ports) { + String instanceType, + String instanceName, + Map ports, + ) { var expectedInputs = 2; if (_en != null) { expectedInputs++; @@ -178,35 +315,134 @@ class FlipFlop extends Module with SystemVerilog { expectedInputs++; } - assert(ports.length == expectedInputs + 1, - 'FlipFlop has exactly $expectedInputs inputs and one output.'); + assert( + ports.length == expectedInputs + 1, + 'FlipFlop has exactly $expectedInputs inputs and one output.', + ); final clk = ports[_clkName]!; final d = ports[_dName]!; final q = ports[_qName]!; final en = _en != null ? ports[_enName]! : null; final reset = _reset != null ? ports[_resetName]! : null; - final triggerString = [ clk, if (reset != null && asyncReset) reset, - ].map((e) => 'posedge $e').join(' or '); + ].map((value) => 'posedge $value').join(' or '); final svBuffer = StringBuffer('always_ff @($triggerString) '); - - if (_reset != null) { + if (reset != null) { final resetValueString = _resetValuePort != null ? ports[_resetValueName]! : _resetValueConst.toString(); - svBuffer.write('if(${reset!}) $q <= $resetValueString; else '); + svBuffer.write('if($reset) $q <= $resetValueString; else '); } - - if (_en != null) { - svBuffer.write('if(${en!}) '); + if (en != null) { + svBuffer.write('if($en) '); } - svBuffer.write('$q <= $d; // $instanceName'); - return svBuffer.toString(); } } + +String _structuredFlipFlopResetIdentity( + LogicStructure structure, + bool hasReset, + bool hasResetPort, + dynamic resetValue, +) { + if (!hasReset) { + return 'N'; + } + if (hasResetPort) { + return 'RP'; + } + final value = LogicValue.of(resetValue ?? 0, width: structure.width); + return 'RC${value.toRadixString(includeWidth: false, sepChar: '')}'; +} + +String _structuredFlipFlopDefinitionName( + LogicStructure structure, { + required bool hasEnable, + required bool hasReset, + required bool hasResetPort, + required dynamic resetValue, + required bool asyncReset, +}) => + 'FlipFlop_${logicStructureShapeSignature(structure)}_' + '${hasEnable ? 'E' : 'N'}_' + '${_structuredFlipFlopResetIdentity( + structure, + hasReset, + hasResetPort, + resetValue, + )}_' + '${asyncReset ? 'A' : 'S'}'; + +/// Structure-preserving implementation of [FlipFlop]. +class _StructuredFlipFlop extends FlipFlop { + @override + late final LogicType q; + + @override + final bool asyncReset; + + @override + final LogicValue? constantResetValue; + + _StructuredFlipFlop( + Logic clk, + LogicType d, + LogicType outputSchema, { + required this.asyncReset, + Logic? en, + Logic? reset, + dynamic resetValue, + super.name = 'flipflop', + }) : constantResetValue = reset != null && resetValue is! Logic + ? LogicValue.of(resetValue ?? 0, width: d.width) + : null, + super._( + definitionName: _structuredFlipFlopDefinitionName( + outputSchema as LogicStructure, + hasEnable: en != null, + hasReset: reset != null, + hasResetPort: reset != null && resetValue is Logic, + resetValue: resetValue, + asyncReset: asyncReset, + ), + ) { + final localClock = addInput('clk', clk); + final localD = addTypedInput('d', d); + final localEnable = en == null ? null : addInput('en', en); + final localReset = reset == null ? null : addInput('reset', reset); + final localResetValue = reset != null && resetValue is Logic + ? addInput('resetValue', resetValue, width: d.width) + : null; + q = addTypedOutput( + 'q', + operationOutputClone(outputSchema), + ); + + final structuredQ = q as LogicStructure; + final structuredD = localD as LogicStructure; + var offset = 0; + for (var index = 0; index < structuredQ.leafElements.length; index++) { + final leaf = structuredQ.leafElements[index]; + final width = leaf.width; + final leafResetValue = localResetValue != null + ? localResetValue.getRange(offset, offset + width) + : constantResetValue?.getRange(offset, offset + width); + leaf <= + flop( + localClock, + structuredD.leafElements[index], + en: localEnable, + reset: localReset, + resetValue: leafResetValue, + asyncReset: asyncReset, + ); + offset += width; + } + } +} diff --git a/lib/src/modules/conditionals/if.dart b/lib/src/modules/conditionals/if.dart index 8f46e4090e..627b253465 100644 --- a/lib/src/modules/conditionals/if.dart +++ b/lib/src/modules/conditionals/if.dart @@ -90,12 +90,12 @@ class If extends Conditional { if (iff is Else) { if (iff != iffs.last) { throw InvalidConditionalException( - 'Else must come last in an IfBlock.'); + 'Else must come last in an If.block.'); } if (iff == iffs.first) { throw InvalidConditionalException( - 'Else cannot be the first in an IfBlock.'); + 'Else cannot be the first in an If.block.'); } } } @@ -220,10 +220,10 @@ ${padding}end '''); for (final localMapping in localMappings.entries) { if (!phiMappings.containsKey(localMapping.key)) { - phiMappings[localMapping.key] = Logic( + phiMappings[localMapping.key] = SsaLogic.cloneLike( + localMapping.key, + localMapping.value, name: '${localMapping.key.name}_phi', - width: localMapping.key.width, - naming: Naming.mergeable, ); } @@ -241,18 +241,20 @@ ${padding}end '''); .flattened .map((e) => e.calculateReceivers()) .flattened - .whereType() + .where((signal) => SsaLogic.isSsaProxy(signal, context: context)) .toSet(); for (final iff in iffs) { final alreadyDrivenSsaSignals = iff.then .map((e) => e.calculateReceivers()) .flattened - .whereType() + .where((signal) => SsaLogic.isSsaProxy(signal, context: context)) .toSet(); for (final signalNeedingInit in signalsNeedingInit) { if (!alreadyDrivenSsaSignals.contains(signalNeedingInit)) { - iff.then.add(signalNeedingInit < 0); + iff.then.add( + signalNeedingInit < SsaLogic.branchDefaultValue(signalNeedingInit), + ); } } } diff --git a/lib/src/modules/conditionals/ssa.dart b/lib/src/modules/conditionals/ssa.dart index b76884ff3b..16a2bdc140 100644 --- a/lib/src/modules/conditionals/ssa.dart +++ b/lib/src/modules/conditionals/ssa.dart @@ -26,4 +26,133 @@ class SsaLogic extends Logic { @override SsaLogic clone({String? name}) => throw UnimplementedError('Should not clone an SsaLogic'); + + /// Creates an SSA proxy for [ref], preserving its concrete type when that + /// type can be reconstructed as a driveable signal. + static Logic create( + Logic ref, + int context, { + required bool preserveTypes, + }) { + if (preserveTypes && _canPreserveType(ref)) { + final proxy = ref.clone(name: ref.name); + if (proxy.runtimeType != ref.runtimeType) { + throw LogicConstructionException( + 'SSA proxy clone did not preserve ${ref.runtimeType}.', + ); + } + _markProxy(ref, proxy, context); + return proxy; + } + + return SsaLogic(ref, context); + } + + /// Returns whether [signal] is an SSA proxy in [context]. + static bool isSsaProxy(Logic signal, {required int context}) { + if (signal is SsaLogic) { + return signal.context == context; + } + return _typedReferences[signal]?.context == context; + } + + /// Returns the signal represented by an SSA [signal] in [context]. + static Logic referenceOf(Logic signal, {required int context}) { + if (signal is SsaLogic && signal.context == context) { + return signal.ref; + } + + final reference = _typedReferences[signal]; + if (reference != null && reference.context == context) { + return reference.ref; + } + + throw ArgumentError.value( + signal, + 'signal', + 'Not an SSA proxy in context $context.', + ); + } + + /// Returns all leaf-level SSA proxies contained by [signal]. + static Iterable proxyLeaves(Logic signal, {required int context}) { + if (signal is LogicStructure) { + return signal.leafElements.where( + (element) => isSsaProxy(element, context: context), + ); + } + + return isSsaProxy(signal, context: context) ? [signal] : const []; + } + + /// Returns whether [signal] contains a type-preserving SSA proxy. + static bool containsTypedProxy(Logic signal) => signal is LogicStructure + ? signal.leafElements.any( + (element) => _typedReferences[element] != null, + ) + : _typedReferences[signal] != null; + + /// Creates a driveable signal with [reference]'s concrete type. + static Logic cloneLike( + Logic reference, + Logic currentValue, { + required String name, + }) => + _typedReferences[currentValue] != null + ? reference.clone(name: name) + : Logic(name: name, width: reference.width, naming: Naming.mergeable); + + /// Returns the zero-value driver used to initialize an SSA branch. + /// + /// A [LogicEnum] must receive a variable driver so its mapping can apply the + /// same invalid-value handling as a width-only SSA node. + static dynamic branchDefaultValue(Logic signal) => signal is LogicEnum + ? (Logic( + name: '${signal.name}_ssa_default', + width: signal.width, + naming: Naming.mergeable, + )..gets(Const(0, width: signal.width))) + : 0; + + static bool _canPreserveType(Logic ref) { + if (ref is Const || ref is LogicNet || ref is SsaLogic) { + return false; + } + if (ref is! LogicStructure) { + return ref.runtimeType != Logic; + } + + return ref.leafElements.every( + (element) => element is! Const && element is! LogicNet, + ); + } + + static void _markProxy(Logic ref, Logic proxy, int context) { + if (ref is LogicStructure || proxy is LogicStructure) { + if (ref is! LogicStructure || + proxy is! LogicStructure || + ref.elements.length != proxy.elements.length) { + throw LogicConstructionException( + 'SSA proxy clone did not preserve the structure of ${ref.name}.', + ); + } + + for (var index = 0; index < ref.elements.length; index++) { + _markProxy(ref.elements[index], proxy.elements[index], context); + } + return; + } + + _typedReferences[proxy] = _SsaReference(ref, context); + } } + +class _SsaReference { + final Logic ref; + final int context; + + const _SsaReference(this.ref, this.context); +} + +final Expando<_SsaReference> _typedReferences = + Expando<_SsaReference>('typedSsaReferences'); diff --git a/lib/src/modules/gates.dart b/lib/src/modules/gates.dart index 6501554e4f..6e4f7a4d06 100644 --- a/lib/src/modules/gates.dart +++ b/lib/src/modules/gates.dart @@ -9,6 +9,8 @@ import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/modules/operation_utils.dart'; +import 'package:rohd/src/signals/logic_structure_signature.dart'; /// A gate [Module] that performs bit-wise inversion. class NotGate extends Module with InlineSystemVerilog { @@ -31,8 +33,10 @@ class NotGate extends Module with InlineSystemVerilog { _inName = Naming.unpreferredName(in_.name); _outName = Naming.unpreferredName('${in_.name}_b'); addInput(_inName, in_, width: in_.width); - addOutput(_outName, width: in_.width) - .makeUnassignable(reason: 'Output of a gate $this cannot be assigned.'); + addOutput( + _outName, + width: in_.width, + ).makeUnassignable(reason: 'Output of a gate $this cannot be assigned.'); _setup(); } @@ -166,11 +170,15 @@ abstract class _TwoInputBitwiseGate extends Module with InlineSystemVerilog { /// this [Module] is in-lined as SystemVerilog, it will use [_opStr] as a /// String between the two input signal names (e.g. if [_opStr] was "&", /// generated SystemVerilog may look like "a & b"). - _TwoInputBitwiseGate(this._op, this._opStr, Logic in0, dynamic in1, - {String name = 'gate2', - int outputSvWidthExpansion = 0, - bool makeSelfDetermined = false}) - : width = in0.width, + _TwoInputBitwiseGate( + this._op, + this._opStr, + Logic in0, + dynamic in1, { + String name = 'gate2', + int outputSvWidthExpansion = 0, + bool makeSelfDetermined = false, + }) : width = in0.width, assert(!outputSvWidthExpansion.isNegative, 'Should not be negative.'), _outputSvWidthExpansion = outputSvWidthExpansion, _makeSelfDetermined = makeSelfDetermined, @@ -187,8 +195,10 @@ abstract class _TwoInputBitwiseGate extends Module with InlineSystemVerilog { addInput(_in0Name, in0, width: width); addInput(_in1Name, in1Logic, width: width); - addOutput(_outName, width: width + _outputSvWidthExpansion) - .makeUnassignable(reason: 'Output of a gate $this cannot be assigned.'); + addOutput( + _outName, + width: width + _outputSvWidthExpansion, + ).makeUnassignable(reason: 'Output of a gate $this cannot be assigned.'); _setup(); } @@ -258,9 +268,13 @@ abstract class _TwoInputComparisonGate extends Module with InlineSystemVerilog { /// this [Module] is in-lined as SystemVerilog, it will use [_opStr] as a /// String between the two input signal names (e.g. if [_opStr] was ">", /// generated SystemVerilog may look like "a > b"). - _TwoInputComparisonGate(this._op, this._opStr, Logic in0, dynamic in1, - {String name = 'cmp2'}) - : super(name: name) { + _TwoInputComparisonGate( + this._op, + this._opStr, + Logic in0, + dynamic in1, { + String name = 'cmp2', + }) : super(name: name) { if (in1 is Logic && in0.width != in1.width) { throw PortWidthMismatchException.equalWidth(in0, in1); } @@ -365,11 +379,15 @@ abstract class _ShiftGate extends Module with InlineSystemVerilog { /// this [Module] is in-lined as SystemVerilog, it will use [_opStr] as a /// String between the two input signal names (e.g. if [_opStr] was ">>", /// generated SystemVerilog may look like "a >> b"). - _ShiftGate(this._op, this._opStr, Logic in_, dynamic shiftAmount, - {String name = 'gate2', - this.signed = false, - bool outputSvWidthExpansion = false}) - : width = in_.width, + _ShiftGate( + this._op, + this._opStr, + Logic in_, + dynamic shiftAmount, { + String name = 'gate2', + this.signed = false, + bool outputSvWidthExpansion = false, + }) : width = in_.width, _outputSvWidthExpansion = outputSvWidthExpansion, _isNet = in_.isNet && // if it's a Logic, then we can't treat this like a net @@ -398,17 +416,22 @@ abstract class _ShiftGate extends Module with InlineSystemVerilog { _inName = Naming.unpreferredName('in_${in_.name}'); - _shiftAmountName = - Naming.unpreferredName('shiftAmount_${shiftAmountLogic.name}'); + _shiftAmountName = Naming.unpreferredName( + 'shiftAmount_${shiftAmountLogic.name}', + ); - _outName = - Naming.unpreferredName('${in_.name}_${name}_${shiftAmountLogic.name}'); + _outName = Naming.unpreferredName( + '${in_.name}_${name}_${shiftAmountLogic.name}', + ); final inputCreator = _isNet ? addInOut : addInput; _in = inputCreator(_inName, in_, width: in_.width); - _shiftAmount = inputCreator(_shiftAmountName, shiftAmountLogic, - width: shiftAmountLogic.width); + _shiftAmount = inputCreator( + _shiftAmountName, + shiftAmountLogic, + width: shiftAmountLogic.width, + ); if (_isNet) { out = LogicNet(name: _outName, width: width, naming: Naming.unnamed); @@ -416,9 +439,10 @@ abstract class _ShiftGate extends Module with InlineSystemVerilog { _netSetup(internalOut); } else { - out = addOutput(_outName, width: width) - ..makeUnassignable( - reason: 'Output of a gate $this cannot be assigned.'); + out = addOutput( + _outName, + width: width, + )..makeUnassignable(reason: 'Output of a gate $this cannot be assigned.'); _setup(); } @@ -576,7 +600,10 @@ class Add extends Module with SystemVerilog { @override String instantiationVerilog( - String instanceType, String instanceName, Map ports) { + String instanceType, + String instanceName, + Map ports, + ) { assert(inputs.length == 2, 'Gate has exactly two inputs'); assert(outputs.length == 2, 'Gate has exactly two outputs'); @@ -685,9 +712,11 @@ class GreaterThanOrEqual extends _TwoInputComparisonGate { /// /// [in1] can be either a [Logic] or a constant be processable by /// [LogicValue.of]. - GreaterThanOrEqual(Logic in0, dynamic in1, - {super.name = 'greaterThanOrEqual'}) - : super((a, b) => a >= b, '>=', in0, in1); + GreaterThanOrEqual( + Logic in0, + dynamic in1, { + super.name = 'greaterThanOrEqual', + }) : super((a, b) => a >= b, '>=', in0, in1); } /// A unary AND gate. @@ -763,8 +792,13 @@ class LShift extends _ShiftGate { /// [shiftAmount] can be either a [Logic] or a constant be processable by /// [LogicValue.of]. LShift(Logic in_, dynamic shiftAmount, {super.name = 'lshift'}) - : super((a, shamt) => a << shamt, '<<', in_, shiftAmount, - outputSvWidthExpansion: true); + : super( + (a, shamt) => a << shamt, + '<<', + in_, + shiftAmount, + outputSvWidthExpansion: true, + ); @override void _netSetup(LogicNet internalOut) { @@ -781,26 +815,104 @@ class LShift extends _ShiftGate { /// ``` /// /// If [control] is a valid [Const], returns the selected input directly. -Logic mux(Logic control, Logic d1, Logic d0) { +LogicType mux( + Logic control, + LogicType d1, + LogicType d0, { + LogicType Function({String? name})? outputGenerator, +}) { if (control.width != 1) { throw PortWidthMismatchException(control, 1); } if (d0.width != d1.width) { throw PortWidthMismatchException.equalWidth(d0, d1); } + if (d0 is LogicStructure && d1 is LogicStructure) { + validateMatchingOperationStructure(d1, d0, operation: 'mux'); + } - if (control is Const && control.value.isValid) { + if (outputGenerator == null && control is Const && control.value.isValid) { return control.value == LogicValue.one ? d1 : d0; } - return Mux(control, d1, d0).out; + return Mux( + control, + d1, + d0, + outputGenerator: outputGenerator, + ).out; } -/// A mux (multiplexer) module. +String _muxSchemaSignature(Logic logic) => logic is LogicStructure + ? logicStructureShapeSignature(logic) + : '${logic.runtimeType}_W${logic.width}'; + +/// A multiplexer with an output represented by [LogicType]. +/// +/// The false/default data operand `d0` supplies the output representation when +/// `outputGenerator` is omitted. A supplied `outputGenerator` overrides that +/// default and is invoked exactly once to create the output schema. /// -/// If [_control] has value `1`, then [out] gets [_d1]. -/// If [_control] has value `0`, then [out] gets [_d0]. -class Mux extends Module with InlineSystemVerilog { +/// Plain [Logic] results normalize scalar constants and nets into driveable +/// outputs. A concrete [Const] or [LogicNet] result type is rejected because a +/// dynamic mux output must be driveable. +abstract class Mux extends Module { + /// Output selected by the control input. + LogicType get out; + + /// Constructs a mux preserving [LogicType]. + factory Mux( + Logic control, + LogicType d1, + LogicType d0, { + LogicType Function({String? name})? outputGenerator, + String name = 'mux', + }) { + if (control.width != 1) { + throw PortWidthMismatchException(control, 1); + } + if (d0.width != d1.width) { + throw PortWidthMismatchException.equalWidth(d0, d1); + } + if (d0 is LogicStructure && d1 is LogicStructure) { + validateMatchingOperationStructure(d1, d0, operation: 'Mux'); + } + + final outputSchema = createOperationOutput( + width: d0.width, + name: 'out', + operation: 'Mux<$LogicType>', + prototype: d0, + outputGenerator: outputGenerator, + ); + validateOperationSource(d0, outputSchema, operation: 'Mux d0'); + validateOperationSource(d1, outputSchema, operation: 'Mux d1'); + + if (LogicType == Logic || d0 is! LogicStructure) { + return _ScalarMux( + control, + d1, + d0, + outputSchema, + usesOutputGenerator: outputGenerator != null, + name: name, + ); + } + return _DomainMux( + control, + d1, + d0, + outputSchema, + name: name, + ); + } + + Mux._({super.name, super.definitionName}); +} + +/// A scalar inline implementation of [Mux]. +class _ScalarMux extends Mux + with InlineSystemVerilog { /// Name for the control signal of this mux. late final String _controlName; @@ -822,29 +934,41 @@ class Mux extends Module with InlineSystemVerilog { /// [Mux] input propagated when [out] is `1`. late final Logic _d1 = input(_d1Name); - /// Output port of the [Mux]. - late final Logic out = output(_outName); - - /// Constructs a multiplexer which passes [d0] or [d1] to [out] depending - /// on if [control] is 0 or 1, respectively. - Mux(Logic control, Logic d1, Logic d0, {super.name = 'mux'}) { - if (control.width != 1) { - throw PortWidthMismatchException(control, 1); - } - if (d0.width != d1.width) { - throw PortWidthMismatchException.equalWidth(d0, d1); - } - + /// Output port of the mux. + @override + late final LogicType out; + + _ScalarMux( + Logic control, + LogicType d1, + LogicType d0, + LogicType outputSchema, { + required bool usesOutputGenerator, + super.name = 'mux', + }) : super._() { _controlName = Naming.unpreferredName('control_${control.name}'); _d0Name = Naming.unpreferredName('d0_${d0.name}'); _d1Name = Naming.unpreferredName('d1_${d1.name}'); _outName = Naming.unpreferredName('out'); addInput(_controlName, control); - addInput(_d0Name, d0, width: d0.width); - addInput(_d1Name, d1, width: d1.width); - addOutput(_outName, width: d0.width) - .makeUnassignable(reason: 'Output of a gate $this cannot be assigned.'); + if (LogicType == Logic) { + addInput(_d0Name, d0, width: d0.width); + addInput(_d1Name, d1, width: d1.width); + } else { + addTypedInput(_d0Name, d0); + addTypedInput(_d1Name, d1); + } + out = (LogicType != Logic || usesOutputGenerator + ? addTypedOutput( + _outName, + operationOutputClone(outputSchema), + ) + : addOutput( + _outName, + width: d0.width, + ) as LogicType) + ..makeUnassignable(reason: 'Output of a gate $this cannot be assigned.'); _setup(); } @@ -886,6 +1010,52 @@ class Mux extends Module with InlineSystemVerilog { } } +/// A non-inline implementation preserving a non-scalar [LogicType]. +class _DomainMux extends Mux { + late final Logic _control; + late final LogicType _d0; + late final LogicType _d1; + + @override + late final LogicType out; + + _DomainMux( + Logic control, + LogicType d1, + LogicType d0, + LogicType outputSchema, { + super.name = 'mux', + }) : super._( + definitionName: 'Mux_${_muxSchemaSignature(outputSchema)}', + ) { + _control = addInput('control', control); + _d0 = addTypedInput('d0', d0); + _d1 = addTypedInput('d1', d1); + out = addTypedOutput( + 'out', + operationOutputClone(outputSchema), + ); + + if (out is LogicStructure && + _d0 is LogicStructure && + _d1 is LogicStructure) { + final structuredOut = out as LogicStructure; + final structuredD0 = _d0 as LogicStructure; + final structuredD1 = _d1 as LogicStructure; + for (var index = 0; index < structuredOut.leafElements.length; index++) { + structuredOut.leafElements[index] <= + mux( + _control, + structuredD1.leafElements[index], + structuredD0.leafElements[index], + ); + } + } else { + out <= mux(_control, _d1, _d0); + } + } +} + /// A two-input bit index gate [Module]. /// /// It always takes two inputs and has one output of width 1. @@ -914,8 +1084,9 @@ class IndexGate extends Module with InlineSystemVerilog { IndexGate(Logic original, Logic index) : super() { _originalName = 'original_${original.name}'; _indexName = Naming.unpreferredName('index_${index.name}'); - _selectionName = - Naming.unpreferredName('${original.name}_indexby_${index.name}'); + _selectionName = Naming.unpreferredName( + '${original.name}_indexby_${index.name}', + ); addInput(_originalName, original, width: original.width); addInput(_indexName, index, width: index.width); @@ -1012,19 +1183,25 @@ class ReplicationOp extends Module with InlineSystemVerilog { if (_isNet) { original = addInOut(_inputName, original, width: original.width); - replicated = - LogicNet(name: _outputName, width: newWidth, naming: Naming.unnamed); + replicated = LogicNet( + name: _outputName, + width: newWidth, + naming: Naming.unnamed, + ); final internalOut = addInOut(_outputName, replicated, width: newWidth); for (var i = 0; i < _multiplier; i++) { - internalOut.quietlyMergeSubsetTo(original as LogicNet, - start: i * original.width); + internalOut.quietlyMergeSubsetTo( + original as LogicNet, + start: i * original.width, + ); } } else { addInput(_inputName, original, width: original.width); - replicated = addOutput(_outputName, width: original.width * _multiplier) - ..makeUnassignable( - reason: 'Output of a gate $this cannot be assigned.'); + replicated = addOutput( + _outputName, + width: original.width * _multiplier, + )..makeUnassignable(reason: 'Output of a gate $this cannot be assigned.'); _setup(); } } diff --git a/lib/src/modules/modules.dart b/lib/src/modules/modules.dart index 7a4afaca5a..bfd5244c3a 100644 --- a/lib/src/modules/modules.dart +++ b/lib/src/modules/modules.dart @@ -1,9 +1,10 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause export 'bus.dart'; export 'clkgen.dart'; export 'conditionals/conditionals.dart'; export 'gates.dart'; +export 'passthrough.dart'; export 'pipeline.dart'; export 'tristate.dart'; diff --git a/lib/src/modules/operation_utils.dart b/lib/src/modules/operation_utils.dart new file mode 100644 index 0000000000..da0172f14b --- /dev/null +++ b/lib/src/modules/operation_utils.dart @@ -0,0 +1,194 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// operation_utils.dart +// Shared construction and validation for type-preserving operations. +// +// Author: Desmond A. Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/signals/signals.dart' show BaseLogicArray; + +bool _sameDimensions(List first, List second) => + first.length == second.length && + first.indexed.every((entry) => entry.$2 == second[entry.$1]); + +void _validateMatchingOperationStructure( + Logic first, + Logic second, + String operation, + String path, +) { + if (first.width != second.width) { + throw LogicConstructionException( + '$operation operands differ in width at $path.', + ); + } + + if (first is LogicEnum || second is LogicEnum) { + if (first is! LogicEnum || !first.isEquivalentTypeTo(second)) { + throw LogicConstructionException( + '$operation operands use incompatible LogicEnum mappings at $path.', + ); + } + } + + if (first is LogicStructure || second is LogicStructure) { + if (first is! LogicStructure || + second is! LogicStructure || + first.runtimeType != second.runtimeType || + first.elements.length != second.elements.length) { + throw LogicConstructionException( + '$operation operands differ in structure at $path.', + ); + } + + if (first is BaseLogicArray && second is BaseLogicArray) { + if (!_sameDimensions(first.dimensions, second.dimensions) || + first.elementWidth != second.elementWidth || + first.numUnpackedDimensions != second.numUnpackedDimensions || + first.isNet != second.isNet) { + throw LogicConstructionException( + '$operation array operands differ in shape at $path.', + ); + } + if (first is TypedLogicArray && + second is TypedLogicArray && + !identical(first.valueCodec, second.valueCodec)) { + throw LogicConstructionException( + '$operation typed-array operands use different value codecs at ' + '$path. Convert through packed to reinterpret their bits explicitly.', + ); + } + } + + for (var index = 0; index < first.elements.length; index++) { + _validateMatchingOperationStructure( + first.elements[index], + second.elements[index], + operation, + '$path[$index]', + ); + } + } +} + +/// Verifies structural compatibility for type-preserving operations. +/// +/// This is an internal operation-building contract, not a signal API. +@internal +void validateMatchingOperationStructure( + LogicStructure first, + LogicStructure second, { + required String operation, +}) => + _validateMatchingOperationStructure(first, second, operation, 'root'); + +/// Creates the output representation for a type-preserving operation. +/// +/// If [outputGenerator] is supplied, it defines the output representation. +/// Otherwise, [prototype] supplies it. A requested [Logic] result is always +/// normalized to a fresh ordinary [Logic], allowing constants and nets to be +/// used as sources without making them the output type. +@internal +LogicType createOperationOutput({ + required String name, + required String operation, + int? width, + Naming? naming, + LogicType? prototype, + LogicType Function({String? name})? outputGenerator, +}) { + if (width != null && prototype != null && prototype.width != width) { + throw PortWidthMismatchException(prototype, width); + } + + final LogicType output; + if (outputGenerator != null) { + output = outputGenerator(name: name); + } else if (LogicType == Logic) { + if (width == null) { + throw LogicConstructionException( + '$operation requires a width for a normalized Logic output.', + ); + } + output = Logic(name: name, width: width, naming: naming) as LogicType; + } else if (prototype != null) { + output = prototype.cloneTyped(name: name); + } else { + throw LogicConstructionException( + '$operation cannot infer a $LogicType output representation. ' + 'Provide an outputGenerator.', + ); + } + + if (width != null && output.width != width) { + throw PortWidthMismatchException(output, width); + } + if (output is Const || + output.isNet || + (output is LogicStructure && (output.hasConsts || output.hasNets))) { + throw LogicConstructionException( + '$operation requires a driveable output, but $output is not driveable. ' + 'Request to normalize a constant or net source.', + ); + } + if (output.name != name) { + throw LogicConstructionException( + '$operation output generator did not apply the requested name "$name".', + ); + } + + return output; +} + +/// Validates that [source] can drive [output] without losing a domain type. +/// +/// Structured sources must match the output structure exactly. Callers that +/// intentionally reinterpret a different structure as packed bits must pass +/// its [Logic.packed] representation instead. +@internal +void validateOperationSource( + Logic source, + Logic output, { + required String operation, +}) { + if (source.width != output.width) { + throw PortWidthMismatchException.equalWidth(source, output); + } + + if (source is LogicEnum && + output is LogicEnum && + !source.isEquivalentTypeTo(output)) { + throw LogicConstructionException( + '$operation source and output use incompatible LogicEnum mappings.', + ); + } + + if (source is LogicStructure && output is LogicStructure) { + validateMatchingOperationStructure(source, output, operation: operation); + } else if (source is LogicStructure && output is! LogicStructure) { + // An explicit result intentionally normalizes packed structures. + return; + } else if (source is! LogicStructure && output is LogicStructure) { + // A scalar Logic source is an explicit packed representation. + return; + } +} + +/// Returns a typed output generator that clones [schema]. +@internal +LogicType Function({String? name}) + operationOutputClone(LogicType schema) { + final cloneFactory = _OperationOutputClone(schema); + return cloneFactory.call; +} + +class _OperationOutputClone { + final LogicType schema; + + _OperationOutputClone(this.schema); + + LogicType call({String? name}) => schema.cloneTyped(name: name); +} diff --git a/lib/src/modules/passthrough.dart b/lib/src/modules/passthrough.dart index 4a93f15941..86e5a3cbe8 100644 --- a/lib/src/modules/passthrough.dart +++ b/lib/src/modules/passthrough.dart @@ -1,33 +1,114 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // passthrough.dart // A module that does nothing but pass a signal through. -// -// 2021 May 7 -// Author: Max Korbel import 'package:rohd/rohd.dart'; +import 'package:rohd/src/modules/operation_utils.dart'; +import 'package:rohd/src/signals/logic_structure_signature.dart'; + +/// A no-op module that preserves [LogicType]. +/// +/// The input supplies the output representation. Request `` explicitly +/// to normalize a constant or net input into a driveable scalar output. +abstract class Passthrough extends Module { + /// Input port. + LogicType get in_; -/// A very simple noop module that just passes a signal through. -class Passthrough extends Module { - /// The input port. - Logic get in_ => input('in'); + /// Output port. + LogicType get out; - /// The output port. - Logic get out => output('out'); + /// Constructs a pass-through preserving [LogicType]. + factory Passthrough( + LogicType input, [ + String name = 'passthrough', + ]) => + Passthrough._create(input, name); - /// Constructs a simple pass-through module that performs no operations - /// between [a] and [out]. - Passthrough(Logic a, [String name = 'passthrough']) : super(name: name) { - addInput('in', a, width: a.width); - addOutput('out', width: a.width); - _setup(); + static Passthrough _create( + LogicType input, + String name, + ) { + final outputSchema = createOperationOutput( + width: input.width, + name: 'out', + operation: 'Passthrough<$LogicType>', + prototype: input, + ); + validateOperationSource( + input, + outputSchema, + operation: 'Passthrough input', + ); + + if (LogicType == Logic) { + return _ScalarPassthrough( + input, + name: name, + ) as Passthrough; + } + return _DomainPassthrough( + input, + outputSchema, + name: name, + ); } - void _setup() { + Passthrough._({super.name, super.definitionName}); +} + +/// Scalar implementation of [Passthrough]. +class _ScalarPassthrough extends Passthrough { + @override + late final Logic in_; + + @override + late final Logic out; + + _ScalarPassthrough(Logic input, {super.name = 'passthrough'}) : super._() { + in_ = addInput('in', input, width: input.width); + out = addOutput('out', width: input.width); final inner = Logic(name: 'inner', width: in_.width); inner <= in_; out <= inner; } } + +/// Type-preserving implementation of [Passthrough]. +class _DomainPassthrough + extends Passthrough { + @override + late final LogicType in_; + + @override + late final LogicType out; + + _DomainPassthrough( + LogicType input, + LogicType outputSchema, { + super.name = 'passthrough', + }) : super._( + definitionName: 'Passthrough_${_schemaSignature(outputSchema)}', + ) { + in_ = addTypedInput('in', input); + out = addTypedOutput( + 'out', + operationOutputClone(outputSchema), + ); + + if (out is LogicStructure && in_ is LogicStructure) { + final structuredOut = out as LogicStructure; + final structuredIn = in_ as LogicStructure; + for (var index = 0; index < structuredOut.leafElements.length; index++) { + structuredOut.leafElements[index] <= structuredIn.leafElements[index]; + } + } else { + out <= in_; + } + } +} + +String _schemaSignature(Logic logic) => logic is LogicStructure + ? logicStructureShapeSignature(logic) + : '${logic.runtimeType}_W${logic.width}'; diff --git a/lib/src/modules/pipeline.dart b/lib/src/modules/pipeline.dart index 97cda2dbfe..df7b9168f2 100644 --- a/lib/src/modules/pipeline.dart +++ b/lib/src/modules/pipeline.dart @@ -32,15 +32,25 @@ class PipelineStageInfo { /// stage, you can access it relatively using [stageAdjustment]. For example, /// `p.get(x, -1)` will access the value of `x` at the output of one stage /// prior. - Logic get(Logic identifier, [int stageAdjustment = 0]) => + T get(T identifier, [int stageAdjustment = 0]) => getAbs(identifier, stage + stageAdjustment); /// Returns the output of [identifier] at the specified absolute [stageIndex] /// stage, if other than the current [stage]. Otherwise, returns the same /// thing as [get], for the current [stage]. - Logic getAbs(Logic identifier, int stageIndex) { + T getAbs(T identifier, int stageIndex) { final l = _pipeline.get(identifier, stageIndex); - return stageIndex == stage ? _ssa(l) : l; + if (stageIndex != stage) { + return l; + } + + final remapped = _ssa(l); + if (remapped is! T) { + throw LogicConstructionException( + 'SSA remapping did not preserve the requested $T type.', + ); + } + return remapped; } } @@ -67,24 +77,86 @@ class _PipeStage { /// Registers [newLogic] with this stage and creates appropriate inputs, /// outputs, and internal signals for the stage. void _addLogic(Logic newLogic, int index) { - input[newLogic] = Logic( + input[newLogic] = _clonePipelineLogic( + newLogic, name: '${newLogic.name}_stage${index}_i', - width: newLogic.width, - naming: Naming.mergeable, ); - output[newLogic] = Logic( + output[newLogic] = _clonePipelineLogic( + newLogic, name: '${newLogic.name}_stage${index}_o', - width: newLogic.width, - naming: Naming.mergeable, ); - main[newLogic] = Logic( + main[newLogic] = _clonePipelineLogic( + newLogic, name: '${newLogic.name}_stage$index', - width: newLogic.width, - naming: Naming.mergeable, ); } } +Logic _clonePipelineLogic(Logic source, {required String name}) { + if (_containsNonDriveablePipelineSignal(source)) { + throw LogicConstructionException( + 'Pipeline signals must contain only driveable, non-net Logic values. ' + 'Create an ordinary Logic alias before adding $source to a Pipeline.', + ); + } + + final clone = source.clone(name: name); + if (clone.runtimeType != source.runtimeType) { + throw LogicConstructionException( + 'Pipeline reconstruction did not preserve ${source.runtimeType}.', + ); + } + return clone; +} + +bool _containsNonDriveablePipelineSignal(Logic logic) { + if (logic is Const || logic is LogicNet) { + return true; + } + return logic is LogicStructure && + logic.leafElements.any(_containsNonDriveablePipelineSignal); +} + +Conditional _pipelineAssignment(Logic receiver, Logic driver) { + if (receiver is! LogicStructure || driver is! LogicStructure) { + return receiver < driver; + } + + final receiverLeaves = receiver.leafElements; + final driverLeaves = driver.leafElements; + if (receiverLeaves.length != driverLeaves.length) { + throw LogicConstructionException( + 'Pipeline reconstruction did not preserve the structure of ' + '${receiver.name}.', + ); + } + + return ConditionalGroup([ + for (var index = 0; index < receiverLeaves.length; index++) + receiverLeaves[index] < driverLeaves[index], + ]); +} + +void _connectPipelineLogic(Logic receiver, Logic driver) { + if (receiver is! LogicStructure || driver is! LogicStructure) { + receiver <= driver; + return; + } + + final receiverLeaves = receiver.leafElements; + final driverLeaves = driver.leafElements; + if (receiverLeaves.length != driverLeaves.length) { + throw LogicConstructionException( + 'Pipeline reconstruction did not preserve the structure of ' + '${receiver.name}.', + ); + } + + for (var index = 0; index < receiverLeaves.length; index++) { + receiverLeaves[index] <= driverLeaves[index]; + } +} + /// A simple pipeline, separating arbitrary combinational logic by flop stages. class Pipeline { /// The clocks whose positive edges trigger the flops in this pipeline. @@ -144,30 +216,34 @@ class Pipeline { /// Each stage can be stalled independently using [stalls], where every index /// of [stalls] corresponds to the index of the stage to be stalled. When a /// stage's stall is asserted, the output of that stage will not change. - Pipeline(Logic clk, - {List Function(PipelineStageInfo p)> stages = const [], - List? stalls, - List signals = const [], - Map resetValues = const {}, - Logic? reset, - bool asyncReset = false}) - : this.multi([clk], - stages: stages, - stalls: stalls, - signals: signals, - resetValues: resetValues, - reset: reset, - asyncReset: asyncReset); + Pipeline( + Logic clk, { + List Function(PipelineStageInfo p)> stages = const [], + List? stalls, + List signals = const [], + Map resetValues = const {}, + Logic? reset, + bool asyncReset = false, + }) : this.multi( + [clk], + stages: stages, + stalls: stalls, + signals: signals, + resetValues: resetValues, + reset: reset, + asyncReset: asyncReset, + ); /// Constructs a [Pipeline] with multiple triggers on any of [_clks]. - Pipeline.multi(this._clks, - {List Function(PipelineStageInfo p)> stages = const [], - List? stalls, - List signals = const [], - Map? resetValues, - this.reset, - this.asyncReset = false}) - : _resetValues = resetValues == null ? null : Map.from(resetValues) { + Pipeline.multi( + this._clks, { + List Function(PipelineStageInfo p)> stages = const [], + List? stalls, + List signals = const [], + Map? resetValues, + this.reset, + this.asyncReset = false, + }) : _resetValues = resetValues == null ? null : Map.from(resetValues) { _stages = stages.map(_PipeStage.new).toList(); _stages.add(_PipeStage((p) => [])); // output stage @@ -176,7 +252,7 @@ class Pipeline { signals.forEach(_add); for (var stageIndex = 0; stageIndex < stageCount; stageIndex++) { - Combinational.ssa((ssa) { + Combinational.ssaWithTypes((ssa) { // keep track of the previously registered logics: final prevRegisteredLogics = _registeredLogics.toSet(); @@ -191,22 +267,22 @@ class Pipeline { if (!prevRegisteredLogics.contains(l)) { for (var i = 0; i < stageIndex; i++) { // make sure to hook up in-to-out through main for .get's - get(l, i) <= _i(l, i); - _o(l, i) <= get(l, i); + _connectPipelineLogic(get(l, i), _i(l, i)); + _connectPipelineLogic(_o(l, i), get(l, i)); } } } return [ for (final l in _registeredLogics) - ssa(get(l, stageIndex)) < _i(l, stageIndex), + _pipelineAssignment(ssa(get(l, stageIndex)), _i(l, stageIndex)), ...stageConditionals, ]; }, name: 'comb_stage$stageIndex'); // do output connections as assignments so they can be collapsed for (final l in _registeredLogics) { - _o(l, stageIndex) <= get(l, stageIndex); + _connectPipelineLogic(_o(l, stageIndex), get(l, stageIndex)); } } @@ -217,8 +293,10 @@ class Pipeline { void _setStalls(List? stalls) { if (stalls != null) { if (stalls.length != stageCount - 1) { - throw Exception('Stall list length (${stalls.length}) must match ' - 'number of stages (${stageCount - 1}).'); + throw Exception( + 'Stall list length (${stalls.length}) must match ' + 'number of stages (${stageCount - 1}).', + ); } for (var i = 0; i < stageCount - 1; i++) { final stall = stalls[i]; @@ -239,37 +317,35 @@ class Pipeline { _stages[i]._addLogic(newLogic, i); } - _stages[0].input[newLogic]! <= newLogic; - final ffAssigns = []; + _connectPipelineLogic(_stages[0].input[newLogic]!, newLogic); + final ffAssigns = []; for (var i = 1; i < _stages.length; i++) { - ffAssigns.add(_i(newLogic, i) < _o(newLogic, i - 1) as ConditionalAssign); + final assignment = + _pipelineAssignment(_i(newLogic, i), _o(newLogic, i - 1)); + final stall = _stages[i - 1].stall; + ffAssigns.add( + stall == null ? assignment : If(~stall, then: [assignment]), + ); } - final ffAssignsWithStall = - List.generate(stageCount - 1, (index) { - final stall = _stages[index].stall; - final ffAssign = ffAssigns[index]; - final driver = stall != null - ? mux(stall, ffAssign.receiver, ffAssign.driver) - : ffAssign.driver; - return ffAssign.receiver < driver as ConditionalAssign; - }); - - final stageResetVal = _resetValues?[newLogic]; final resetValuesForNewLogic = {}; - if (stageResetVal != null) { - for (final ffAssign in ffAssignsWithStall) { - resetValuesForNewLogic[ffAssign.receiver] = stageResetVal; + if (_resetValues?.containsKey(newLogic) ?? false) { + final stageResetVal = _resetValues![newLogic]; + for (var i = 1; i < _stages.length; i++) { + resetValuesForNewLogic[_i(newLogic, i)] = stageResetVal; } } - Sequential.multi( + if (ffAssigns.isNotEmpty) { + Sequential.multi( _clks, + ffAssigns, reset: reset, resetValues: resetValuesForNewLogic, asyncReset: asyncReset, - ffAssignsWithStall, - name: 'ff_${newLogic.name}'); + name: 'ff_${newLogic.name}', + ); + } } /// The stage input for a signal associated with [logic] to [stageIndex]. @@ -305,11 +381,12 @@ class Pipeline { /// /// Use [stageIndex] to select the value of [logic] at a specific stage of the /// pipeline. The [stageIndex] must be less than [stageCount]. - Logic get(Logic logic, [int? stageIndex]) { + T get(T logic, [int? stageIndex]) { if (!_isRegistered(logic)) { if (_constructionComplete) { throw PortDoesNotExistException( - 'Signal $logic was not piped through this Pipeline.'); + 'Signal $logic was not piped through this Pipeline.', + ); } else { _add(logic); } @@ -318,11 +395,16 @@ class Pipeline { stageIndex ??= _stages.length - 1; final stageLogic = _stages[stageIndex].main[logic]!; + if (stageLogic is! T) { + throw LogicConstructionException( + 'Pipeline reconstruction did not preserve the requested $T type.', + ); + } return stageLogic; } } -/// A pipeline that implements Ready/Valid protocol at each stage. +/// A multi-signal ready/valid [Pipeline]. class ReadyValidPipeline extends Pipeline { /// Indicates that valid contents are ready to be received /// at the output of the pipeline. @@ -338,7 +420,7 @@ class ReadyValidPipeline extends Pipeline { /// is ready to pull out of the pipeline. final Logic readyPipeOut; - /// Constructs a pipeline with Ready/Valid protocol at each stage. + /// Constructs a multi-signal ready/valid pipeline. /// /// The [validPipeIn] signal indicates that the input to the pipeline is /// valid. The [readyPipeOut] signal indicates that the receiver of the @@ -392,17 +474,20 @@ class ReadyValidPipeline extends Pipeline { }) : super.multi( stages: stages, signals: [validPipeIn, ...signals], - stalls: List.generate(stages.length, - (index) => Logic(name: 'stall_$index', naming: Naming.mergeable)), + stalls: List.generate( + stages.length, + (index) => Logic(name: 'stall_$index', naming: Naming.mergeable), + ), ) { final valid = validPipeIn; final stalls = _stages.map((stage) => stage.stall).toList() ..removeLast(); // garbage value at the end - final readys = List.generate(stages.length, - (index) => Logic(name: 'ready_$index', naming: Naming.mergeable)) - ..add(readyPipeOut); + final readys = List.generate( + stages.length, + (index) => Logic(name: 'ready_$index', naming: Naming.mergeable), + )..add(readyPipeOut); for (var i = 0; i < stalls.length; i++) { readys[i] <= ~get(valid, i + 1) | readys[i + 1]; diff --git a/lib/src/selection.dart b/lib/src/selection.dart index 2d46499682..cf519eeec6 100644 --- a/lib/src/selection.dart +++ b/lib/src/selection.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2023-2024 Intel Corporation +// Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // selection.dart @@ -9,10 +9,10 @@ import 'package:rohd/rohd.dart'; -/// Allows a lists of [Logic]s to have its elemets picked -/// by a [Logic] index value. -extension IndexedLogic on List { - /// Performs a [index] based selection on an [List] of [Logic]. +/// Allows a list of [LogicType]s to have an element selected by a [Logic] +/// index. +extension IndexedLogic on List { + /// Performs an [index]-based selection on this list. /// /// Given a [List] of [Logic] say `logicList` on which we apply [selectIndex] /// and an element [index] as argument , we can select any valid element @@ -27,6 +27,16 @@ extension IndexedLogic on List { /// selected <= logicList.selectIndex(index); /// ``` /// - Logic selectIndex(Logic index, {Logic? defaultValue}) => - index.selectFrom(this, defaultValue: defaultValue); + LogicType selectIndex( + Logic index, { + dynamic defaultValue, + LogicType Function({String? name})? outputGenerator, + String name = 'selectFrom', + }) => + index.selectFrom( + this, + defaultValue: defaultValue, + outputGenerator: outputGenerator, + name: name, + ); } diff --git a/lib/src/signals/logic.dart b/lib/src/signals/logic.dart index 9daf72d947..e1a332c7df 100644 --- a/lib/src/signals/logic.dart +++ b/lib/src/signals/logic.dart @@ -1001,13 +1001,12 @@ class Logic { return isLogicIn; } - /// Performs a [Logic] `index` based selection on an [List] of [Logic] - /// named [busList]. + /// Performs a [Logic] `index` based selection on [busList]. /// /// Using the [Logic] `index` on which [selectFrom] is performed on and /// a [List] of [Logic] named [busList] for `index` based selection, we can - /// select any valid element of type [Logic] within the `logicList` using - /// the `index` of [Logic] type. + /// select any valid element of type [LogicType] within the `logicList` + /// using the `index` of [Logic] type. /// /// Alternatively we can approach this with `busList.selectIndex(index)` /// @@ -1016,26 +1015,28 @@ class Logic { /// // ordering matches closer to array indexing with `0` index-based. /// selected <= index.selectFrom(busList); /// ``` - Logic selectFrom(List busList, {Logic? defaultValue}) { - final selected = Logic( - name: 'selectFrom', - width: busList.first.width, - naming: Naming.mergeable, + LogicType selectFrom( + List busList, { + dynamic defaultValue, + LogicType Function({String? name})? outputGenerator, + String name = 'selectFrom', + }) { + if (busList.isEmpty) { + throw LogicConstructionException( + 'selectFrom requires at least one value.', + ); + } + return cases( + this, + { + for (var index = 0; index < busList.length; index++) + index: busList[index], + }, + conditionalType: ConditionalType.unique, + defaultValue: defaultValue ?? 0, + outputGenerator: outputGenerator, + name: name, ); - - Combinational([ - Case( - this, - [ - for (var i = 0; i < busList.length; i++) - CaseItem(Const(i, width: width), [selected < busList[i]]), - ], - conditionalType: ConditionalType.unique, - defaultItem: [selected < (defaultValue ?? 0)], - ), - ]); - - return selected; } /// If [assignSubset] has been used on this signal, a reference to the diff --git a/lib/src/signals/logic_structure.dart b/lib/src/signals/logic_structure.dart index 1196799c7c..6b98ff2924 100644 --- a/lib/src/signals/logic_structure.dart +++ b/lib/src/signals/logic_structure.dart @@ -9,6 +9,69 @@ part of 'signals.dart'; +LogicType _checkedTypedClone( + LogicType source, { + String? name, +}) { + final cloned = source.clone(name: name); + if (cloned is! LogicType) { + throw LogicConstructionException( + 'Clone did not preserve the requested $LogicType type.', + ); + } + return cloned; +} + +void _validateTypedNamedClone(Logic source, String name) { + if (source is! LogicStructure) { + return; + } + + final clone = source.clone(name: name); + if (clone is Const || clone.hasConsts) { + throw LogicConstructionException( + 'A named typed alias requires driveable cloned fields.', + ); + } +} + +/// Type-preserving conveniences for concrete [Logic] values. +/// +/// The static receiver type determines the static result type. If a value has +/// already been widened to [Logic], this extension cannot recover its original +/// subtype. +extension TypedLogicUtilities on LogicType { + /// Equivalent to [Logic.clone], preserving the receiver's static type. + /// + /// Literal cloning is legal: a concrete [Const] receiver returns another + /// [Const] rather than a driveable alias. + LogicType cloneTyped({String? name}) => _checkedTypedClone(this, name: name); + + /// Creates a named driveable alias while preserving the static result type. + /// + /// A concrete [Const] receiver cannot satisfy this contract because + /// [Const.clone] is immutable while [Const.named] creates an ordinary + /// [Logic] alias. Use [Logic.named] when an ordinary [Logic] alias is + /// intended. + LogicType namedTyped(String name, {Naming? naming}) { + if (this is Const && LogicType == Const) { + throw LogicConstructionException( + 'Const.namedTyped cannot preserve Const because a named alias must be ' + 'driveable. Use named() to create a Logic alias.', + ); + } + + _validateTypedNamedClone(this, name); + final named = this.named(name, naming: naming); + if (named is! LogicType) { + throw LogicConstructionException( + 'Named alias did not preserve the requested $LogicType type.', + ); + } + return named; + } +} + /// Collects a group of [Logic] signals into one entity which can be manipulated /// in a similar way as an individual [Logic]. class LogicStructure implements Logic { @@ -165,7 +228,9 @@ class LogicStructure implements Logic { index += element.width; } - return ConditionalGroup(conditionalAssigns); + return this is BaseLogicArray + ? ConditionalGroup(conditionalAssigns) + : StructureConditionalGroup(this, otherLogic, conditionalAssigns); } /// A list of all leaf-level elements at the deepest hierarchy of this @@ -627,8 +692,18 @@ class LogicStructure implements Logic { } @override - Logic selectFrom(List busList, {Logic? defaultValue}) => - packed.selectFrom(busList, defaultValue: defaultValue); + LogicType selectFrom( + List busList, { + dynamic defaultValue, + LogicType Function({String? name})? outputGenerator, + String name = 'selectFrom', + }) => + packed.selectFrom( + busList, + defaultValue: defaultValue, + outputGenerator: outputGenerator, + name: name, + ); @override bool get isNet => _isNet; diff --git a/lib/src/signals/logic_structure_signature.dart b/lib/src/signals/logic_structure_signature.dart new file mode 100644 index 0000000000..e9364bd0c0 --- /dev/null +++ b/lib/src/signals/logic_structure_signature.dart @@ -0,0 +1,37 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// logic_structure_signature.dart +// Internal structural signatures for operation module definitions. + +import 'package:meta/meta.dart'; +import 'package:rohd/src/signals/signals.dart'; + +String _logicShapeSignature(Logic logic) { + if (logic is BaseLogicArray) { + final signatures = + logic.arrayElements.map(_logicShapeSignature).toList(growable: false); + final elementSignature = signatures.isEmpty + ? 'E' + : signatures.every((signature) => signature == signatures.first) + ? 'R${signatures.length}_${signatures.first}' + : 'H${signatures.join('_')}'; + return 'A${logic.dimensions.join('x')}_W${logic.elementWidth}_' + 'U${logic.numUnpackedDimensions}_${logic.isNet ? 'N' : 'L'}_' + '$elementSignature'; + } + if (logic is LogicStructure) { + return 'S${logic.elements.length}_' + '${logic.elements.map(_logicShapeSignature).join('_')}'; + } + return '${logic is Const ? 'C' : logic.isNet ? 'N' : 'L'}${logic.width}'; +} + +/// Returns an internal signature for a structure's recursive geometry. +/// +/// This is used only when an operation's generated module definition depends +/// on the represented structure shape. It is intentionally not exported from +/// the public signal API. +@internal +String logicStructureShapeSignature(LogicStructure structure) => + _logicShapeSignature(structure); diff --git a/lib/src/signals/signals.dart b/lib/src/signals/signals.dart index 03b760ed06..fadac258a8 100644 --- a/lib/src/signals/signals.dart +++ b/lib/src/signals/signals.dart @@ -9,6 +9,8 @@ import 'dart:math'; import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'package:rohd/rohd.dart'; +import 'package:rohd/src/modules/conditionals/conditional_group.dart' + show StructureConditionalGroup; import 'package:rohd/src/utilities/index_utilities.dart'; import 'package:rohd/src/utilities/sanitizer.dart'; import 'package:rohd/src/utilities/synchronous_propagator.dart'; diff --git a/lib/src/synthesizers/netlist/netlist_cell_mapper.dart b/lib/src/synthesizers/netlist/netlist_cell_mapper.dart index eb3a16742a..1ad56a7f9d 100644 --- a/lib/src/synthesizers/netlist/netlist_cell_mapper.dart +++ b/lib/src/synthesizers/netlist/netlist_cell_mapper.dart @@ -391,6 +391,13 @@ class NetlistCellMapper { parameters: {'WIDTH': ctx.width(d0)}, ); }) + // ── Passthrough ──────────────────────────────────────────────────── + ..register((ctx) { + if (ctx.module is! Passthrough) { + return null; + } + return unaryAY(ctx, r'$buf'); + }) // ── Add ─────────────────────────────────────────────────────────── ..register((ctx) { if (ctx.module is! Add) { diff --git a/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart b/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart index 4227226e40..aa508dc4f6 100644 --- a/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart +++ b/lib/src/synthesizers/netlist/netlist_synth_module_definition.dart @@ -139,6 +139,6 @@ class NetlistSynthModuleDefinition extends SynthModuleDefinition { @override void process() { - // Netlist synthesis preserves every submodule as a cell. + super.process(); } } diff --git a/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart b/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart index 506df78655..a338524ef2 100644 --- a/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart +++ b/lib/src/synthesizers/netlist/netlist_synthesizer_configuration.dart @@ -58,8 +58,8 @@ class NetlistSynthesizerConfiguration { /// Determines which modules should stop netlist hierarchy traversal and be /// emitted as cells in their parent. /// - /// Defaults to matching [FlipFlop] and its subclasses, which contain internal - /// sequential submodules but should be emitted as `$dff` netlist cells. + /// Defaults to matching [Mux], [FlipFlop], and [Passthrough] and their + /// subclasses, which should be emitted as aggregate primitive netlist cells. final SynthModuleLeafPredicate leafModulePredicate; /// The netlist-internal mapper used to convert selected leaf modules to @@ -113,7 +113,7 @@ class NetlistSynthesizerConfiguration { /// Creates a configuration for netlist synthesis. const NetlistSynthesizerConfiguration({ this.moduleStopPolicy, - this.leafModulePredicate = _isFlipFlop, + this.leafModulePredicate = _isNetlistPrimitive, this.netlistCellMapper, @visibleForTesting this.collapseTransparentClusters = false, @visibleForTesting this.enableDeadCellElimination = true, @@ -129,4 +129,5 @@ class NetlistSynthesizerConfiguration { trace ? (packageRoot ?? Directory.current.path) : null; } -bool _isFlipFlop(Module module) => module is FlipFlop; +bool _isNetlistPrimitive(Module module) => + module is Mux || module is FlipFlop || module is Passthrough; diff --git a/lib/src/synthesizers/systemc/systemc_synthesis_result.dart b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart index 5e759fd1f8..af55ba244e 100644 --- a/lib/src/synthesizers/systemc/systemc_synthesis_result.dart +++ b/lib/src/synthesizers/systemc/systemc_synthesis_result.dart @@ -794,7 +794,7 @@ class SystemCSynthesisResult extends SynthesisResult { // ── Mux (ternary) ── if (m is Mux) { final vals = inputs.values.toList(); - final w = m.out.width; + final w = m.outputs[m.resultSignalName]!.width; final utype = systemCType(w); // Cast both branches to avoid C++ ternary type mismatch // (e.g., when one branch is bool and the other is sc_uint<1>) diff --git a/lib/src/synthesizers/systemverilog/system_verilog_service.dart b/lib/src/synthesizers/systemverilog/system_verilog_service.dart index 2f064c5816..1a3ea307b0 100644 --- a/lib/src/synthesizers/systemverilog/system_verilog_service.dart +++ b/lib/src/synthesizers/systemverilog/system_verilog_service.dart @@ -352,6 +352,16 @@ class SystemVerilogService extends CodeGenService { return null; } + /// Returns a map from generated module instance type name to its SV contents. + /// + /// Keys are [SynthesisResult.instanceTypeName] (the uniquified definition + /// name used in the generated SV). + @Deprecated('Use instanceTypeOutput(instanceTypeName) for lookup or ' + 'fileContents for iteration instead.') + Map get contentsByName => { + for (final fc in fileContents) fc.name: fc.contents, + }; + /// The artifacts generated by this service. /// /// Single-file output is named `.sv`. Split output retains diff --git a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart index 1124580576..33b2bda794 100644 --- a/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart +++ b/lib/src/synthesizers/systemverilog/systemverilog_synth_module_definition.dart @@ -37,6 +37,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { _expandNestedUnpackedArrayAssignments(); _inlinePackedRangesIntoSubmoduleInputs(); _collapseAggregateConnections(); + super.process(); _collapseWholeNetBuses(); _forwardPassthroughElementsIntoInlineables(); _replaceNetConnections(); @@ -465,7 +466,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { // sources that are tied through a pass-through net bus rather than via a // direct assignment (see [_traceNetSubsetSource]). final netSubsets = _netSubsetLookups(); - final logicSubsets = _logicSubsetLookups(); + final logicSubsets = logicSubsetLookups(); // Count how many times each array [SynthLogic] is used "as a whole", // along with where (a submodule port mapping is the only use we can @@ -592,7 +593,7 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { aggregateLogic.elementWidth == 1 && aggregateLogic.numUnpackedDimensions == 0; final packedSubsetSource = isPackedBitArray - ? _packedLogicSubsetSource( + ? packedLogicSubsetSource( elementLogics, logicSubsets, expectedWidth: agg.width, @@ -736,70 +737,6 @@ class SystemVerilogSynthModuleDefinition extends SynthModuleDefinition { } } - /// Indexes non-net [BusSubset] views by their resolved subset output. - Map> _logicSubsetLookups() { - final bySubset = >{}; - for (final instantiation in subModuleInstantiations) { - instantiation as SystemVerilogSynthSubModuleInstantiation; - final subsetModule = instantiation.module; - if (subsetModule is! BusSubset || subsetModule.original.isNet) { - continue; - } - final original = - instantiation.inputMapping[subsetModule.original.name]?.resolved; - final subset = - instantiation.outputMapping[subsetModule.subset.name]?.resolved; - if (original == null || subset == null) { - continue; - } - final view = ( - inst: instantiation, - original: original, - subset: subset, - start: subsetModule.startIndex, - end: subsetModule.endIndex, - ); - bySubset.putIfAbsent(subset, () => []).add(view); - } - return bySubset; - } - - /// Reconstructs one fully covered packed source from ordered element subset - /// mappings, returning the source and helper instantiations to clear. - ({ - SynthLogic source, - Set subsets, - })? _packedLogicSubsetSource( - List elements, - Map> subsetsByOutput, { - required int expectedWidth, - }) { - SynthLogic? source; - var nextBit = 0; - final subsets = {}; - for (final element in elements) { - if (element == null) { - return null; - } - final view = subsetsByOutput[element]?.singleOrNull; - if (view == null || - view.start != nextBit || - view.end != nextBit + element.width - 1 || - (source != null && source != view.original)) { - return null; - } - source ??= view.original; - subsets.add(view.inst); - nextBit += element.width; - } - if (source == null || - source.width != expectedWidth || - nextBit != expectedWidth) { - return null; - } - return (source: source, subsets: subsets); - } - /// Builds resolved lookups over every net [BusSubset] instantiation: a list /// of all views, plus maps keyed by the resolved `original` bus and the /// resolved `subset`. Reversed slices (`start > end`) are skipped because @@ -1624,16 +1561,6 @@ typedef _NetSubsetView = ({ int end, }); -/// A resolved view of a non-net [BusSubset] used to reconstruct a packed bus -/// from an array input's element mappings. -typedef _LogicSubsetView = ({ - SystemVerilogSynthSubModuleInstantiation inst, - SynthLogic original, - SynthLogic subset, - int start, - int end, -}); - /// A special [Module] for connecting or assigning two SystemVerilog nets /// together bidirectionally. /// diff --git a/lib/src/synthesizers/utilities/synth_module_definition.dart b/lib/src/synthesizers/utilities/synth_module_definition.dart index d53b696486..848800efae 100644 --- a/lib/src/synthesizers/utilities/synth_module_definition.dart +++ b/lib/src/synthesizers/utilities/synth_module_definition.dart @@ -1279,11 +1279,137 @@ class SynthModuleDefinition { } /// Performs additional processing on the current definition to simplify, - /// reduce, etc. + /// reduce, etc. Overrides must invoke `super.process()` at the appropriate + /// graph-processing point. @protected @visibleForOverriding void process() { - // by default, nothing! + _collapsePackedArrayLogicAssignments(); + } + + /// Collapses a full packed module input split across a packed output array. + /// + /// The rewrite is backend-independent: it replaces complete [BusSubset] + /// coverage with a direct [SynthAssignment] and removes only the consumed + /// helper instantiations. Backends remain responsible for declaration + /// legality and rendering of the resulting assignment. + void _collapsePackedArrayLogicAssignments() { + final subsetsByOutput = logicSubsetLookups(); + for (final arraySynth in outputs.where((signal) => signal.isArray)) { + final array = arraySynth.logics.whereType().firstOrNull; + if (array == null || + array.dimensions.length != 1 || + array.numUnpackedDimensions != 0) { + continue; + } + + final packedSource = packedLogicSubsetSource( + array.elements.map(getSynthLogic).toList(growable: false), + subsetsByOutput, + expectedWidth: array.width, + ); + if (packedSource == null || + !packedSource.source.isPort(module) || + !inputs.contains(packedSource.source.resolved)) { + continue; + } + + assignments.add(SynthAssignment(packedSource.source, arraySynth)); + for (final subset in packedSource.subsets) { + subset.clearInstantiation(); + } + } + } + + /// Indexes non-net [BusSubset] views by their resolved subset output. + @protected + Map< + SynthLogic, + List< + ({ + SynthSubModuleInstantiation inst, + SynthLogic original, + SynthLogic subset, + int start, + int end, + })>> logicSubsetLookups() { + final bySubset = >{}; + for (final instantiation in subModuleInstantiations) { + final subsetModule = instantiation.module; + if (subsetModule is! BusSubset || subsetModule.original.isNet) { + continue; + } + final original = + instantiation.inputMapping[subsetModule.original.name]?.resolved; + final subset = + instantiation.outputMapping[subsetModule.subset.name]?.resolved; + if (original == null || subset == null) { + continue; + } + final view = ( + inst: instantiation, + original: original, + subset: subset, + start: subsetModule.startIndex, + end: subsetModule.endIndex, + ); + bySubset.putIfAbsent(subset, () => []).add(view); + } + return bySubset; + } + + /// Reconstructs one fully covered packed source from ordered element subsets. + @protected + ({ + SynthLogic source, + Set subsets, + })? packedLogicSubsetSource( + List elements, + Map< + SynthLogic, + List< + ({ + SynthSubModuleInstantiation inst, + SynthLogic original, + SynthLogic subset, + int start, + int end, + })>> + subsetsByOutput, { + required int expectedWidth, + }) { + SynthLogic? source; + var nextBit = 0; + final subsets = {}; + for (final element in elements) { + if (element == null) { + return null; + } + final view = subsetsByOutput[element]?.singleOrNull; + if (view == null || + view.start != nextBit || + view.end != nextBit + element.width - 1 || + (source != null && source != view.original)) { + return null; + } + source ??= view.original; + subsets.add(view.inst); + nextBit += element.width; + } + if (source == null || + source.width != expectedWidth || + nextBit != expectedWidth) { + return null; + } + return (source: source, subsets: subsets); } /// Prunes any signals that are not used in this definition, including any diff --git a/lib/src/synthesizers/utilities/synth_module_stop_policy.dart b/lib/src/synthesizers/utilities/synth_module_stop_policy.dart index 9447f27642..e62aa5f099 100644 --- a/lib/src/synthesizers/utilities/synth_module_stop_policy.dart +++ b/lib/src/synthesizers/utilities/synth_module_stop_policy.dart @@ -45,7 +45,7 @@ class SynthModuleStopPolicy { /// Creates the default netlist stopping policy. factory SynthModuleStopPolicy.netlist({ - SynthModuleLeafPredicate leafModulePredicate = _isFlipFlop, + SynthModuleLeafPredicate leafModulePredicate = _isNetlistPrimitive, }) => SynthModuleStopPolicy( generatesDefinitionByDefault: (module) => module.subModules.isNotEmpty, @@ -62,4 +62,5 @@ class SynthModuleStopPolicy { bool generatesDefinition(Module module) => !isLeaf(module); } -bool _isFlipFlop(Module module) => module is FlipFlop; +bool _isNetlistPrimitive(Module module) => + module is Mux || module is FlipFlop || module is Passthrough; diff --git a/lib/src/values/logic_value.dart b/lib/src/values/logic_value.dart index c3826dc162..bce00a5b92 100644 --- a/lib/src/values/logic_value.dart +++ b/lib/src/values/logic_value.dart @@ -425,8 +425,8 @@ abstract class LogicValue implements Comparable { /// The [stringRepresentation] should only contain bit values (e.g. no `0b` /// at the start). The order of the created [LogicValue] will be such that /// the `i`th character in [stringRepresentation] corresponds to the - /// `length - i - 1`th bit. That is, the last character of - /// [stringRepresentation] will be the 0th bit of the returned [LogicValue]. + /// `width - i - 1`th bit. That is, the last character in + /// [stringRepresentation] is the 0th bit of the returned [LogicValue]. /// /// ```dart /// var stringRepresentation = '1x0'; diff --git a/lib/src/wave_dumper.dart b/lib/src/wave_dumper.dart index 0b46e88666..a71b7b753c 100644 --- a/lib/src/wave_dumper.dart +++ b/lib/src/wave_dumper.dart @@ -11,8 +11,35 @@ import 'package:rohd/rohd.dart'; /// Records VCD waveforms through [Module.dumpWaves] for legacy callers. /// -/// Use [Module.dumpWaves] for simple VCD output or [WaveformService] for -/// filtering, timescales, and recording windows. +/// [WaveDumper] is a simple wrapper around [WaveformService] for backward +/// compatibility. It provides the legacy API for recording all signal changes +/// in a simulation to a VCD file without retaining the complete VCD in memory. +/// +/// **Migration guide:** +/// +/// Replace: +/// ```dart +/// var dumper = WaveDumper(module, outputPath: 'output.vcd'); +/// ``` +/// +/// With: +/// ```dart +/// var service = module.dumpWaves(outputPath: 'output.vcd'); +/// ``` +/// +/// For more control over filtering, timescale, and recording windows, create a +/// [WaveformService] directly: +/// ```dart +/// final service = WaveformService( +/// module, +/// outputDirectory: 'build', +/// outputBaseName: 'debug', +/// writeToFile: true, +/// timescale: '1ns', +/// startTime: 100, +/// signalFilter: (signal) => signal.name.startsWith('debug_'), +/// ); +/// ``` @Deprecated('Use Module.dumpWaves() for simple VCD output, or ' 'WaveformService for advanced waveform configuration.') class WaveDumper { @@ -24,7 +51,14 @@ class WaveDumper { /// The output filepath of the generated waveforms. String get outputPath => _service.outputPath; - /// Records all signal changes in the built [module] at [outputPath]. + /// Attaches a [WaveDumper] to record all signal changes in a simulation of + /// [module] in a VCD file at [outputPath]. + /// + /// [module] must be built prior to construction. + /// + /// **Deprecated:** Use [Module.dumpWaves] for simple VCD output, or + /// [WaveformService] for signal filtering, custom timescale, recording + /// windows, and extensibility hooks for streaming applications. @Deprecated('Use Module.dumpWaves() for simple VCD output, or ' 'WaveformService for advanced waveform configuration.') WaveDumper(Module module, {String outputPath = 'waves.vcd'}) diff --git a/test/array_collapsing_test.dart b/test/array_collapsing_test.dart index 18c18d3f71..18beec7b92 100644 --- a/test/array_collapsing_test.dart +++ b/test/array_collapsing_test.dart @@ -2565,7 +2565,7 @@ void main() { expect(_topModuleBody(mod.dumpSystemVerilog()), topBody); final vectors = [ for (final text in [ - for (final pattern in [0, 0xff, 0xe4, 0x1b, 0x55, 0xaa]) + for (final pattern in [0, 0xff, 0xe4]) pattern.toRadixString(2).padLeft(8, '0'), 'xz10zx01', 'z01x10xz', @@ -2582,7 +2582,7 @@ void main() { ]; await SimCompare.checkFunctionalVector(mod, vectors); if (configuration.unpacked > 0) { - SimCompare.checkVerilatorVector(mod, vectors.take(6).toList()); + SimCompare.checkVerilatorVector(mod, vectors.take(3).toList()); } else { SimCompare.checkIverilogVector(mod, vectors); } @@ -2599,7 +2599,7 @@ void main() { expect(topBody, isNot(contains('splitStage'))); expect(topBody, isNot(contains('assign '))); final vectors = [ - for (final pattern in [0, 0xff, 0x1b, 0xe4, 0x55, 0xaa]) + for (final pattern in [0, 0xff, 0x1b]) Vector({ 'source': pattern }, { @@ -2894,7 +2894,7 @@ void main() { expect(topBody, isNot(contains('_subset'))); expect(topBody, isNot(contains('assign dst[0]'))); - expect(topBody, contains('assign dst = src[7:0];')); + expect(topBody, contains('assign dst = src;')); final vectors = [ for (final pattern in [0x00, 0x5A, 0xA5, 0xFF]) @@ -3423,7 +3423,7 @@ void main() { expect(topBody, contains('manual_subset')); expect(topBody, contains('assign manual_subset[4:2] = src[4:2];')); - expect(topBody, contains('assign y = manual_subset[5:0];')); + expect(topBody, contains('assign y = manual_subset;')); final vectors = [ for (final pattern in [0x00, 0x15, 0x2A, 0x3F]) diff --git a/test/async_reset_test.dart b/test/async_reset_test.dart index 82e1dbcdfc..5e573643bb 100644 --- a/test/async_reset_test.dart +++ b/test/async_reset_test.dart @@ -114,7 +114,7 @@ void main() { ), 'FlipFlop with no asyncReset': (Logic clk, Logic reset, Logic val) { val <= - FlipFlop( + FlipFlop( clk, reset: reset, Const(1), @@ -122,7 +122,7 @@ void main() { }, 'flop with no asyncReset': (Logic clk, Logic reset, Logic val) { val <= - flop( + flop( clk, reset: reset, Const(1), @@ -198,7 +198,7 @@ void main() { ), 'FlipFlop with asyncReset': (Logic clk, Logic reset, Logic val) { val <= - FlipFlop( + FlipFlop( clk, reset: reset, Const(1), @@ -207,7 +207,7 @@ void main() { }, 'flop with asyncReset': (Logic clk, Logic reset, Logic val) { val <= - flop( + flop( clk, reset: reset, Const(1), diff --git a/test/logic_array_test.dart b/test/logic_array_test.dart index ddd6de3332..e0a11a1f77 100644 --- a/test/logic_array_test.dart +++ b/test/logic_array_test.dart @@ -7,13 +7,10 @@ // 2023 May 2 // Author: Max Korbel -import 'dart:math'; - import 'package:collection/collection.dart'; import 'package:rohd/rohd.dart'; import 'package:rohd/src/utilities/simcompare.dart'; import 'package:rohd/src/utilities/sv_cleaner.dart'; -import 'package:rohd/src/utilities/web.dart'; import 'package:test/test.dart'; class _TypedInputModule extends Module { @@ -580,14 +577,14 @@ class PartialArrayAssignTop extends Module { void main() { List passthroughVectors(int width) { - const randWidth = 23; - final rand = Random(1234); - final values = List.generate( - 10, - (index) => - LogicValue.ofInt(rand.nextInt(oneSllBy(randWidth)), randWidth) - .replicate(width ~/ randWidth + 1) - .getRange(0, width)); + final values = [ + LogicValue.ofInt(0, width), + LogicValue.filled(width, LogicValue.one), + LogicValue.ofString([ + for (var bit = width - 1; bit >= 0; bit--) + if (bit.isEven) '1' else '0', + ].join()), + ]; return [ for (final value in values) Vector({'laIn': value}, {'laOut': value}) ]; @@ -881,7 +878,7 @@ void main() { group('simple', () { test('single dimension', () async { - final mod = SimpleLAPassthrough(LogicArray([3], 8)); + final mod = SimpleLAPassthrough(LogicArray([3], 3)); await testArrayPassthrough(mod); }); @@ -891,12 +888,12 @@ void main() { }); test('2 dimensions', () async { - final mod = SimpleLAPassthrough(LogicArray([3, 2], 8)); + final mod = SimpleLAPassthrough(LogicArray([3, 2], 3)); await testArrayPassthrough(mod); }); test('3 dimensions', () async { - final mod = SimpleLAPassthrough(LogicArray([3, 2, 3], 8)); + final mod = SimpleLAPassthrough(LogicArray([3, 2, 2], 3)); await testArrayPassthrough(mod); }); @@ -941,7 +938,7 @@ void main() { }); test('sub-array', () async { - final superArray = LogicArray([4, 3, 2], 8); + final superArray = LogicArray([3, 2, 2], 3); final subArray = superArray.elements[0] as LogicArray; final mod = SimpleLAPassthrough(subArray); await testArrayPassthrough(mod); @@ -949,8 +946,8 @@ void main() { test('3 dimensions with interface', () async { final mod = LAPassthroughWithIntf(LAPassthroughIntf( - dimensions: [3, 2, 3], - elementWidth: 3, + dimensions: [3, 2, 2], + elementWidth: 2, numUnpackedDimensions: 0, )); @@ -958,29 +955,29 @@ void main() { // ensure ports with interface are still an array final sv = mod.dumpSystemVerilog(); - expect(sv, contains('input logic [2:0][1:0][2:0][2:0] laIn')); - expect(sv, contains('output logic [2:0][1:0][2:0][2:0] laOut')); + expect(sv, contains('input logic [2:0][1:0][1:0][1:0] laIn')); + expect(sv, contains('output logic [2:0][1:0][1:0][1:0] laOut')); }); testWithVerilator( '3 dimensions with interface and unpacked', () => LAPassthroughWithIntf(LAPassthroughIntf( - dimensions: [3, 2, 3], - elementWidth: 3, + dimensions: [3, 2, 2], + elementWidth: 2, numUnpackedDimensions: 1, )), (mod) async { await testArrayPassthrough(mod, noSvSim: true); // ensure ports with interface are still an array final sv = mod.dumpSystemVerilog(); - expect(sv, contains('input logic [1:0][2:0][2:0] laIn [2:0]')); - expect(sv, contains('output logic [1:0][2:0][2:0] laOut [2:0]')); + expect(sv, contains('input logic [1:0][1:0][1:0] laIn [2:0]')); + expect(sv, contains('output logic [1:0][1:0][1:0] laOut [2:0]')); }); }); group('pack and unpack', () { test('3d', () async { - final mod = PackAndUnpackPassthrough(LogicArray([5, 3, 2], 8)); + final mod = PackAndUnpackPassthrough(LogicArray([3, 2, 2], 3)); await testArrayPassthrough(mod, checkNoSwizzle: false); }); @@ -995,13 +992,13 @@ void main() { group('pack and unpack with arrays', () { test('2d', () async { - final mod = PackAndUnpackWithArraysPassthrough(LogicArray([3, 2], 8)); + final mod = PackAndUnpackWithArraysPassthrough(LogicArray([3, 2], 3)); await testArrayPassthrough(mod, checkNoSwizzle: false); }); test('3d', () async { final mod = - PackAndUnpackWithArraysPassthrough(LogicArray([4, 3, 2], 8)); + PackAndUnpackWithArraysPassthrough(LogicArray([3, 2, 2], 3)); await testArrayPassthrough(mod, checkNoSwizzle: false); }); @@ -1017,31 +1014,31 @@ void main() { group('change array dimensions around and back', () { test('3d', () async { - final mod = RearrangeArraysPassthrough(LogicArray([4, 3, 2], 8)); + final mod = RearrangeArraysPassthrough(LogicArray([3, 2, 2], 3)); await testArrayPassthrough(mod); }); testWithVerilator( '3d unpacked', () => RearrangeArraysPassthrough( - LogicArray([4, 3, 2], 3, numUnpackedDimensions: 2), + LogicArray([3, 2, 2], 3, numUnpackedDimensions: 2), intermediateUnpacked: 1), (mod) async { // unpacked array assignment not fully supported in iverilog await testArrayPassthrough(mod, noSvSim: true); final sv = mod.dumpSystemVerilog(); - expect(sv.contains('logic [2:0][3:0][2:0] intermediate [1:0]'), true); + expect(sv.contains('logic [1:0][2:0][2:0] intermediate [1:0]'), true); }); }); group('different port and input widths', () { test('array param mismatch', () async { - final i = LogicArray([3, 2], 8, numUnpackedDimensions: 1); - final o = LogicArray([3, 2], 8, numUnpackedDimensions: 1); + final i = LogicArray([3, 2], 3, numUnpackedDimensions: 1); + final o = LogicArray([3, 2], 3, numUnpackedDimensions: 1); final mod = SimpleLAPassthrough( i, dimOverride: [1, 3], - elemWidthOverride: 16, + elemWidthOverride: 6, numUnpackedOverride: 0, ); o <= mod.laOut; @@ -1049,12 +1046,12 @@ void main() { }); test('logic into array', () async { - final i = Logic(width: 3 * 2 * 8); - final o = Logic(width: 3 * 2 * 8); + final i = Logic(width: 3 * 2 * 3); + final o = Logic(width: 3 * 2 * 3); final mod = SimpleLAPassthroughLogic( i, dimensions: [1, 3], - elementWidth: 16, + elementWidth: 6, numUnpackedDimensions: 0, ); o <= mod.laOut; @@ -1064,7 +1061,7 @@ void main() { group('name collisions', () { test('3d', () async { - final mod = ArrayNameConflicts(LogicArray([4, 3, 2], 8)); + final mod = ArrayNameConflicts(LogicArray([3, 2, 2], 3)); await testArrayPassthrough(mod, checkNoSwizzle: false); }); @@ -1080,7 +1077,7 @@ void main() { group('simple hierarchy', () { test('3d', () async { - final mod = SimpleArraysAndHierarchy(LogicArray([2], 8)); + final mod = SimpleArraysAndHierarchy(LogicArray([2], 3)); await testArrayPassthrough(mod); final sv = mod.dumpSystemVerilog(); expect(sv, contains('SimpleLAPassthrough simple_la_passthrough')); @@ -1099,7 +1096,7 @@ void main() { group('fancy hierarchy', () { test('3d', () async { - final mod = FancyArraysAndHierarchy(LogicArray([4, 3, 2], 8)); + final mod = FancyArraysAndHierarchy(LogicArray([3, 2, 2], 3)); await testArrayPassthrough(mod, checkNoSwizzle: false); final sv = mod.dumpSystemVerilog(); @@ -1120,7 +1117,7 @@ void main() { group('conditionals', () { test('3 dimensions conditional assignment', () async { - final mod = CondAssignArray(LogicArray([3, 2, 3], 8)); + final mod = CondAssignArray(LogicArray([3, 2, 2], 3)); await testArrayPassthrough(mod); }); diff --git a/test/logic_enum_test.dart b/test/logic_enum_test.dart index 202b6b8815..32020534db 100644 --- a/test/logic_enum_test.dart +++ b/test/logic_enum_test.dart @@ -3132,7 +3132,12 @@ void main() { ]; await SimCompare.checkFunctionalVector(module, vectors); - for (final generateEnums in [true, false]) { + // Array-layout coverage needs every packing mode. The packed case also + // covers disabling enum declarations, which is representation-only. + for (final generateEnums in [ + true, + if (unpacked == 0) false, + ]) { final configuration = SystemVerilogSynthesizerConfiguration( generateEnums: generateEnums); final sv = module.dumpSystemVerilog(configuration: configuration); diff --git a/test/logic_structure_test.dart b/test/logic_structure_test.dart index 362706f8e7..9232175247 100644 --- a/test/logic_structure_test.dart +++ b/test/logic_structure_test.dart @@ -244,6 +244,17 @@ void main() { expect(orig.clone(name: 'newName').name, 'newName'); }); + test('whole structure conditional assignment groups leaf assignments', () { + final receiver = MyStruct(name: 'receiver'); + final driver = MyStruct(name: 'driver'); + + final assignment = receiver < driver; + + expect(assignment, isA()); + final group = assignment as ConditionalGroup; + expect(group.conditionals, hasLength(receiver.leafElements.length)); + }); + test('tricky withSet', () { // first field has width of 72 so this is the starting point // second field has a width of 12 diff --git a/test/long_chain_test.dart b/test/long_chain_test.dart index d22352b0c6..309c6d0e8e 100644 --- a/test/long_chain_test.dart +++ b/test/long_chain_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2022-2023 Intel Corporation +// Copyright (C) 2022-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // long_chain_test.dart @@ -8,7 +8,6 @@ // Author: Max Korbel import 'package:rohd/rohd.dart'; -import 'package:rohd/src/modules/passthrough.dart'; import 'package:rohd/src/utilities/web.dart'; import 'package:test/test.dart'; diff --git a/test/module_test.dart b/test/module_test.dart index 07963890cf..313831dfe7 100644 --- a/test/module_test.dart +++ b/test/module_test.dart @@ -126,7 +126,7 @@ class StructWithOutputAsElementMod extends Module { b = addInput('b', b); final s = SimpleLogicStructure( - a: mux(a, Const(0), Const(1)), + a: mux(a, Const(0), Const(1)), b: Const(1), ); diff --git a/test/multimodule3_test.dart b/test/multimodule3_test.dart index 76fde0b1dc..e1275682fe 100644 --- a/test/multimodule3_test.dart +++ b/test/multimodule3_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2021-2023 Intel Corporation +// Copyright (C) 2021-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // multimodule3_test.dart @@ -9,7 +9,6 @@ // Author: Max Korbel import 'package:rohd/rohd.dart'; -import 'package:rohd/src/modules/passthrough.dart'; import 'package:rohd/src/utilities/simcompare.dart'; import 'package:test/test.dart'; diff --git a/test/multimodule4_test.dart b/test/multimodule4_test.dart index 5db4ec513d..94922aeb37 100644 --- a/test/multimodule4_test.dart +++ b/test/multimodule4_test.dart @@ -9,7 +9,6 @@ // Author: Max Korbel import 'package:rohd/rohd.dart'; -import 'package:rohd/src/modules/passthrough.dart'; import 'package:test/test.dart'; // mostly all inputs @@ -47,17 +46,22 @@ void main() { // find a module with 'z' output 2 levels deep assert( - ftm.subModules - .where((pIn1) => pIn1.subModules + ftm.subModules + .where( + (pIn1) => pIn1.subModules .where((pIn2) => pIn2.outputs.containsKey('z')) - .isNotEmpty) - .isNotEmpty, - 'Should find a z two levels deep'); + .isNotEmpty, + ) + .isNotEmpty, + 'Should find a z two levels deep', + ); final synth = ftm.dumpSystemVerilog(); // "z = 1" means it correctly traversed down from inputs - assert(synth.contains('z = 1'), - 'Should correctly traverse from inputs to z=1'); + assert( + synth.contains('z = 1'), + 'Should correctly traverse from inputs to z=1', + ); }); } diff --git a/test/multimodule5_test.dart b/test/multimodule5_test.dart index a8813276f1..8b0c8dc83a 100644 --- a/test/multimodule5_test.dart +++ b/test/multimodule5_test.dart @@ -9,7 +9,6 @@ // Author: Max Korbel import 'package:rohd/rohd.dart'; -import 'package:rohd/src/modules/passthrough.dart'; import 'package:test/test.dart'; class TopModule extends Module { @@ -22,7 +21,7 @@ class TopModule extends Module { Combinational([internalNet < inPort]); Combinational([outPort < internalNet]); - Passthrough(internalNet); + Passthrough(internalNet); } } diff --git a/test/net_bus_test.dart b/test/net_bus_test.dart index e09c1c8293..7a6cae924b 100644 --- a/test/net_bus_test.dart +++ b/test/net_bus_test.dart @@ -788,8 +788,7 @@ void main() { expect( sv, contains('assign _swizzled = ' - '{({({in0[1][1],in0[1][0]}),({in0[0][1],in0[0][0]})}),' - '(in1[3:0])};')); + '{({(in0[1]),(in0[0])}),(in1)};')); }); test('net array 2', () async { diff --git a/test/netlist_synthesizer_test.dart b/test/netlist_synthesizer_test.dart index dd4da394c5..b44ed130ad 100644 --- a/test/netlist_synthesizer_test.dart +++ b/test/netlist_synthesizer_test.dart @@ -14,8 +14,10 @@ import 'package:rohd/rohd.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_cell_mapper.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_module_translation.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_passes.dart'; +import 'package:rohd/src/synthesizers/netlist/netlist_synth_module_definition.dart'; import 'package:rohd/src/synthesizers/netlist/netlist_validation.dart'; import 'package:rohd/src/synthesizers/utilities/synth_logic.dart'; +import 'package:rohd/src/synthesizers/utilities/synth_module_definition.dart'; import 'package:rohd/src/synthesizers/utilities/synth_structure_concat.dart'; import 'package:test/test.dart'; @@ -70,6 +72,14 @@ class NotModule extends Module { } } +/// Splits a packed input across a packed array output. +class PackedInputArrayOutputModule extends Module { + PackedInputArrayOutputModule(Logic data) { + data = addInput('data', data, width: 8); + addOutputArray('out', dimensions: [4], elementWidth: 2) <= data; + } +} + /// Exercises preservation of user-named internal signals. class UserNamedInternalSignalsModule extends Module { UserNamedInternalSignalsModule(Logic a, Logic b) @@ -103,8 +113,8 @@ class FlopModule extends Module { } /// A custom [FlipFlop] used to verify inheritance-aware leaf matching. -class CustomFlipFlop extends FlipFlop { - CustomFlipFlop(super.clk, super.d); +class CustomFlipFlop extends FlipFlop { + CustomFlipFlop(super.clk, super.d) : super.scalar(); } /// Exercises flip-flops with optional control signals. @@ -141,6 +151,39 @@ class ControlledFlopModule extends Module { } } +enum _NetlistResetState { idle, active, done } + +LogicEnum<_NetlistResetState> _netlistResetState({String? name}) => + LogicEnum<_NetlistResetState>.withMapping( + { + _NetlistResetState.idle: 0, + _NetlistResetState.active: 2, + _NetlistResetState.done: 3, + }, + width: 2, + name: name, + ); + +class EnumResetFlopModule extends Module { + EnumResetFlopModule( + Logic clk, + Logic reset, { + required bool asyncReset, + }) : super(name: 'enumresetflop') { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + final d = addTypedInput('d', _netlistResetState(name: 'd')); + final q = flop( + clk, + d, + reset: reset, + resetValue: _NetlistResetState.active, + asyncReset: asyncReset, + ); + addTypedOutput('q', q.clone).gets(q); + } +} + /// Exercises Add. class AddModule extends Module { Logic get sum => output('sum'); @@ -452,6 +495,26 @@ class StructOutputProducerModule extends Module { } } +/// Applies structure-preserving primitive operations to an aggregate port. +class TypedStructOperationModule extends Module { + TypedStructOperationModule( + Logic clk, + Logic control, + NetlistPairStruct first, + NetlistPairStruct second, + ) : super(name: 'typedstructoperation') { + clk = addInput('clk', clk); + control = addInput('control', control); + first = addTypedInput('first', first); + second = addTypedInput('second', second); + + final selected = Mux(control, second, first).out; + final forwarded = Passthrough(selected).out; + final registered = FlipFlop(clk, forwarded); + addTypedOutput('out', first.clone) <= registered.q; + } +} + /// Instantiates identical structure-packing children at distinct parent paths. class StructOutputProducerDedupTop extends Module { StructOutputProducerDedupTop() : super(name: 'structoutputproducerdeduptop') { @@ -853,6 +916,38 @@ void main() { expect(replacementIds, isNot(equals(storageIds))); }); + test('shared packed-array rewrite applies before backend rendering', + () async { + final module = PackedInputArrayOutputModule(Logic(width: 8)); + await module.build(); + + void expectSharedRewrite(SynthModuleDefinition definition) { + final input = + definition.inputs.singleWhere((signal) => signal.name == 'data'); + final output = + definition.outputs.singleWhere((signal) => signal.name == 'out'); + expect( + definition.assignments.any( + (assignment) => + identical(assignment.src.resolved, input.resolved) && + identical(assignment.dst.resolved, output.resolved), + ), + isTrue, + ); + + final subsets = definition.subModuleInstantiations + .where((instantiation) => instantiation.module is BusSubset) + .toList(); + expect(subsets, isNotEmpty); + expect( + subsets.every((instantiation) => !instantiation.needsInstantiation), + isTrue); + } + + expectSharedRewrite(SynthModuleDefinition(module)); + expectSharedRewrite(NetlistSynthModuleDefinition(module)); + }); + for (final unpackedDimensions in [0, 2]) { test('split packed child drives permuted array bits $unpackedDimensions', () async { @@ -971,6 +1066,45 @@ void main() { expect(_hasCellType(json, r'$dff'), isTrue); }); + test('typed structure operations map to aggregate primitive cells', + () async { + final module = TypedStructOperationModule( + SimpleClockGenerator(10).clk, + Logic(), + NetlistPairStruct(name: 'first'), + NetlistPairStruct(name: 'second'), + ); + final json = await _synthToMap(module); + final moduleDef = + _modules(json)[module.definitionName] as Map; + final cells = _cells(moduleDef).values.cast>(); + + for (final cellType in [r'$mux', r'$buf', r'$dff']) { + final matchingCells = + cells.where((candidate) => candidate['type'] == cellType).toList(); + expect( + matchingCells, + hasLength(1), + reason: cells.map((candidate) => candidate['type']).join(', '), + ); + final cell = matchingCells.single; + final parameters = cell['parameters'] as Map; + final connections = cell['connections'] as Map; + if (cellType == r'$buf') { + expect(parameters['A_WIDTH'], 8); + expect(parameters['Y_WIDTH'], 8); + expect(connections['A'] as List, hasLength(8)); + expect(connections['Y'] as List, hasLength(8)); + } else { + expect(parameters['WIDTH'], 8); + final dataPorts = cellType == r'$mux' ? ['A', 'B', 'Y'] : ['D', 'Q']; + for (final port in dataPorts) { + expect(connections[port] as List, hasLength(8)); + } + } + } + }); + test('FlipFlop controls map to standard Yosys register cells', () async { final clk = SimpleClockGenerator(10).clk; final d = Logic(width: 4); @@ -1131,6 +1265,41 @@ void main() { ); }); + test('LogicEnum constant resets preserve mapped netlist parameters', + () async { + final cases = [ + ( + asyncReset: false, + type: r'$sdff', + parameter: 'SRST_VALUE', + ), + ( + asyncReset: true, + type: r'$adff', + parameter: 'ARST_VALUE', + ), + ]; + + for (final testCase in cases) { + final module = EnumResetFlopModule( + SimpleClockGenerator(10).clk, + Logic(), + asyncReset: testCase.asyncReset, + ); + final json = await _synthToMap(module); + final moduleDef = + _modules(json)[module.definitionName] as Map; + final cell = + _cells(moduleDef).values.cast>().singleWhere( + (candidate) => candidate['type'] == testCase.type, + ); + final parameters = cell['parameters'] as Map; + + expect(parameters['WIDTH'], 2); + expect(parameters[testCase.parameter], '10'); + } + }); + test(r'Add maps to $add cell', () async { final json = await _synthToMap( AddModule(Logic(width: 8), Logic(width: 8)), diff --git a/test/pair_interface_hier_w_modify_test.dart b/test/pair_interface_hier_uniquify_test.dart similarity index 98% rename from test/pair_interface_hier_w_modify_test.dart rename to test/pair_interface_hier_uniquify_test.dart index 57756dc76a..8cd354aa28 100644 --- a/test/pair_interface_hier_w_modify_test.dart +++ b/test/pair_interface_hier_uniquify_test.dart @@ -1,7 +1,7 @@ // Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // -// pair_interface_hier_w_modify_test.dart +// pair_interface_hier_uniquify_test.dart // Tests for PairInterface with hierarchy and explicit uniquification. // // 2023 March 9 diff --git a/test/provider_consumer_w_modify_test.dart b/test/provider_consumer_uniquify_test.dart similarity index 99% rename from test/provider_consumer_w_modify_test.dart rename to test/provider_consumer_uniquify_test.dart index f4d087cc02..fd28d7e5d0 100644 --- a/test/provider_consumer_w_modify_test.dart +++ b/test/provider_consumer_uniquify_test.dart @@ -1,7 +1,7 @@ // Copyright (C) 2023-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // -// provider_consumer_w_modify_test.dart +// provider_consumer_uniquify_test.dart // Tests PairInterface provider and consumer naming with explicit uniquifiers. // // 2023 March 9 diff --git a/test/shuffle_test.dart b/test/shuffle_test.dart index 8ab355decb..85e46bab63 100644 --- a/test/shuffle_test.dart +++ b/test/shuffle_test.dart @@ -1,4 +1,4 @@ -// Copyright (C) 2022-2023 Intel Corporation +// Copyright (C) 2022-2026 Intel Corporation // SPDX-License-Identifier: BSD-3-Clause // // shuffle_test.dart @@ -8,7 +8,6 @@ // Author: Max Korbel import 'package:rohd/rohd.dart'; -import 'package:rohd/src/modules/passthrough.dart'; import 'package:rohd/src/utilities/simcompare.dart'; import 'package:test/test.dart'; diff --git a/test/submodule_output_mapping_test.dart b/test/submodule_output_mapping_test.dart index c037675277..15fee8cd75 100644 --- a/test/submodule_output_mapping_test.dart +++ b/test/submodule_output_mapping_test.dart @@ -506,424 +506,665 @@ void main() { }); group('independent outputs to packed parent bits', () { - for (final producer in ProducerKind.values) { - for (final order in AssignmentOrder.values) { - // Three bits distinguish both boundaries and an interior position. - for (final width in [2, 3]) { - for (final aliasDepth in [0, 3]) { - test( - '${producer.name} ${order.name} ' - 'width=$width aliases=$aliasDepth', () async { - final module = OutputMappingTop( - width: width, - producer: producer, - order: order, - aliasDepth: aliasDepth, - ); - await module.build(); - final body = topBody(module); - for (var index = 0; index < width; index++) { - expect(body, matches('\\.result\\d*\\(observed\\[$index\\]\\)'), - reason: body); - } - expect(body, isNot(matches(r'assign\s+observed\['))); - - NetlistSynthesizer().synthesizeToJson(module); - expect(topBody(module), body); + // Cover every producer and assignment mechanism while retaining the + // two-bit edge case, three-bit interior bit, and deep alias path. + final configurations = [ + ( + producer: ProducerKind.independent, + order: AssignmentOrder.batch, + width: 2, + aliasDepth: 0, + ), + ( + producer: ProducerKind.multiport, + order: AssignmentOrder.forward, + width: 3, + aliasDepth: 3, + ), + ( + producer: ProducerKind.hierarchical, + order: AssignmentOrder.reverse, + width: 3, + aliasDepth: 0, + ), + ( + producer: ProducerKind.custom, + order: AssignmentOrder.swizzle, + width: 2, + aliasDepth: 3, + ), + ( + producer: ProducerKind.independent, + order: AssignmentOrder.rswizzle, + width: 3, + aliasDepth: 3, + ), + ]; - final vectors = [ - for (final input in twoStateInputPatterns(width)) - Vector({ - 'data': input - }, { - 'observed': LogicValue.ofString([ - for (var index = 0; index < width; index++) - invertedBit(input, index), - ].join()), - }), - ]; - await SimCompare.checkVectors( - module, - vectors, - excludedBackends: producer == ProducerKind.custom - ? const { - SimCompareBackend.systemc: - 'CustomStage provides only a custom SystemVerilog ' - 'definition.', - } - : const {}, - ); - expect(body, isNot(matches(r'logic\s+result\w*;')), - reason: 'Directly mapped outputs should not leave unused ' - 'intermediate declarations.\n$body'); - }); - } + for (final configuration in configurations) { + final producer = configuration.producer; + final order = configuration.order; + final width = configuration.width; + final aliasDepth = configuration.aliasDepth; + test( + '${producer.name} ${order.name} ' + 'width=$width aliases=$aliasDepth', () async { + final module = OutputMappingTop( + width: width, + producer: producer, + order: order, + aliasDepth: aliasDepth, + ); + await module.build(); + final body = topBody(module); + for (var index = 0; index < width; index++) { + expect(body, matches('\\.result\\d*\\(observed\\[$index\\]\\)'), + reason: body); } - } + expect(body, isNot(matches(r'assign\s+observed\['))); + + NetlistSynthesizer().synthesizeToJson(module); + expect(topBody(module), body); + + final vectors = [ + for (final input in twoStateInputPatterns(width)) + Vector({ + 'data': input + }, { + 'observed': LogicValue.ofString([ + for (var index = 0; index < width; index++) + invertedBit(input, index), + ].join()), + }), + ]; + await SimCompare.checkVectors( + module, + vectors, + excludedBackends: producer == ProducerKind.custom + ? const { + SimCompareBackend.systemc: + 'CustomStage provides only a custom SystemVerilog ' + 'definition.', + } + : const {}, + ); + expect(body, isNot(matches(r'logic\s+result\w*;')), + reason: 'Directly mapped outputs should not leave unused ' + 'intermediate declarations.\n$body'); + }); } }); group('concatenated output alias naming', () { - for (final producer in [ProducerKind.independent, ProducerKind.custom]) { - for (final order in [AssignmentOrder.swizzle, AssignmentOrder.rswizzle]) { - for (final naming in Naming.values) { - test('${producer.name} ${order.name} ${naming.name}', () async { - final module = OutputMappingTop( - width: 3, - producer: producer, - order: order, - aliasDepth: 3, - bitNaming: naming, - ); - await module.build(); - final body = topBody(module); - final preserve = - naming == Naming.reserved || naming == Naming.renameable; - for (var index = 0; index < 3; index++) { - if (preserve) { - expect(body, contains('logic bit_alias$index;')); - expect(body, contains('.result(bit_alias$index)')); - } else { - expect(body, contains('.result(observed[$index])'), - reason: body); - expect(body, isNot(contains('logic bit_alias$index;'))); - } - } - NetlistSynthesizer().synthesizeToJson(module); - expect(topBody(module), body); - final vectors = [ - for (final input in inputPatterns(3)) - Vector({ - 'data': input - }, { - 'observed': LogicValue.ofString([ - for (var index = 0; index < 3; index++) - invertedBit(input, index), - ].join()), - }), - ]; - await SimCompare.checkFunctionalVector(module, vectors); - SimCompare.checkIverilogVector(module, vectors); - }); + final configurations = [ + ( + producer: ProducerKind.independent, + order: AssignmentOrder.swizzle, + naming: Naming.reserved, + ), + ( + producer: ProducerKind.independent, + order: AssignmentOrder.rswizzle, + naming: Naming.renameable, + ), + ( + producer: ProducerKind.custom, + order: AssignmentOrder.swizzle, + naming: Naming.mergeable, + ), + ( + producer: ProducerKind.independent, + order: AssignmentOrder.rswizzle, + naming: Naming.mergeable, + ), + ( + producer: ProducerKind.custom, + order: AssignmentOrder.swizzle, + naming: Naming.unnamed, + ), + ]; + + for (final configuration in configurations) { + final producer = configuration.producer; + final order = configuration.order; + final naming = configuration.naming; + test('${producer.name} ${order.name} ${naming.name}', () async { + final module = OutputMappingTop( + width: 3, + producer: producer, + order: order, + aliasDepth: 3, + bitNaming: naming, + ); + await module.build(); + final body = topBody(module); + final preserve = + naming == Naming.reserved || naming == Naming.renameable; + for (var index = 0; index < 3; index++) { + if (preserve) { + expect(body, contains('logic bit_alias$index;')); + expect(body, contains('.result(bit_alias$index)')); + } else { + expect(body, contains('.result(observed[$index])'), reason: body); + expect(body, isNot(contains('logic bit_alias$index;'))); + } } - } + NetlistSynthesizer().synthesizeToJson(module); + expect(topBody(module), body); + final vectors = [ + for (final input in inputPatterns(3)) + Vector({ + 'data': input + }, { + 'observed': LogicValue.ofString([ + for (var index = 0; index < 3; index++) + invertedBit(input, index), + ].join()), + }), + ]; + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); + }); } }); group('output mapping safety cross-products', () { - for (final guard in MappingGuard.values) { - for (final producer in [ - ProducerKind.independent, - ProducerKind.multiport, - ProducerKind.custom, - ]) { - for (final order in [ - AssignmentOrder.batch, - AssignmentOrder.reverse, - if (guard != MappingGuard.partial) ...[ - AssignmentOrder.swizzle, - AssignmentOrder.rswizzle, - ], - ]) { - test('${guard.name} ${producer.name} ${order.name}', () async { - final module = GuardedOutputTop( - guard: guard, - producer: producer, - order: order, - ); - await module.build(); - final body = topBody(module); - expect(body, isNot(matches(r'\.result\d*\(\)')), reason: body); - if (guard == MappingGuard.renameableBus || - guard == MappingGuard.reservedBus || - guard == MappingGuard.partial) { - expect(body, isNot(matches(r'\.result\d*\(observed\['))); - } else { - for (final index in [0, 2]) { - expect(body, - matches('\\.result\\d*\\((observed|mirror)\\[$index\\]\\)'), - reason: body); - } - } - if (guard == MappingGuard.renameableBus || - guard == MappingGuard.reservedBus) { - expect(body, contains('logic [3:0] retained;')); - } - if (guard == MappingGuard.namedBit) { - expect(body, contains('logic retained;')); - } - NetlistSynthesizer().synthesizeToJson(module); - expect(topBody(module), body); + // Each guard represents a distinct condition that prevents direct output + // mapping. Rotate the producer and assignment forms across the guards. + final configurations = [ + ( + guard: MappingGuard.outputFanout, + producer: ProducerKind.independent, + order: AssignmentOrder.batch + ), + ( + guard: MappingGuard.siblingFanout, + producer: ProducerKind.multiport, + order: AssignmentOrder.reverse + ), + ( + guard: MappingGuard.busFanout, + producer: ProducerKind.custom, + order: AssignmentOrder.swizzle + ), + ( + guard: MappingGuard.repeatedBit, + producer: ProducerKind.independent, + order: AssignmentOrder.rswizzle + ), + ( + guard: MappingGuard.wholeBusFanout, + producer: ProducerKind.multiport, + order: AssignmentOrder.batch + ), + ( + guard: MappingGuard.renameableBus, + producer: ProducerKind.custom, + order: AssignmentOrder.reverse + ), + ( + guard: MappingGuard.reservedBus, + producer: ProducerKind.independent, + order: AssignmentOrder.swizzle + ), + ( + guard: MappingGuard.namedBit, + producer: ProducerKind.multiport, + order: AssignmentOrder.rswizzle + ), + ( + guard: MappingGuard.partial, + producer: ProducerKind.custom, + order: AssignmentOrder.reverse + ), + ]; - final vectors = [ - for (final input in inputPatterns(4)) - Vector({ - 'data': input - }, { - 'observed': LogicValue.ofString([ - if (guard == MappingGuard.partial) 'z', - for (var index = 3; index >= 0; index--) - invertedBit( - input, - guard == MappingGuard.repeatedBit && index == 3 - ? 1 - : index), - ].join()), - if (guard == MappingGuard.outputFanout) - 'tap': LogicValue.ofString(invertedBit(input, 1)), - if (guard == MappingGuard.siblingFanout) - 'tap': input[1].isValid ? input[1] : LogicValue.x, - if (guard == MappingGuard.busFanout) - 'secondary': - LogicValue.ofString('10${invertedBit(input, 1)}'), - if (guard == MappingGuard.wholeBusFanout) 'mirror': ~input, - }), - ]; - await SimCompare.checkFunctionalVector(module, vectors); - SimCompare.checkIverilogVector(module, vectors); - }); + for (final configuration in configurations) { + final guard = configuration.guard; + final producer = configuration.producer; + final order = configuration.order; + test('${guard.name} ${producer.name} ${order.name}', () async { + final module = GuardedOutputTop( + guard: guard, + producer: producer, + order: order, + ); + await module.build(); + final body = topBody(module); + expect(body, isNot(matches(r'\.result\d*\(\)')), reason: body); + if (guard == MappingGuard.renameableBus || + guard == MappingGuard.reservedBus || + guard == MappingGuard.partial) { + expect(body, isNot(matches(r'\.result\d*\(observed\['))); + } else { + for (final index in [0, 2]) { + expect(body, + matches('\\.result\\d*\\((observed|mirror)\\[$index\\]\\)'), + reason: body); + } } - } + if (guard == MappingGuard.renameableBus || + guard == MappingGuard.reservedBus) { + expect(body, contains('logic [3:0] retained;')); + } + if (guard == MappingGuard.namedBit) { + expect(body, contains('logic retained;')); + } + NetlistSynthesizer().synthesizeToJson(module); + expect(topBody(module), body); + + final vectors = [ + for (final input in inputPatterns(4)) + Vector({ + 'data': input + }, { + 'observed': LogicValue.ofString([ + if (guard == MappingGuard.partial) 'z', + for (var index = 3; index >= 0; index--) + invertedBit( + input, + guard == MappingGuard.repeatedBit && index == 3 + ? 1 + : index), + ].join()), + if (guard == MappingGuard.outputFanout) + 'tap': LogicValue.ofString(invertedBit(input, 1)), + if (guard == MappingGuard.siblingFanout) + 'tap': input[1].isValid ? input[1] : LogicValue.x, + if (guard == MappingGuard.busFanout) + 'secondary': LogicValue.ofString('10${invertedBit(input, 1)}'), + if (guard == MappingGuard.wholeBusFanout) 'mirror': ~input, + }), + ]; + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); + }); } }); group('custom output declaration retention', () { - for (final acceptsEmptyPortConnections in [false, true]) { - for (final fanout in [false, true]) { - for (final order in AssignmentOrder.values) { - test( - 'empty=$acceptsEmptyPortConnections ' - 'fanout=$fanout ${order.name}', () async { - final module = CustomRetentionTop( - acceptsEmptyPortConnections: acceptsEmptyPortConnections, - fanout: fanout, - order: order, - ); - await module.build(); - final body = topBody(module); - String connection(int index, String target) => - '.result$index($target)'; + final configurations = [ + ( + acceptsEmptyPortConnections: false, + fanout: false, + order: AssignmentOrder.batch, + ), + ( + acceptsEmptyPortConnections: true, + fanout: false, + order: AssignmentOrder.forward, + ), + ( + acceptsEmptyPortConnections: false, + fanout: true, + order: AssignmentOrder.reverse, + ), + ( + acceptsEmptyPortConnections: true, + fanout: true, + order: AssignmentOrder.swizzle, + ), + ( + acceptsEmptyPortConnections: false, + fanout: true, + order: AssignmentOrder.rswizzle, + ), + ]; - expect(body, contains(connection(0, 'observed[0]'))); - expect(body, isNot(contains('logic result0;'))); - if (fanout) { - expect(body, contains('logic result1;')); - expect(body, contains(connection(1, 'result1'))); - } else { - expect(body, isNot(contains('logic result1;'))); - expect(body, contains(connection(1, 'observed[1]'))); - } - if (acceptsEmptyPortConnections) { - expect(body, contains('.result2()')); - expect(body, isNot(contains('logic result2;'))); - } else { - expect(body, contains(connection(2, 'result2'))); - expect(body, contains('logic result2;')); - } - NetlistSynthesizer().synthesizeToJson(module); - expect(topBody(module), body); + for (final configuration in configurations) { + final acceptsEmptyPortConnections = + configuration.acceptsEmptyPortConnections; + final fanout = configuration.fanout; + final order = configuration.order; + test( + 'empty=$acceptsEmptyPortConnections ' + 'fanout=$fanout ${order.name}', () async { + final module = CustomRetentionTop( + acceptsEmptyPortConnections: acceptsEmptyPortConnections, + fanout: fanout, + order: order, + ); + await module.build(); + final body = topBody(module); + String connection(int index, String target) => '.result$index($target)'; - final vectors = [ - for (final input in inputPatterns(3)) - Vector({ - 'data': input - }, { - 'observed': ~input.getRange(0, 2), - if (fanout) 'tap': input[1].isValid ? input[1] : LogicValue.x, - }), - ]; - await SimCompare.checkFunctionalVector(module, vectors); - SimCompare.checkIverilogVector(module, vectors); - }); + expect(body, contains(connection(0, 'observed[0]'))); + expect(body, isNot(contains('logic result0;'))); + if (fanout) { + expect(body, contains('logic result1;')); + expect(body, contains(connection(1, 'result1'))); + } else { + expect(body, isNot(contains('logic result1;'))); + expect(body, contains(connection(1, 'observed[1]'))); } - } + if (acceptsEmptyPortConnections) { + expect(body, contains('.result2()')); + expect(body, isNot(contains('logic result2;'))); + } else { + expect(body, contains(connection(2, 'result2'))); + expect(body, contains('logic result2;')); + } + NetlistSynthesizer().synthesizeToJson(module); + expect(topBody(module), body); + + final vectors = [ + for (final input in inputPatterns(3)) + Vector({ + 'data': input + }, { + 'observed': ~input.getRange(0, 2), + if (fanout) 'tap': input[1].isValid ? input[1] : LogicValue.x, + }), + ]; + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); + }); } }); group('constant position and four-state cross-products', () { - for (final constant in ['0', '1', 'x', 'z']) { - for (final constantIndex in [0, 1, 2]) { - for (final namedConstant in [false, true]) { - for (final order in [ - AssignmentOrder.batch, - AssignmentOrder.reverse, - AssignmentOrder.swizzle, - AssignmentOrder.rswizzle, - ]) { - test( - '$constant at $constantIndex ' - 'named=$namedConstant ${order.name}', () async { - final module = ConstantOutputTop( - constant: constant, - constantIndex: constantIndex, - namedConstant: namedConstant, - order: order, - ); - await module.build(); - final body = topBody(module); - expect(body, isNot(contains('.result()')), reason: body); - if (constant != 'z') { - for (var index = 0; - index < module.output('observed').width; - index++) { - if (index != constantIndex) { - expect(body, contains('.result(observed[$index])'), - reason: body); - } - } - } - NetlistSynthesizer().synthesizeToJson(module); - expect(topBody(module), body); - final vectors = [ - for (final input in inputPatterns(module.input('data').width)) - Vector({ - 'data': input - }, { - 'observed': LogicValue.ofString([ - for (var index = module.output('observed').width - 1; - index >= 0; - index--) - if (index == constantIndex) - constant - else - invertedBit( - input, index > constantIndex ? index - 1 : index), - ].join()), - }), - ]; - await SimCompare.checkFunctionalVector(module, vectors); - SimCompare.checkIverilogVector(module, vectors); - }); + // Retain each constant state, destination position, naming behavior, and + // assignment mechanism. Z remains covered at every position because it + // follows the distinct floating-output pruning path. + final configurations = [ + ( + constant: '0', + constantIndex: 0, + namedConstant: false, + order: AssignmentOrder.batch, + ), + ( + constant: '1', + constantIndex: 1, + namedConstant: true, + order: AssignmentOrder.reverse, + ), + ( + constant: 'x', + constantIndex: 2, + namedConstant: false, + order: AssignmentOrder.swizzle, + ), + ( + constant: 'z', + constantIndex: 0, + namedConstant: true, + order: AssignmentOrder.rswizzle, + ), + ( + constant: 'z', + constantIndex: 1, + namedConstant: false, + order: AssignmentOrder.batch, + ), + ( + constant: 'z', + constantIndex: 2, + namedConstant: true, + order: AssignmentOrder.reverse, + ), + ]; + + for (final configuration in configurations) { + final constant = configuration.constant; + final constantIndex = configuration.constantIndex; + final namedConstant = configuration.namedConstant; + final order = configuration.order; + test( + '$constant at $constantIndex ' + 'named=$namedConstant ${order.name}', () async { + final module = ConstantOutputTop( + constant: constant, + constantIndex: constantIndex, + namedConstant: namedConstant, + order: order, + ); + await module.build(); + final body = topBody(module); + expect(body, isNot(contains('.result()')), reason: body); + if (constant != 'z') { + for (var index = 0; + index < module.output('observed').width; + index++) { + if (index != constantIndex) { + expect(body, contains('.result(observed[$index])'), reason: body); + } } } - } + NetlistSynthesizer().synthesizeToJson(module); + expect(topBody(module), body); + final vectors = [ + for (final input in inputPatterns(module.input('data').width)) + Vector({ + 'data': input + }, { + 'observed': LogicValue.ofString([ + for (var index = module.output('observed').width - 1; + index >= 0; + index--) + if (index == constantIndex) + constant + else + invertedBit( + input, index > constantIndex ? index - 1 : index), + ].join()), + }), + ]; + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); + }); } }); group('scalar producers beside packed ranges', () { - for (final wideProducer in [false, true]) { - for (final fanout in [false, true]) { - for (final order in AssignmentOrder.values) { - test('wide=$wideProducer fanout=$fanout ${order.name}', () async { - final module = MixedRangeTop( - wideProducer: wideProducer, - fanout: fanout, - order: order, - ); - await module.build(); - final body = topBody(module); - for (final index in [0, 4]) { - expect(body, contains('.result(observed[$index])'), reason: body); - } - expect(body, isNot(contains('.wide_result()'))); - NetlistSynthesizer().synthesizeToJson(module); - expect(topBody(module), body); - final vectors = [ - for (final input in inputPatterns(5)) - Vector({ - 'data': input - }, { - 'observed': LogicValue.ofString([ - for (var index = 4; index >= 0; index--) - if (wideProducer || index == 0 || index == 4) - invertedBit(input, index) - else - input[index].toString(includeWidth: false), - ].join()), - if (fanout) - 'tap': wideProducer - ? ~input.getRange(1, 4) - : input.getRange(1, 4), - }), - ]; - await SimCompare.checkFunctionalVector(module, vectors); - SimCompare.checkIverilogVector(module, vectors); - }); + final configurations = [ + ( + wideProducer: false, + fanout: false, + order: AssignmentOrder.batch, + ), + ( + wideProducer: true, + fanout: false, + order: AssignmentOrder.forward, + ), + ( + wideProducer: false, + fanout: true, + order: AssignmentOrder.reverse, + ), + ( + wideProducer: true, + fanout: true, + order: AssignmentOrder.swizzle, + ), + ( + wideProducer: true, + fanout: false, + order: AssignmentOrder.rswizzle, + ), + ]; + + for (final configuration in configurations) { + final wideProducer = configuration.wideProducer; + final fanout = configuration.fanout; + final order = configuration.order; + test('wide=$wideProducer fanout=$fanout ${order.name}', () async { + final module = MixedRangeTop( + wideProducer: wideProducer, + fanout: fanout, + order: order, + ); + await module.build(); + final body = topBody(module); + for (final index in [0, 4]) { + expect(body, contains('.result(observed[$index])'), reason: body); } - } + expect(body, isNot(contains('.wide_result()'))); + NetlistSynthesizer().synthesizeToJson(module); + expect(topBody(module), body); + final vectors = [ + for (final input in inputPatterns(5)) + Vector({ + 'data': input + }, { + 'observed': LogicValue.ofString([ + for (var index = 4; index >= 0; index--) + if (wideProducer || index == 0 || index == 4) + invertedBit(input, index) + else + input[index].toString(includeWidth: false), + ].join()), + if (fanout) + 'tap': + wideProducer ? ~input.getRange(1, 4) : input.getRange(1, 4), + }), + ]; + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); + }); } }); group('sliced swizzle equivalence cross-products', () { - for (final producer in ProducerKind.values) { - for (final order in [AssignmentOrder.swizzle, AssignmentOrder.rswizzle]) { - for (final naming in Naming.values) { - for (final use in SwizzleUse.values) { - for (final fanout in [false, true]) { - for (final (lower, upper) in [(0, 1), (1, 3), (3, 4)]) { - // Cover all names on independent producers; exercise other - // producer kinds with mergeable names to bound the matrix. - if (producer != ProducerKind.independent && - naming != Naming.mergeable) { - continue; - } - // The middle slice covers naming and producer variations; - // boundary slices still cover every use, order, and fanout. - if (lower != 1 && - (producer != ProducerKind.independent || - naming != Naming.mergeable)) { - continue; - } - test( - '${producer.name} ${order.name} ${naming.name} ' - '${use.name} fanout=$fanout [$lower:$upper]', () async { - final module = SlicedSwizzleTop( - producer: producer, - order: order, - naming: naming, - use: use, - fanout: fanout, - lower: lower, - upper: upper, - ); - await module.build(); - final body = topBody(module); - printOnFailure(body); - NetlistSynthesizer().synthesizeToJson(module); - expect(topBody(module), body); - final vectors = [ - for (final input in fourStateInputPatterns(4, - exhaustive: producer == ProducerKind.independent && - order == AssignmentOrder.rswizzle && - naming == Naming.mergeable && - use == SwizzleUse.wholeAndSlice && - fanout)) - Vector({ - 'data': input - }, { - 'observed': LogicValue.ofString([ - if (use == SwizzleUse.partialDestination) 'z', - if (use == SwizzleUse.filledDestination) '1', - for (var bit = upper - 1; bit >= lower; bit--) - invertedBit( - input, - order == AssignmentOrder.swizzle - ? 3 - bit - : bit), - if (use == SwizzleUse.partialDestination) 'z', - if (use == SwizzleUse.filledDestination) '0', - ].join()), - if (use == SwizzleUse.wholeAndSlice) - 'whole': LogicValue.ofString([ - for (var bit = 3; bit >= 0; bit--) - invertedBit( - input, - order == AssignmentOrder.swizzle - ? 3 - bit - : bit), - ].join()), - if (fanout) - 'tap': input[1].isValid ? input[1] : LogicValue.x, - }), - ]; - await SimCompare.checkFunctionalVector(module, vectors); - SimCompare.checkIverilogVector(module, vectors); - }); - } - } - } - } - } + // Retain each naming policy, producer topology, destination form, slice + // boundary, and fanout path. The mergeable whole-and-slice case remains + // exhaustive over all four-state inputs. + final configurations = [ + ( + producer: ProducerKind.independent, + order: AssignmentOrder.swizzle, + naming: Naming.reserved, + use: SwizzleUse.slice, + fanout: false, + lower: 0, + upper: 1, + exhaustive: false, + ), + ( + producer: ProducerKind.independent, + order: AssignmentOrder.rswizzle, + naming: Naming.renameable, + use: SwizzleUse.wholeAndSlice, + fanout: true, + lower: 1, + upper: 3, + exhaustive: false, + ), + ( + producer: ProducerKind.independent, + order: AssignmentOrder.rswizzle, + naming: Naming.mergeable, + use: SwizzleUse.wholeAndSlice, + fanout: true, + lower: 1, + upper: 3, + exhaustive: true, + ), + ( + producer: ProducerKind.independent, + order: AssignmentOrder.swizzle, + naming: Naming.mergeable, + use: SwizzleUse.partialDestination, + fanout: false, + lower: 3, + upper: 4, + exhaustive: false, + ), + ( + producer: ProducerKind.independent, + order: AssignmentOrder.rswizzle, + naming: Naming.unnamed, + use: SwizzleUse.filledDestination, + fanout: true, + lower: 0, + upper: 1, + exhaustive: false, + ), + ( + producer: ProducerKind.multiport, + order: AssignmentOrder.swizzle, + naming: Naming.mergeable, + use: SwizzleUse.slice, + fanout: true, + lower: 1, + upper: 3, + exhaustive: false, + ), + ( + producer: ProducerKind.hierarchical, + order: AssignmentOrder.rswizzle, + naming: Naming.mergeable, + use: SwizzleUse.wholeAndSlice, + fanout: false, + lower: 1, + upper: 3, + exhaustive: false, + ), + ( + producer: ProducerKind.custom, + order: AssignmentOrder.swizzle, + naming: Naming.mergeable, + use: SwizzleUse.partialDestination, + fanout: true, + lower: 1, + upper: 3, + exhaustive: false, + ), + ]; + + for (final configuration in configurations) { + final producer = configuration.producer; + final order = configuration.order; + final naming = configuration.naming; + final use = configuration.use; + final fanout = configuration.fanout; + final lower = configuration.lower; + final upper = configuration.upper; + final exhaustive = configuration.exhaustive; + test( + '${producer.name} ${order.name} ${naming.name} ' + '${use.name} fanout=$fanout [$lower:$upper]', () async { + final module = SlicedSwizzleTop( + producer: producer, + order: order, + naming: naming, + use: use, + fanout: fanout, + lower: lower, + upper: upper, + ); + await module.build(); + final body = topBody(module); + printOnFailure(body); + NetlistSynthesizer().synthesizeToJson(module); + expect(topBody(module), body); + final vectors = [ + for (final input in fourStateInputPatterns(4, exhaustive: exhaustive)) + Vector({ + 'data': input + }, { + 'observed': LogicValue.ofString([ + if (use == SwizzleUse.partialDestination) 'z', + if (use == SwizzleUse.filledDestination) '1', + for (var bit = upper - 1; bit >= lower; bit--) + invertedBit( + input, order == AssignmentOrder.swizzle ? 3 - bit : bit), + if (use == SwizzleUse.partialDestination) 'z', + if (use == SwizzleUse.filledDestination) '0', + ].join()), + if (use == SwizzleUse.wholeAndSlice) + 'whole': LogicValue.ofString([ + for (var bit = 3; bit >= 0; bit--) + invertedBit(input, + order == AssignmentOrder.swizzle ? 3 - bit : bit), + ].join()), + if (fanout) 'tap': input[1].isValid ? input[1] : LogicValue.x, + }), + ]; + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); + }); } }); @@ -971,196 +1212,311 @@ void main() { }); group('range operand equivalence cross-products', () { - for (final producer in [ProducerKind.independent, ProducerKind.custom]) { - for (final order in [AssignmentOrder.swizzle, AssignmentOrder.rswizzle]) { - for (final naming in Naming.values) { - for (final ranges in OperandRanges.values) { - for (final fanout in [false, true]) { - for (final constant in [ - '0', - '1', - 'x', - 'z', - ]) { - for (final constantPosition in [0, 1, -1]) { - // Cover every topology/name/fanout combination with middle Z. - final topologyCase = constant == 'z' && constantPosition == 1; - // Sweep all constant states and positions on one topology. - final constantCase = producer == ProducerKind.independent && - naming == Naming.mergeable && - !fanout; - // Keep all preserved-Z positions for the pruning regression. - final preservedFloatingCase = constant == 'z' && - (naming == Naming.reserved || - naming == Naming.renameable) && - !fanout; - // Bound runtime by omitting the remaining cross-products. - if (!topologyCase && - !constantCase && - !preservedFloatingCase) { - continue; - } - test( - '${producer.name} ${order.name} ${naming.name} ' - '${ranges.name} fanout=$fanout ' - '$constant at $constantPosition', () async { - final module = RangeOperandSwizzleTop( - producer: producer, - order: order, - naming: naming, - ranges: ranges, - constant: constant, - constantPosition: constantPosition, - fanout: fanout, - ); - await module.build(); - final body = topBody(module); - printOnFailure(body); - if (naming == Naming.reserved || - naming == Naming.renameable) { - expect(body, contains('wide_alias')); - expect(body, contains('range0')); - } else if (constantPosition != -1) { - expect(body, matches(r'\.result\(observed\[\d+\]\)')); - } - if (topologyCase) { - NetlistSynthesizer().synthesizeToJson(module); - expect(topBody(module), body); - } - final sourceIndices = switch (ranges) { - OperandRanges.slice => [ - [1, 2] - ], - OperandRanges.reordered => [ - [2, 3], - [0, 1] - ], - OperandRanges.overlapping => [ - [0, 1], - [1, 2] - ], - OperandRanges.nested => [ - [2] - ], - }; - final vectors = []; - for (final input in fourStateInputPatterns(4, - exhaustive: producer == ProducerKind.custom && - order == AssignmentOrder.rswizzle && - naming == Naming.mergeable && - !fanout && - constant == 'z' && - constantPosition == 1)) { - final expectedOperands = [ - [invertedBit(input, 0)], - for (final indices in sourceIndices) - [ - for (final index in indices) - invertedBit(input, index) - ], - [invertedBit(input, 3)], - ]; - expectedOperands.insert( - constantPosition < 0 - ? expectedOperands.length - : constantPosition, - [constant]); - final expectedBits = (order == AssignmentOrder.swizzle - ? expectedOperands.reversed - : expectedOperands) - .expand((operand) => operand) - .toList(); - vectors.add(Vector({ - 'data': input - }, { - 'observed': - LogicValue.ofString(expectedBits.reversed.join()), - if (fanout) ...{ - 'wide_tap': ~input, - 'bit_tap': input[0].isValid ? input[0] : LogicValue.x, - 'window': LogicValue.ofString(expectedBits - .sublist(1, expectedBits.length - 1) - .reversed - .join()), - }, - })); - } - await SimCompare.checkFunctionalVector(module, vectors); - SimCompare.checkIverilogVector(module, vectors); - }); - } - } - } - } + // Preserve every range topology, naming mode, constant state and + // placement. Reserved and renameable names keep Z at all positions because + // those are the pruning-sensitive cases. + final configurations = [ + ( + producer: ProducerKind.independent, + order: AssignmentOrder.swizzle, + naming: Naming.reserved, + ranges: OperandRanges.slice, + constant: 'z', + constantPosition: 0, + fanout: false, + exhaustive: false, + ), + ( + producer: ProducerKind.custom, + order: AssignmentOrder.rswizzle, + naming: Naming.reserved, + ranges: OperandRanges.reordered, + constant: 'z', + constantPosition: 1, + fanout: true, + exhaustive: false, + ), + ( + producer: ProducerKind.independent, + order: AssignmentOrder.swizzle, + naming: Naming.reserved, + ranges: OperandRanges.overlapping, + constant: 'z', + constantPosition: -1, + fanout: false, + exhaustive: false, + ), + ( + producer: ProducerKind.custom, + order: AssignmentOrder.rswizzle, + naming: Naming.renameable, + ranges: OperandRanges.nested, + constant: 'z', + constantPosition: 0, + fanout: true, + exhaustive: false, + ), + ( + producer: ProducerKind.independent, + order: AssignmentOrder.swizzle, + naming: Naming.renameable, + ranges: OperandRanges.slice, + constant: 'z', + constantPosition: 1, + fanout: false, + exhaustive: false, + ), + ( + producer: ProducerKind.custom, + order: AssignmentOrder.rswizzle, + naming: Naming.renameable, + ranges: OperandRanges.reordered, + constant: 'z', + constantPosition: -1, + fanout: true, + exhaustive: false, + ), + ( + producer: ProducerKind.independent, + order: AssignmentOrder.swizzle, + naming: Naming.mergeable, + ranges: OperandRanges.overlapping, + constant: '0', + constantPosition: 0, + fanout: false, + exhaustive: false, + ), + ( + producer: ProducerKind.custom, + order: AssignmentOrder.rswizzle, + naming: Naming.mergeable, + ranges: OperandRanges.nested, + constant: '1', + constantPosition: 1, + fanout: true, + exhaustive: false, + ), + ( + producer: ProducerKind.independent, + order: AssignmentOrder.swizzle, + naming: Naming.mergeable, + ranges: OperandRanges.reordered, + constant: 'x', + constantPosition: -1, + fanout: false, + exhaustive: false, + ), + ( + producer: ProducerKind.custom, + order: AssignmentOrder.rswizzle, + naming: Naming.unnamed, + ranges: OperandRanges.slice, + constant: 'z', + constantPosition: 1, + fanout: true, + exhaustive: false, + ), + ( + producer: ProducerKind.custom, + order: AssignmentOrder.rswizzle, + naming: Naming.mergeable, + ranges: OperandRanges.overlapping, + constant: 'z', + constantPosition: 1, + fanout: false, + exhaustive: true, + ), + ]; + + for (final configuration in configurations) { + final producer = configuration.producer; + final order = configuration.order; + final naming = configuration.naming; + final ranges = configuration.ranges; + final constant = configuration.constant; + final constantPosition = configuration.constantPosition; + final fanout = configuration.fanout; + final exhaustive = configuration.exhaustive; + test( + '${producer.name} ${order.name} ${naming.name} ' + '${ranges.name} fanout=$fanout ' + '$constant at $constantPosition', () async { + final module = RangeOperandSwizzleTop( + producer: producer, + order: order, + naming: naming, + ranges: ranges, + constant: constant, + constantPosition: constantPosition, + fanout: fanout, + ); + await module.build(); + final body = topBody(module); + printOnFailure(body); + if (naming == Naming.reserved || naming == Naming.renameable) { + expect(body, contains('wide_alias')); + expect(body, contains('range0')); + } else if (constantPosition != -1) { + expect(body, matches(r'\.result\(observed\[\d+\]\)')); } - } + if (constant == 'z' && constantPosition == 1) { + NetlistSynthesizer().synthesizeToJson(module); + expect(topBody(module), body); + } + final sourceIndices = switch (ranges) { + OperandRanges.slice => [ + [1, 2] + ], + OperandRanges.reordered => [ + [2, 3], + [0, 1] + ], + OperandRanges.overlapping => [ + [0, 1], + [1, 2] + ], + OperandRanges.nested => [ + [2] + ], + }; + final vectors = []; + for (final input in fourStateInputPatterns(4, exhaustive: exhaustive)) { + final expectedOperands = [ + [invertedBit(input, 0)], + for (final indices in sourceIndices) + [ + for (final index in indices) invertedBit(input, index), + ], + [invertedBit(input, 3)], + ]; + expectedOperands.insert( + constantPosition < 0 ? expectedOperands.length : constantPosition, + [constant]); + final expectedBits = (order == AssignmentOrder.swizzle + ? expectedOperands.reversed + : expectedOperands) + .expand((operand) => operand) + .toList(); + vectors.add(Vector({ + 'data': input + }, { + 'observed': LogicValue.ofString(expectedBits.reversed.join()), + if (fanout) ...{ + 'wide_tap': ~input, + 'bit_tap': input[0].isValid ? input[0] : LogicValue.x, + 'window': LogicValue.ofString(expectedBits + .sublist(1, expectedBits.length - 1) + .reversed + .join()), + }, + })); + } + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); + }); } }); group('packed word boundaries and deep aliases', () { - for (final (width, order) in [ - for (final width in [33, 65]) - for (final order in [ - AssignmentOrder.reverse, - AssignmentOrder.swizzle, - AssignmentOrder.rswizzle, - ]) - (width, order), - ]) { - for (final producer in [ - ProducerKind.independent, - ProducerKind.multiport - ]) { - for (final aliasDepth in [0, 16]) { - test( - 'width=$width ${producer.name} ${order.name} aliases=$aliasDepth', - () async { - final module = OutputMappingTop( - width: width, - producer: producer, - order: order, - aliasDepth: aliasDepth, - ); - await module.build(); - final body = topBody(module); - for (var index = 0; index < width; index++) { - expect(body, matches('\\.result\\d*\\(observed\\[$index\\]\\)'), - reason: body); - } - NetlistSynthesizer().synthesizeToJson(module); - expect(topBody(module), body); - final inputs = [ - LogicValue.ofBigInt(BigInt.zero, width), - LogicValue.ofBigInt((BigInt.one << width) - BigInt.one, width), - for (var index = 0; index < width; index++) - LogicValue.ofBigInt(BigInt.one << index, width), - for (final index in [ - 31, - 32, - if (width > 64) ...[63, 64] - ]) - for (final state in ['x', 'z']) - LogicValue.ofString([ - for (var bit = width - 1; bit >= 0; bit--) - if (bit == index) state else (bit % 2).toString(), - ].join()), - ]; - final vectors = [ - for (final input in inputs) - Vector({ - 'data': input - }, { - 'observed': LogicValue.ofString([ - for (var index = 0; index < width; index++) - invertedBit(input, index), - ].join()), - }), - ]; - await SimCompare.checkFunctionalVector(module, vectors); - SimCompare.checkIverilogVector(module, vectors); - }); + // Cover every pair of word boundary, assignment order, producer form, and + // alias depth without compiling their full Cartesian product. + final configurations = [ + ( + width: 33, + order: AssignmentOrder.reverse, + producer: ProducerKind.independent, + aliasDepth: 0, + ), + ( + width: 33, + order: AssignmentOrder.swizzle, + producer: ProducerKind.multiport, + aliasDepth: 16, + ), + ( + width: 33, + order: AssignmentOrder.rswizzle, + producer: ProducerKind.independent, + aliasDepth: 16, + ), + ( + width: 65, + order: AssignmentOrder.reverse, + producer: ProducerKind.multiport, + aliasDepth: 16, + ), + ( + width: 65, + order: AssignmentOrder.swizzle, + producer: ProducerKind.independent, + aliasDepth: 0, + ), + ( + width: 65, + order: AssignmentOrder.rswizzle, + producer: ProducerKind.multiport, + aliasDepth: 0, + ), + ]; + + for (final configuration in configurations) { + final width = configuration.width; + final order = configuration.order; + final producer = configuration.producer; + final aliasDepth = configuration.aliasDepth; + test('width=$width ${producer.name} ${order.name} aliases=$aliasDepth', + () async { + final module = OutputMappingTop( + width: width, + producer: producer, + order: order, + aliasDepth: aliasDepth, + ); + await module.build(); + final body = topBody(module); + for (var index = 0; index < width; index++) { + expect(body, matches('\\.result\\d*\\(observed\\[$index\\]\\)'), + reason: body); } - } + NetlistSynthesizer().synthesizeToJson(module); + expect(topBody(module), body); + final boundaryBits = { + 0, + width - 1, + 31, + 32, + if (width > 64) ...[63, 64], + }; + final inputs = [ + LogicValue.ofBigInt(BigInt.zero, width), + LogicValue.ofBigInt((BigInt.one << width) - BigInt.one, width), + for (final index in boundaryBits) + LogicValue.ofBigInt(BigInt.one << index, width), + for (final index in [ + 31, + 32, + if (width > 64) ...[63, 64] + ]) + for (final state in ['x', 'z']) + LogicValue.ofString([ + for (var bit = width - 1; bit >= 0; bit--) + if (bit == index) state else (bit % 2).toString(), + ].join()), + ]; + final vectors = [ + for (final input in inputs) + Vector({ + 'data': input + }, { + 'observed': LogicValue.ofString([ + for (var index = 0; index < width; index++) + invertedBit(input, index), + ].join()), + }), + ]; + await SimCompare.checkFunctionalVector(module, vectors); + SimCompare.checkIverilogVector(module, vectors); + }); } }); diff --git a/test/sv_gen_test.dart b/test/sv_gen_test.dart index 7f95bb305f..400d760766 100644 --- a/test/sv_gen_test.dart +++ b/test/sv_gen_test.dart @@ -467,6 +467,46 @@ class ModWithPartialArrayAssignment extends Module { } } +class PackedArrayToLogic extends Module { + PackedArrayToLogic(LogicArray array) { + array = addInputArray( + 'array', + array, + dimensions: array.dimensions, + elementWidth: array.elementWidth, + numUnpackedDimensions: array.numUnpackedDimensions, + ); + addOutput('out', width: array.width) <= array; + } +} + +class LogicToPackedArray extends Module { + LogicToPackedArray(Logic data) { + data = addInput('data', data, width: 8); + addOutputArray('out', dimensions: [4], elementWidth: 2) <= data; + } +} + +class ComputedLogicToPackedArray extends Module { + ComputedLogicToPackedArray(Logic data) { + data = addInput('data', data, width: 8); + final computed = (data + Const(1, width: 8)).named('computed'); + addOutputArray('out', dimensions: [4], elementWidth: 2) <= computed; + } +} + +class ReversedPackedArrayToLogic extends Module { + ReversedPackedArrayToLogic(LogicArray array) { + array = addInputArray( + 'array', + array, + dimensions: array.dimensions, + elementWidth: array.elementWidth, + ); + addOutput('out', width: array.width) <= array.elements.swizzle(); + } +} + class ModWithConstInlineUnaryOp extends Module { ModWithConstInlineUnaryOp() { addOutput('b', width: 8) <= NotGate(Const(0, width: 8)).out; @@ -888,6 +928,61 @@ void main() { SimCompare.checkIverilogVector(mod, vectors); }); + test('packed one-dimensional array assigns directly to Logic', () async { + final mod = PackedArrayToLogic(LogicArray([4], 2, name: 'array')); + await mod.build(); + final sv = mod.dumpSystemVerilog(); + + expect(sv, contains('assign out = array;'), reason: sv); + expect(sv, isNot(contains('// swizzle')), reason: sv); + + final vectors = [ + Vector({'array': 0x00}, {'out': 0x00}), + Vector({'array': 0xa5}, {'out': 0xa5}), + Vector({'array': 0xff}, {'out': 0xff}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('Logic assigns directly to packed one-dimensional array', () async { + final mod = LogicToPackedArray(Logic(width: 8)); + await mod.build(); + final sv = mod.dumpSystemVerilog(); + + expect(sv, contains('assign out = data;'), reason: sv); + + final vectors = [ + Vector({'data': 0x00}, {'out': 0x00}), + Vector({'data': 0xa5}, {'out': 0xa5}), + Vector({'data': 0xff}, {'out': 0xff}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('computed Logic remains declared for packed array assignment', () async { + final mod = ComputedLogicToPackedArray(Logic(width: 8)); + await mod.build(); + + final vectors = [ + Vector({'data': 0x00}, {'out': 0x01}), + Vector({'data': 0xa5}, {'out': 0xa6}), + Vector({'data': 0xff}, {'out': 0x00}), + ]; + await SimCompare.checkFunctionalVector(mod, vectors); + SimCompare.checkIverilogVector(mod, vectors); + }); + + test('reordered packed array assignment retains its swizzle', () async { + final mod = ReversedPackedArrayToLogic(LogicArray([4], 2, name: 'array')); + await mod.build(); + final sv = mod.dumpSystemVerilog(); + + expect(sv, isNot(contains('assign out = array;')), reason: sv); + expect(sv, contains('// swizzle'), reason: sv); + }); + group('connected ports and pruning', () { for (final naming in [Naming.renameable, Naming.mergeable]) { test('with naming $naming', () async { diff --git a/test/swizzle_test.dart b/test/swizzle_test.dart index c155927189..6269b01afd 100644 --- a/test/swizzle_test.dart +++ b/test/swizzle_test.dart @@ -542,11 +542,7 @@ assign b = { 1'h0, /* 34 */ a, /* 33:26 */ x, /* 25:22 */ -({ -y[2], /* 5:4 */ -y[1], /* 3:2 */ -y[0] /* 1:0 */ -}), /* 21:16 */ +(y), /* 21:16 */ 5'h3, /* 15:11 */ ({ 3'h2, /* 10:8 */ diff --git a/test/typed_operations_test.dart b/test/typed_operations_test.dart new file mode 100644 index 0000000000..87b98207a4 --- /dev/null +++ b/test/typed_operations_test.dart @@ -0,0 +1,1906 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// typed_operations_test.dart +// Tests operations that preserve concrete LogicStructure types. +// +// 2026 September 2 +// Author: Desmond A. Kirkpatrick + +import 'dart:async'; + +import 'package:rohd/rohd.dart'; +import 'package:rohd/src/utilities/simcompare.dart'; +import 'package:test/test.dart'; + +class _TypedLane extends LogicStructure { + Logic get data => elements[0]; + Logic get enable => elements[1]; + + _TypedLane({String? name}) + : super([ + Logic(name: 'data', width: 8), + Logic(name: 'enable'), + ], name: name ?? 'typed_lane'); + + @override + _TypedLane clone({String? name}) => _TypedLane(name: name ?? this.name); +} + +class _ScalarDomainLogic extends Logic { + final String schema; + + _ScalarDomainLogic({required this.schema, super.name}) : super(width: 4); + + @override + _ScalarDomainLogic clone({String? name}) => + _ScalarDomainLogic(schema: schema, name: name ?? this.name); +} + +enum _OperationEnum { idle, active, done } + +LogicEnum<_OperationEnum> _operationEnum({String? name}) => + LogicEnum<_OperationEnum>.withMapping( + { + _OperationEnum.idle: 0, + _OperationEnum.active: 2, + _OperationEnum.done: 3, + }, + width: 2, + name: name, + ); + +LogicEnum<_OperationEnum> _operationEnumWithActiveEncoding( + int activeEncoding, { + String? name, +}) => + LogicEnum<_OperationEnum>.withMapping( + { + _OperationEnum.idle: 0, + _OperationEnum.active: activeEncoding, + _OperationEnum.done: 3, + }, + width: 2, + name: name, + ); + +LogicValueCodec _typedLaneCodec() => LogicValueCodec( + decode: (value) => value.isValid ? value.toInt() : 0, + encode: (value) => LogicValue.ofInt(value, 9), + ); + +class _TypedPacket extends LogicStructure { + Logic get opcode => elements[0]; + TypedLogicArray<_TypedLane, LogicValue> get lanes => + elements[1] as TypedLogicArray<_TypedLane, LogicValue>; + + final List laneDimensions; + + final int laneNumUnpackedDimensions; + + _TypedPacket({ + this.laneDimensions = const [2], + this.laneNumUnpackedDimensions = 0, + String? name, + }) : super([ + Logic(name: 'opcode', width: 3), + TypedLogicArray<_TypedLane, LogicValue>( + laneDimensions, _TypedLane.new, + name: 'lanes', numUnpackedDimensions: laneNumUnpackedDimensions), + ], name: name ?? 'typed_packet'); + + @override + _TypedPacket clone({String? name}) => _TypedPacket( + laneDimensions: laneDimensions, + laneNumUnpackedDimensions: laneNumUnpackedDimensions, + name: name ?? this.name, + ); +} + +class _EnumPacket extends LogicStructure { + LogicEnum<_OperationEnum> get state => + elements.single as LogicEnum<_OperationEnum>; + + final int activeEncoding; + + _EnumPacket({required this.activeEncoding, String? name}) + : super([ + _operationEnumWithActiveEncoding(activeEncoding, name: 'state'), + ], name: name ?? 'enum_packet'); + + @override + _EnumPacket clone({String? name}) => + _EnumPacket(activeEncoding: activeEncoding, name: name ?? this.name); +} + +class _BadClonePacket extends LogicStructure { + _BadClonePacket({String? name}) + : super([Logic(name: 'data')], name: name ?? 'bad_clone_packet'); + + @override + LogicStructure clone({String? name}) => + LogicStructure([Logic(name: 'data')], name: name ?? this.name); +} + +class _ConstPacket extends LogicStructure { + Logic get data => elements.single; + + _ConstPacket({int? value = 1, String? name}) + : super([ + if (value == null) + Logic(name: 'data', width: 4) + else + Const(value, width: 4) + ], name: name ?? 'const_packet'); + + @override + _ConstPacket clone({String? name}) => + _ConstPacket(value: null, name: name ?? this.name); +} + +class _NetPacket extends LogicStructure { + _NetPacket({String? name}) : super([LogicNet()], name: name ?? 'net_packet'); + + @override + _NetPacket clone({String? name}) => _NetPacket(name: name ?? this.name); +} + +class _ConfiguredPacket extends LogicStructure { + final String schema; + + _ConfiguredPacket({required this.schema, String? name}) + : super([ + Logic(name: 'data', width: 4), + ], name: name ?? 'configured_packet'); + + Logic get data => elements.single; + + @override + _ConfiguredPacket clone({String? name}) => + _ConfiguredPacket(schema: schema, name: name ?? this.name); +} + +class _ConfiguredPacketGenerator { + final String schema; + int calls = 0; + + _ConfiguredPacketGenerator(this.schema); + + _ConfiguredPacket call({String? name}) { + calls++; + return _ConfiguredPacket(schema: schema, name: name); + } +} + +class _ScalarDomainVectorHarness extends Module { + _ScalarDomainVectorHarness( + Logic control, + Logic reset, + _ScalarDomainLogic d0, + _ScalarDomainLogic d1, + ) { + final clk = SimpleClockGenerator(10).clk; + control = addInput('control', control); + reset = addInput('reset', reset); + d0 = addTypedInput('d0', d0); + d1 = addTypedInput('d1', d1); + + final selected = Mux(control, d1, d0).out; + final registered = FlipFlop( + clk, + selected, + reset: reset, + resetValue: 0, + ).q; + addTypedOutput('selected', selected.clone).gets(selected); + addTypedOutput('registered', registered.clone).gets(registered); + } +} + +class _ArrayOperationVectorHarness extends Module { + _ArrayOperationVectorHarness( + Logic control, + Logic enable, + Logic reset, + LogicArray first, + LogicArray second, + ) { + final clk = SimpleClockGenerator(10).clk; + control = addInput('control', control); + enable = addInput('enable', enable); + reset = addInput('reset', reset); + first = addTypedInput('first', first); + second = addTypedInput('second', second); + + final selected = Mux(control, second, first).out; + final registered = FlipFlop( + clk, + selected, + en: enable, + reset: reset, + resetValue: 0, + ).q; + addTypedOutput('selected', selected.clone).gets(selected); + addTypedOutput('out', registered.clone).gets(registered); + } +} + +class _PacketArrayOperationVectorHarness extends Module { + _PacketArrayOperationVectorHarness( + Logic control, + Logic enable, + Logic reset, + _TypedPacket first, + _TypedPacket second, + ) { + final clk = SimpleClockGenerator(10).clk; + control = addInput('control', control); + enable = addInput('enable', enable); + reset = addInput('reset', reset); + first = addTypedInput('first', first); + second = addTypedInput('second', second); + + final selected = Mux(control, second, first).out; + final registered = FlipFlop( + clk, + selected, + en: enable, + reset: reset, + resetValue: 0, + ).q; + addTypedOutput('selected', selected.clone).gets(selected); + addTypedOutput('out', registered.clone).gets(registered); + } +} + +class _TypedOperationsSynthesisHarness extends Module { + _TypedOperationsSynthesisHarness(Logic clk, Logic reset, Logic control, + Logic selector, _TypedPacket first, _TypedPacket second) { + clk = addInput('clk', clk); + reset = addInput('reset', reset); + control = addInput('control', control); + selector = addInput('selector', selector, width: selector.width); + first = addTypedInput('first', first); + second = addTypedInput('second', second); + + final selected = Mux( + control, + second, + first, + outputGenerator: first.clone, + ).out; + final _TypedPacket selectedByCase = cases( + selector, + {0: first, 1: second}, + defaultValue: selected, + outputGenerator: first.clone, + name: 'selected_by_case', + ); + final selectedByIndex = [first, second, selectedByCase].selectIndex( + selector, + defaultValue: first, + name: 'selected_by_index', + ); + final passed = Passthrough(selectedByIndex).out; + final registered = FlipFlop( + clk, + passed, + reset: reset, + resetValue: 0, + outputGenerator: passed.clone, + ).q; + final pipeline = Pipeline( + clk, + reset: reset, + resetValues: {registered: 0}, + stages: [ + (p) => [p.get(registered) < p.get(registered)], + ], + ); + final piped = pipeline.get(registered); + addTypedOutput('out', piped.clone).gets(piped); + + final readyValidPipeline = ReadyValidPipeline( + clk, + control, + reset, + reset: reset, + resetValues: {registered: 0}, + stages: [ + (p) => [p.get(registered) < p.get(registered)], + ], + ); + final readyValidPiped = readyValidPipeline.get(registered); + addTypedOutput('ready_valid_out', readyValidPiped.clone) + .gets(readyValidPiped); + addOutput('ready_valid').gets(readyValidPipeline.validPipeOut); + addOutput('ready_ready').gets(readyValidPipeline.readyPipeIn); + } +} + +class _GeneratorVectorHarness extends Module { + _GeneratorVectorHarness( + Logic control, + Logic selector, + _ConfiguredPacket first, + _ConfiguredPacket second, + Logic rawBits, + ) { + control = addInput('control', control); + selector = addInput('selector', selector); + first = addTypedInput('first', first); + second = addTypedInput('second', second); + rawBits = addInput('rawBits', rawBits, width: rawBits.width); + + final muxGenerator = _ConfiguredPacketGenerator('mux'); + final muxed = Mux( + control, + second, + first, + outputGenerator: muxGenerator.call, + ).out; + final caseGenerator = _ConfiguredPacketGenerator('case'); + final selected = cases( + selector, + { + 0: muxed, + 1: rawBits, + }, + outputGenerator: caseGenerator.call, + ); + final constantGenerator = _ConfiguredPacketGenerator('constant'); + final constantSelected = mux( + Const(0), + second, + first, + outputGenerator: constantGenerator.call, + ); + addTypedOutput('out', selected.clone).gets(selected); + addTypedOutput('constantOut', constantSelected.clone) + .gets(constantSelected); + } +} + +void _expectLogicArray(LogicArray array) { + expect(array, isA()); +} + +void _expectTypedLaneArray(TypedLogicArray<_TypedLane, LogicValue> array) { + expect(array, isA>()); +} + +void _expectConst(Const value) { + expect(value, isA()); +} + +void _expectLogic(Logic value) { + expect(value, isA()); +} + +void main() { + tearDown(Simulator.reset); + + test('Mux exposes a scalar Logic output', () { + final muxModule = Mux(Logic(), Logic(width: 4), Logic(width: 4)); + expect(muxModule.out.runtimeType, Logic); + }); + + test('scalar operations explicitly normalize constants and nets', () async { + final clk = SimpleClockGenerator(10).clk; + final control = Logic(); + final ordinary = Logic(width: 4); + final constant = Const(0xa, width: 4); + final portPrototype = Logic.port('port_data', 4); + final netDriver = Logic(width: 4); + final net = LogicNet(width: 4)..gets(netDriver); + final constantMux = Mux(control, constant, ordinary); + final constantD0Mux = Mux(control, ordinary, constant); + final portMux = Mux(control, portPrototype, ordinary); + final netMux = Mux(control, net, ordinary); + final netD0Mux = Mux(control, ordinary, net); + final constantFlop = FlipFlop(clk, constant); + final portFlop = FlipFlop(clk, portPrototype); + final netFlop = FlipFlop(clk, net); + await Future.wait([ + constantMux.build(), + portMux.build(), + netMux.build(), + constantFlop.build(), + portFlop.build(), + netFlop.build(), + ]); + unawaited(Simulator.run()); + + for (final output in [ + constantMux.out, + constantD0Mux.out, + portMux.out, + netMux.out, + netD0Mux.out, + constantFlop.q, + portFlop.q, + netFlop.q, + ]) { + expect(output.runtimeType, Logic); + expect(output.isNet, isFalse); + } + + ordinary.inject(3); + portPrototype.inject(5); + netDriver.inject(7); + control.inject(1); + await Simulator.tick(); + expect(constantMux.out.value.toInt(), 0xa); + expect(constantD0Mux.out.value.toInt(), 3); + expect(portMux.out.value.toInt(), 5); + expect(netMux.out.value.toInt(), 7); + expect(netD0Mux.out.value.toInt(), 3); + control.inject(0); + await Simulator.tick(); + expect(constantMux.out.value.toInt(), 3); + expect(constantD0Mux.out.value.toInt(), 0xa); + expect(netMux.out.value.toInt(), 3); + expect(netD0Mux.out.value.toInt(), 7); + await clk.nextPosedge; + expect(constantFlop.q.value.toInt(), 0xa); + expect(portFlop.q.value.toInt(), 5); + expect(netFlop.q.value.toInt(), 7); + await Simulator.endSimulation(); + }); + + test('scalar domain subclasses retain their concrete output type', () async { + final clk = SimpleClockGenerator(10).clk; + final control = Logic(); + final d0 = _ScalarDomainLogic(schema: 'd0', name: 'd0'); + final d1 = _ScalarDomainLogic(schema: 'd1', name: 'd1'); + final muxed = Mux(control, d1, d0).out; + final forwarded = Passthrough(d0).out; + final registered = FlipFlop(clk, d0).q; + + expect(muxed, isA<_ScalarDomainLogic>()); + expect(muxed.schema, 'd0'); + expect(forwarded, isA<_ScalarDomainLogic>()); + expect(registered, isA<_ScalarDomainLogic>()); + + unawaited(Simulator.run()); + d0.inject(0x2); + d1.inject(0xd); + control.inject(1); + await Simulator.tick(); + expect(muxed.value, LogicValue.ofInt(0xd, 4)); + expect(forwarded.value, LogicValue.ofInt(0x2, 4)); + await clk.nextPosedge; + expect(registered.value, LogicValue.ofInt(0x2, 4)); + await Simulator.endSimulation(); + }); + + test('scalar domain muxes and flops emit directly and match vectors', + () async { + final harness = _ScalarDomainVectorHarness( + Logic(), + Logic(), + _ScalarDomainLogic(schema: 'd0', name: 'd0'), + _ScalarDomainLogic(schema: 'd1', name: 'd1'), + ); + await harness.build(); + + final systemVerilog = harness.dumpSystemVerilog(); + expect(systemVerilog, contains('always_ff')); + expect(systemVerilog, contains('?')); + expect( + systemVerilog, + isNot(matches(RegExp(r'module\s+\w*(?:Mux|FlipFlop)'))), + ); + + final vectors = [ + Vector( + { + 'control': 0, + 'reset': 1, + 'd0': 0x3, + 'd1': 0xc, + }, + { + 'selected': 0x3, + }, + ), + Vector( + { + 'control': 0, + 'reset': 1, + 'd0': 0x3, + 'd1': 0xc, + }, + { + 'selected': 0x3, + 'registered': 0, + }, + ), + Vector( + { + 'control': 1, + 'reset': 0, + 'd0': 0x3, + 'd1': 0xc, + }, + { + 'selected': 0xc, + 'registered': 0, + }, + ), + Vector( + { + 'control': 1, + 'reset': 0, + 'd0': 0x3, + 'd1': 0xc, + }, + { + 'selected': 0xc, + 'registered': 0xc, + }, + ), + Vector( + { + 'control': 0, + 'reset': 0, + 'd0': 0x5, + 'd1': 0xa, + }, + { + 'selected': 0x5, + 'registered': 0xc, + }, + ), + Vector( + { + 'control': 0, + 'reset': 0, + 'd0': 0x5, + 'd1': 0xa, + }, + { + 'selected': 0x5, + 'registered': 0x5, + }, + ), + ]; + await SimCompare.checkFunctionalVector(harness, vectors); + SimCompare.checkIverilogVector(harness, vectors); + }); + + test('array muxes and flops match packed and unpacked backends', () async { + final configurations = [ + ( + label: 'packed', + numUnpackedDimensions: 0, + ), + ( + label: 'mixed', + numUnpackedDimensions: 1, + ), + ( + label: 'unpacked', + numUnpackedDimensions: 2, + ), + ]; + final vectors = [ + Vector( + { + 'control': 0, + 'enable': 1, + 'reset': 1, + 'first': 0x3, + 'second': 0xc, + }, + { + 'selected': 0x3, + }, + ), + Vector( + { + 'control': 0, + 'enable': 1, + 'reset': 1, + 'first': 0x3, + 'second': 0xc, + }, + { + 'selected': 0x3, + 'out': 0, + }, + ), + Vector( + { + 'control': 1, + 'enable': 1, + 'reset': 0, + 'first': 0x3, + 'second': 0xc, + }, + { + 'selected': 0xc, + 'out': 0, + }, + ), + Vector( + { + 'control': 1, + 'enable': 1, + 'reset': 0, + 'first': 0x3, + 'second': 0xc, + }, + { + 'selected': 0xc, + 'out': 0xc, + }, + ), + Vector( + { + 'control': 0, + 'enable': 0, + 'reset': 0, + 'first': 0x5, + 'second': 0xa, + }, + { + 'selected': 0x5, + 'out': 0xc, + }, + ), + Vector( + { + 'control': 0, + 'enable': 1, + 'reset': 0, + 'first': 0x5, + 'second': 0xa, + }, + { + 'selected': 0x5, + 'out': 0xc, + }, + ), + Vector( + { + 'control': 0, + 'enable': 1, + 'reset': 0, + 'first': 0x5, + 'second': 0xa, + }, + { + 'selected': 0x5, + 'out': 0x5, + }, + ), + ]; + + for (final configuration in configurations) { + final first = LogicArray( + [2, 2], + 1, + name: 'first_${configuration.label}', + numUnpackedDimensions: configuration.numUnpackedDimensions, + ); + final second = LogicArray( + [2, 2], + 1, + name: 'second_${configuration.label}', + numUnpackedDimensions: configuration.numUnpackedDimensions, + ); + final harness = _ArrayOperationVectorHarness( + Logic(), + Logic(), + Logic(), + first, + second, + ); + await harness.build(); + + await SimCompare.checkFunctionalVector(harness, vectors); + SimCompare.checkVerilatorVector(harness, vectors); + if (configuration.numUnpackedDimensions == 0) { + SimCompare.checkIverilogVector(harness, vectors); + } + await Simulator.reset(); + } + }, timeout: const Timeout(Duration(minutes: 2))); + + test('nested typed-array packet operations match an emitted backend', + () async { + final configurations = [ + ( + label: 'mixed', + laneDimensions: const [2, 2], + laneNumUnpackedDimensions: 1, + ), + ]; + + for (final configuration in configurations) { + final first = _TypedPacket( + laneDimensions: configuration.laneDimensions, + laneNumUnpackedDimensions: configuration.laneNumUnpackedDimensions, + name: 'first_${configuration.label}', + ); + final second = _TypedPacket( + laneDimensions: configuration.laneDimensions, + laneNumUnpackedDimensions: configuration.laneNumUnpackedDimensions, + name: 'second_${configuration.label}', + ); + final firstValue = LogicValue.ofInt(0x15, first.width); + final secondValue = LogicValue.ofBigInt( + (BigInt.one << second.width) - BigInt.one, second.width); + final harness = _PacketArrayOperationVectorHarness( + Logic(), + Logic(), + Logic(), + first, + second, + ); + final vectors = [ + Vector( + { + 'control': 0, + 'enable': 1, + 'reset': 1, + 'first': firstValue, + 'second': secondValue, + }, + { + 'selected': firstValue, + }, + ), + Vector( + { + 'control': 0, + 'enable': 1, + 'reset': 1, + 'first': firstValue, + 'second': secondValue, + }, + { + 'selected': firstValue, + 'out': LogicValue.ofInt(0, first.width), + }, + ), + Vector( + { + 'control': 1, + 'enable': 1, + 'reset': 0, + 'first': firstValue, + 'second': secondValue, + }, + { + 'selected': secondValue, + 'out': LogicValue.ofInt(0, first.width), + }, + ), + Vector( + { + 'control': 1, + 'enable': 1, + 'reset': 0, + 'first': firstValue, + 'second': secondValue, + }, + { + 'selected': secondValue, + 'out': secondValue, + }, + ), + Vector( + { + 'control': 0, + 'enable': 0, + 'reset': 0, + 'first': firstValue, + 'second': secondValue, + }, + { + 'selected': firstValue, + 'out': secondValue, + }, + ), + Vector( + { + 'control': 0, + 'enable': 1, + 'reset': 0, + 'first': firstValue, + 'second': secondValue, + }, + { + 'selected': firstValue, + 'out': secondValue, + }, + ), + Vector( + { + 'control': 0, + 'enable': 1, + 'reset': 0, + 'first': firstValue, + 'second': secondValue, + }, + { + 'selected': firstValue, + 'out': firstValue, + }, + ), + ]; + + await harness.build(); + await SimCompare.checkFunctionalVector(harness, vectors); + SimCompare.checkVerilatorVector(harness, vectors); + await Simulator.reset(); + } + }); + + test('LogicEnum domains compose with generic operations', () async { + final clk = SimpleClockGenerator(10).clk; + final control = Logic(); + final reset = Logic(); + final selector = _operationEnum(name: 'selector'); + final d0 = _operationEnum(name: 'd0'); + final d1 = _operationEnum(name: 'd1'); + final muxed = Mux(control, d1, d0).out; + final cased = cases>( + selector, + { + _OperationEnum.idle: d0, + _OperationEnum.active: d1, + }, + defaultValue: d0, + ); + final indexed = [d0, d1].selectIndex(control, defaultValue: d0); + final forwarded = Passthrough(d0).out; + final flipFlop = FlipFlop( + clk, + cased, + reset: reset, + resetValue: _OperationEnum.idle, + ); + final registered = flipFlop.q; + + expect(muxed, isA>()); + expect(cased, isA>()); + expect(indexed, isA>()); + expect(forwarded, isA>()); + expect(registered, isA>()); + for (final result in [muxed, cased, indexed, forwarded, registered]) { + expect(result.mapping, d0.mapping); + } + expect( + () => Mux( + control, + LogicEnum<_OperationEnum>.withMapping( + { + _OperationEnum.idle: 0, + _OperationEnum.active: 1, + _OperationEnum.done: 3, + }, + width: 2, + ), + d0, + ), + throwsA(isA()), + ); + expect( + () => cases>( + selector, + {_OperationEnum.idle: _OperationEnum.idle}, + ), + throwsA(isA()), + ); + + await flipFlop.build(); + unawaited(Simulator.run()); + d0.inject(_OperationEnum.idle); + d1.inject(_OperationEnum.done); + control.inject(1); + selector.inject(_OperationEnum.active); + await Simulator.tick(); + expect(muxed.valueEnum, _OperationEnum.done); + expect(cased.valueEnum, _OperationEnum.done); + expect(indexed.valueEnum, _OperationEnum.done); + expect(forwarded.valueEnum, _OperationEnum.idle); + + reset.inject(1); + await clk.nextPosedge; + reset.inject(0); + expect(registered.valueEnum, _OperationEnum.idle); + selector.inject(_OperationEnum.active); + await clk.nextPosedge; + expect(registered.valueEnum, _OperationEnum.done); + await Simulator.endSimulation(); + }); + + test('operations reject incompatible nested enum mappings', () { + final firstPacket = _EnumPacket(activeEncoding: 2, name: 'first_packet'); + final secondPacket = _EnumPacket(activeEncoding: 1, name: 'second_packet'); + final firstArray = TypedLogicArray, LogicValue>( + [2], + ({name}) => _operationEnumWithActiveEncoding(2, name: name), + name: 'first_array', + ); + final secondArray = TypedLogicArray, LogicValue>( + [2], + ({name}) => _operationEnumWithActiveEncoding(1, name: name), + name: 'second_array', + ); + + expect( + () => Mux(Logic(), secondPacket, firstPacket), + throwsA(isA()), + ); + expect( + () => Mux( + Logic(), + firstPacket, + firstPacket, + outputGenerator: ({name}) => _EnumPacket(activeEncoding: 1, name: name), + ), + throwsA(isA()), + ); + expect( + () => Mux(Logic(), secondArray, firstArray), + throwsA(isA()), + ); + expect( + Mux(Logic(), secondArray.packed, firstArray.packed).out, + isA(), + ); + }); + + test('inferred constants and nets reject driveable operation results', () { + final clk = Logic(); + final control = Logic(); + final constant = Const(3, width: 4); + final net = LogicNet(width: 4); + + expect( + () => FlipFlop(clk, constant), + throwsA(isA()), + ); + expect( + () => FlipFlop(clk, net), + throwsA(isA()), + ); + expect( + () => Passthrough(constant), + throwsA(isA()), + ); + expect( + () => Mux(control, Const(1, width: 4), Const(0, width: 4)), + throwsA(isA()), + ); + }); + + test('mux constant shortcuts and generators have explicit identities', + () async { + final d0 = _ConfiguredPacket(schema: 'd0', name: 'd0'); + final d1 = _ConfiguredPacket(schema: 'd1', name: 'd1'); + + expect(mux(Const(0), d1, d0), same(d0)); + expect(mux(Const(1), d1, d0), same(d1)); + + final generator = _ConfiguredPacketGenerator('generated'); + final generated = mux( + Const(0), + d1, + d0, + outputGenerator: generator.call, + ); + expect(generator.calls, 1); + expect(generated, isNot(same(d0))); + expect(generated.schema, 'generated'); + + d0.data.put(0x3); + d1.data.put(0xa); + await Simulator.tick(); + expect(generated.data.value.toInt(), 0x3); + }); + + test('mux defaults to d0 configuration and generators override it', () async { + final control = Logic(); + final d0 = _ConfiguredPacket(schema: 'd0', name: 'd0'); + final d1 = _ConfiguredPacket(schema: 'd1', name: 'd1'); + final defaultSchema = Mux(control, d1, d0).out; + final generator = _ConfiguredPacketGenerator('generated'); + final generatedSchema = Mux( + control, + d1, + d0, + outputGenerator: generator.call, + ).out; + + expect(defaultSchema.schema, 'd0'); + expect(generatedSchema.schema, 'generated'); + expect(generator.calls, 1); + + d0.data.put(0x1); + d1.data.put(0xe); + control.put(0); + await Simulator.tick(); + expect(defaultSchema.data.value.toInt(), 0x1); + expect(generatedSchema.data.value.toInt(), 0x1); + control.put(1); + await Simulator.tick(); + expect(defaultSchema.data.value.toInt(), 0xe); + expect(generatedSchema.data.value.toInt(), 0xe); + }); + + test('flop generators override output schemas', () async { + final clk = SimpleClockGenerator(10).clk; + final source = _ConfiguredPacket(schema: 'source', name: 'source'); + final flopGenerator = _ConfiguredPacketGenerator('flop'); + final forwarded = Passthrough(source).out; + final registered = FlipFlop( + clk, + source, + outputGenerator: flopGenerator.call, + ).q; + + expect(forwarded.schema, 'source'); + expect(registered.schema, 'flop'); + expect(flopGenerator.calls, 1); + + unawaited(Simulator.run()); + source.data.inject(0x9); + await Simulator.tick(); + expect(forwarded.data.value.toInt(), 0x9); + await clk.nextPosedge; + expect(registered.data.value.toInt(), 0x9); + await Simulator.endSimulation(); + }); + + test('typed operations infer top-level array types', () { + final clk = Logic(); + final selector = Logic(width: 2); + final array0 = LogicArray([2], 4, name: 'array0'); + final array1 = LogicArray([2], 4, name: 'array1'); + final typedArray0 = TypedLogicArray<_TypedLane, LogicValue>( + [2], _TypedLane.new, + name: 'typed_array0'); + final typedArray1 = TypedLogicArray<_TypedLane, LogicValue>( + [2], _TypedLane.new, + name: 'typed_array1'); + + final muxedArray = Mux(selector[0], array1, array0).out; + final floppedArray = FlipFlop(clk, array0).q; + final passedArray = Passthrough(array0).out; + final LogicArray casedArray = cases(selector, {0: array0, 1: array1}); + final selectedArray = [array0, array1].selectIndex(selector); + final selectedFromArray = selector.selectFrom([array0, array1]); + final clonedArray = array0.cloneTyped(); + final namedArray = array0.namedTyped('named_array'); + final arrayPipeline = Pipeline( + clk, + stages: [ + (p) => [p.get(array0) < p.get(array0)] + ], + ); + final pipelinedArray = arrayPipeline.get(array0); + + final muxedTypedArray = Mux(selector[0], typedArray1, typedArray0).out; + final floppedTypedArray = FlipFlop(clk, typedArray0).q; + final passedTypedArray = Passthrough(typedArray0).out; + final TypedLogicArray<_TypedLane, LogicValue> casedTypedArray = + cases(selector, {0: typedArray0, 1: typedArray1}); + final selectedTypedArray = [typedArray0, typedArray1].selectIndex(selector); + final selectedFromTypedArray = + selector.selectFrom([typedArray0, typedArray1]); + final clonedTypedArray = typedArray0.cloneTyped(); + final namedTypedArray = typedArray0.namedTyped('named_typed_array'); + final typedArrayPipeline = Pipeline( + clk, + stages: [ + (p) => [p.get(typedArray0) < p.get(typedArray0)] + ], + ); + final pipelinedTypedArray = typedArrayPipeline.get(typedArray0); + + [ + muxedArray, + floppedArray, + passedArray, + casedArray, + selectedArray, + selectedFromArray, + clonedArray, + namedArray, + pipelinedArray, + ].forEach(_expectLogicArray); + [ + muxedTypedArray, + floppedTypedArray, + passedTypedArray, + casedTypedArray, + selectedTypedArray, + selectedFromTypedArray, + clonedTypedArray, + namedTypedArray, + pipelinedTypedArray, + ].forEach(_expectTypedLaneArray); + }); + + test('Mux infers nested TypedLogicArray types', () async { + final control = Logic(); + final d1 = _TypedPacket(name: 'd1'); + final d0 = _TypedPacket(name: 'd0'); + final muxModule = Mux(control, d1, d0); + await muxModule.build(); + expect(muxModule.out, isA<_TypedPacket>()); + expect(muxModule.out.lanes, isA>()); + expect(muxModule.out.lanes.arrayElements, everyElement(isA<_TypedLane>())); + + d0.opcode.put(1); + d0.lanes.arrayElements[0].data.put(0x10); + d0.lanes.arrayElements[1].data.put(0x20); + d1.opcode.put(6); + d1.lanes.arrayElements[0].data.put(0xa0); + d1.lanes.arrayElements[1].data.put(0xb0); + + control.put(0); + expect(muxModule.out.opcode.value.toInt(), 1); + expect(muxModule.out.lanes.arrayElements[0].data.value.toInt(), 0x10); + expect(muxModule.out.lanes.arrayElements[1].data.value.toInt(), 0x20); + + control.put(1); + expect(muxModule.out.opcode.value.toInt(), 6); + expect(muxModule.out.lanes.arrayElements[0].data.value.toInt(), 0xa0); + expect(muxModule.out.lanes.arrayElements[1].data.value.toInt(), 0xb0); + }); + + test('Mux preserves structure type with a constant control', () { + final d1 = _TypedPacket(name: 'd1'); + final d0 = _TypedPacket(name: 'd0'); + expect(mux(Const(0), d1, d0), isA<_TypedPacket>()); + expect(mux(Const(1), d1, d0), isA<_TypedPacket>()); + }); + + test('Mux rejects differently shaped typed arrays', () { + expect( + () => Mux(Logic(), _TypedPacket(laneDimensions: const [2, 2]), + _TypedPacket(laneDimensions: const [4])), + throwsA(isA())); + }); + + test('typed operations require identical typed-array codecs', () { + final first = TypedLogicArray<_TypedLane, int>( + [2], + _TypedLane.new, + valueCodec: _typedLaneCodec(), + ); + final second = TypedLogicArray<_TypedLane, int>( + [2], + _TypedLane.new, + valueCodec: _typedLaneCodec(), + ); + + expect(identical(first.valueCodec, second.valueCodec), isFalse); + expect( + () => Mux(Logic(), second, first), + throwsA(isA()), + ); + expect( + () => cases>( + Logic(), + {0: first, 1: second}, + ), + throwsA(isA()), + ); + expect( + Mux(Logic(), second.packed, first.packed).out, + isA(), + ); + }); + + test('typed operations consume structures with constant leaves', () async { + final clk = SimpleClockGenerator(10).clk; + final control = Logic(); + final constant = _ConstPacket(value: 0xa, name: 'constant'); + final variable = _ConstPacket(value: null, name: 'variable'); + final muxModule = Mux(control, constant, variable); + final passthrough = Passthrough(constant); + final flipFlop = FlipFlop(clk, constant); + await Future.wait([ + muxModule.build(), + passthrough.build(), + flipFlop.build(), + ]); + for (final module in [muxModule, passthrough, flipFlop]) { + SimCompare.checkIverilogVector(module, const [], buildOnly: true); + } + + expect(muxModule.out, isA<_ConstPacket>()); + expect(muxModule.out.hasConsts, isFalse); + expect(passthrough.out, isA<_ConstPacket>()); + expect(passthrough.out.hasConsts, isFalse); + expect(flipFlop.q, isA<_ConstPacket>()); + expect(flipFlop.q.hasConsts, isFalse); + + variable.data.inject(3); + control.inject(0); + await Simulator.tick(); + expect(muxModule.out.data.value.toInt(), 3); + expect(passthrough.out.data.value.toInt(), 0xa); + + control.inject(1); + await Simulator.tick(); + expect(muxModule.out.data.value.toInt(), 0xa); + + unawaited(Simulator.run()); + await clk.nextPosedge; + expect(flipFlop.q.data.value.toInt(), 0xa); + await Simulator.endSimulation(); + }); + + test('explicit Logic flops constants through reset without mutating source', + () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + final constant = Const(0xa, width: 4); + final q = flop( + clk, + constant, + reset: reset, + resetValue: 0, + ); + + expect(q, isA()); + expect(q, isNot(isA())); + expect(constant.value, LogicValue.ofInt(0xa, 4)); + + unawaited(Simulator.run()); + reset.inject(1); + await clk.nextPosedge; + expect(q.value, LogicValue.ofInt(0, 4)); + reset.inject(0); + await clk.nextPosedge; + expect(q.value, LogicValue.ofInt(0xa, 4)); + expect(constant.value, LogicValue.ofInt(0xa, 4)); + await Simulator.endSimulation(); + }); + + test('typed operations reject clones, nets, and reset mismatches', () { + expect(() => _BadClonePacket().cloneTyped(), + throwsA(isA())); + expect(() => Passthrough<_NetPacket>(_NetPacket()), + throwsA(isA())); + expect( + () => FlipFlop<_TypedPacket>(Logic(), _TypedPacket(), + reset: Logic(), + resetValue: _TypedPacket(laneDimensions: const [1, 2])), + throwsA(isA())); + expect( + () => cases<_TypedPacket>(Logic(), { + 0: _TypedPacket(), + 1: _TypedPacket(laneDimensions: const [1, 2]) + }), + throwsA(isA())); + expect(() => <_TypedPacket>[].selectIndex(Logic()), + throwsA(isA())); + }); + + test('cases rejects mismatched LogicValue widths', () { + final selector = Logic(width: 2); + final first = _TypedPacket(name: 'first'); + final second = _TypedPacket(name: 'second'); + + expect( + () => cases(selector, { + LogicValue.ofInt(4, 3): first, + 1: second, + }), + throwsA(isA()), + ); + expect( + () => cases( + selector, + {0: first, 1: second}, + defaultValue: LogicValue.ofInt(0, first.width + 1), + ), + throwsA(isA()), + ); + }); + + test('typed module definitions encode structure and constant reset identity', + () { + final sameShapeFirst = Mux<_TypedPacket>(Logic(), + _TypedPacket(name: 'first_d1'), _TypedPacket(name: 'first_d0')); + final sameShapeSecond = Mux<_TypedPacket>(Logic(), + _TypedPacket(name: 'second_d1'), _TypedPacket(name: 'second_d0')); + final differentShape = Mux<_TypedPacket>( + Logic(), + _TypedPacket(laneDimensions: const [1, 2], name: 'different_d1'), + _TypedPacket(laneDimensions: const [1, 2], name: 'different_d0')); + final resetZero = FlipFlop<_TypedPacket>(Logic(), _TypedPacket(), + reset: Logic(), resetValue: 0); + final resetOne = FlipFlop<_TypedPacket>(Logic(), _TypedPacket(), + reset: Logic(), resetValue: 1); + + expect(sameShapeFirst.definitionName, sameShapeSecond.definitionName); + expect(sameShapeFirst.definitionName, isNot(differentShape.definitionName)); + expect(resetZero.definitionName, isNot(resetOne.definitionName)); + }); + + test('FlipFlop exposes a scalar Logic output', () { + final flipFlop = FlipFlop(Logic(), Logic(width: 4)); + expect(flipFlop.q.runtimeType, Logic); + }); + + test('FlipFlop infers type, reset, and enable behavior', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + final enable = Logic(); + final d = _TypedPacket(name: 'd'); + final flipFlop = FlipFlop( + clk, + d, + en: enable, + reset: reset, + resetValue: 0, + ); + await flipFlop.build(); + unawaited(Simulator.run()); + expect(flipFlop.q, isA<_TypedPacket>()); + expect(flipFlop.q.lanes, isA>()); + + reset.inject(1); + enable.inject(0); + await clk.nextPosedge; + reset.inject(0); + expect(flipFlop.q.value, LogicValue.ofInt(0, flipFlop.q.width)); + + d.opcode.inject(5); + d.lanes.arrayElements[0].data.inject(0x12); + d.lanes.arrayElements[0].enable.inject(1); + d.lanes.arrayElements[1].data.inject(0x34); + d.lanes.arrayElements[1].enable.inject(0); + enable.inject(1); + await clk.nextPosedge; + expect(flipFlop.q.opcode.value.toInt(), 5); + expect(flipFlop.q.lanes.arrayElements[0].data.value.toInt(), 0x12); + expect(flipFlop.q.lanes.arrayElements[1].data.value.toInt(), 0x34); + + d.opcode.inject(2); + d.lanes.arrayElements[0].data.inject(0xaa); + enable.inject(0); + await clk.nextPosedge; + expect(flipFlop.q.opcode.value.toInt(), 5); + expect(flipFlop.q.lanes.arrayElements[0].data.value.toInt(), 0x12); + await Simulator.endSimulation(); + }); + + test('FlipFlop maps packed reset bits to nested fields', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + final d = _TypedPacket(name: 'reset_order_d'); + final resetValue = LogicValue.ofString('000111100110100101101'); + final flipFlop = FlipFlop( + clk, + d, + reset: reset, + resetValue: resetValue, + ); + await flipFlop.build(); + unawaited(Simulator.run()); + + reset.inject(1); + await clk.nextPosedge; + reset.inject(0); + expect(flipFlop.q.opcode.value.toInt(), 0x5); + expect(flipFlop.q.lanes.arrayElements[0].data.value.toInt(), 0xa5); + expect(flipFlop.q.lanes.arrayElements[0].enable.value, LogicValue.one); + expect(flipFlop.q.lanes.arrayElements[1].data.value.toInt(), 0x3c); + expect(flipFlop.q.lanes.arrayElements[1].enable.value, LogicValue.zero); + await Simulator.endSimulation(); + }); + + test('typed clone and naming helpers retain concrete field access', () { + final source = _TypedPacket(name: 'source'); + final clone = source.cloneTyped(name: 'clone'); + final named = source.namedTyped('named'); + + expect(clone, isA<_TypedPacket>()); + expect(clone.name, 'clone'); + expect(named, isA<_TypedPacket>()); + expect(named.name, 'named'); + for (var index = 0; index < named.leafElements.length; index++) { + expect(named.leafElements[index].srcConnections, + contains(source.leafElements[index])); + } + }); + + test('typed clone and naming distinguish constants from aliases', () { + final constant = Const(0x5, width: 4); + final constantClone = constant.cloneTyped(); + _expectConst(constantClone); + expect(constantClone, isA()); + expect(constantClone.value, constant.value); + expect( + () => constant.namedTyped('constant_alias'), + throwsA(isA()), + ); + + final widenedConstant = constant as Logic; + final alias = widenedConstant.namedTyped('constant_alias'); + _expectLogic(alias); + expect(alias, isNot(isA())); + expect(alias.value, constant.value); + + final net = LogicNet(width: 4); + final netClone = net.cloneTyped(); + final netAlias = net.namedTyped('net_alias'); + expect(netClone, isA()); + expect(netAlias, isA()); + + final packet = _TypedPacket(name: 'packet'); + final widenedPacket = packet as Logic; + final widenedClone = widenedPacket.cloneTyped(name: 'packet_clone'); + _expectLogic(widenedClone); + expect(widenedClone, isA<_TypedPacket>()); + + final constantPacket = _ConstPacket(name: 'constant_packet'); + final namedPacket = constantPacket.namedTyped('named_packet'); + expect(namedPacket, isA<_ConstPacket>()); + expect(namedPacket.hasConsts, isFalse); + }); + + test('Passthrough infers typed contracts', () async { + final scalar = Passthrough(Logic(width: 4)); + final input = _TypedPacket(name: 'passthrough_input'); + final structured = Passthrough(input); + await Future.wait([scalar.build(), structured.build()]); + expect(scalar.out.runtimeType, Logic); + expect(structured.in_, isA<_TypedPacket>()); + expect(structured.out, isA<_TypedPacket>()); + expect( + structured.out.lanes, isA>()); + + input.opcode.put(5); + input.lanes.arrayElements[0].data.put(0x12); + input.lanes.arrayElements[1].data.put(0x34); + expect(structured.out.opcode.value.toInt(), 5); + expect(structured.out.lanes.arrayElements[0].data.value.toInt(), 0x12); + expect(structured.out.lanes.arrayElements[1].data.value.toInt(), 0x34); + }); + + test('cases preserves structure and selects a default', () { + final selector = Logic(width: 2); + final first = _TypedPacket(name: 'first'); + final second = _TypedPacket(name: 'second'); + final fallback = _TypedPacket(name: 'fallback'); + final _TypedPacket selected = cases(selector, {0: first, 2: second}, + defaultValue: fallback, name: 'selected'); + + expect(selected, isA<_TypedPacket>()); + first.opcode.put(1); + second.opcode.put(2); + fallback.opcode.put(7); + selector.put(0); + expect(selected.opcode.value.toInt(), 1); + selector.put(2); + expect(selected.opcode.value.toInt(), 2); + selector.put(3); + expect(selected.opcode.value.toInt(), 7); + }); + + test('cases uses an explicit generator for mixed packed sources', () { + final selector = Logic(); + final packet = _ConfiguredPacket(schema: 'packet', name: 'packet'); + final packed = Logic(width: packet.width); + final generator = _ConfiguredPacketGenerator('generated'); + final selected = cases( + selector, + { + 0: packet, + 1: packed, + }, + outputGenerator: generator.call, + ); + + expect(selected, isA<_ConfiguredPacket>()); + expect(selected.schema, 'generated'); + expect(generator.calls, 1); + packet.data.put(0x4); + packed.put(0xb); + selector.put(0); + expect(selected.data.value.toInt(), 0x4); + selector.put(1); + expect(selected.data.value.toInt(), 0xb); + }); + + test('generator-backed mux and cases match functional and SV vectors', + () async { + final harness = _GeneratorVectorHarness( + Logic(), + Logic(), + _ConfiguredPacket(schema: 'first', name: 'first'), + _ConfiguredPacket(schema: 'second', name: 'second'), + Logic(width: 4), + ); + await harness.build(); + + final vectors = [ + Vector( + { + 'control': 0, + 'selector': 0, + 'first': 0x3, + 'second': 0xc, + 'rawBits': 0xe, + }, + {'out': 0x3, 'constantOut': 0x3}, + ), + Vector( + { + 'control': 1, + 'selector': 0, + 'first': 0x3, + 'second': 0xc, + 'rawBits': 0xe, + }, + {'out': 0xc, 'constantOut': 0x3}, + ), + Vector( + { + 'control': 1, + 'selector': 1, + 'first': 0x3, + 'second': 0xc, + 'rawBits': 0xe, + }, + {'out': 0xe, 'constantOut': 0x3}, + ), + ]; + await SimCompare.checkFunctionalVector(harness, vectors); + SimCompare.checkIverilogVector(harness, vectors); + }); + + test('typed indexed selection works in both invocation directions', () { + final index = Logic(width: 2); + final values = [ + _TypedPacket(name: 'value0'), + _TypedPacket(name: 'value1'), + _TypedPacket(name: 'value2'), + ]; + final selectedByList = values.selectIndex(index, name: 'selected_by_list'); + final selectedByIndex = index.selectFrom(values, name: 'selected_by_index'); + + for (var valueIndex = 0; valueIndex < values.length; valueIndex++) { + values[valueIndex].opcode.put(valueIndex + 3); + } + index.put(1); + expect(selectedByList.opcode.value.toInt(), 4); + expect(selectedByIndex.opcode.value.toInt(), 4); + index.put(3); + expect(selectedByList.value, LogicValue.ofInt(0, selectedByList.width)); + expect(selectedByIndex.value, LogicValue.ofInt(0, selectedByIndex.width)); + }); + + test('Pipeline preserves typed structures, arrays, and history', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + final stall = Logic(); + final input = _TypedPacket(name: 'pipeline_input'); + final pipeline = Pipeline( + clk, + reset: reset, + resetValues: {input: 0}, + stalls: [null, stall], + stages: [ + (p) { + final source = p.get(input); + final target = p.get(input); + return [ + target.opcode < source.opcode + 1, + for (var lane = 0; + lane < target.lanes.arrayElements.length; + lane++) ...[ + target.lanes.arrayElements[lane].data < + source.lanes.arrayElements[lane].data, + target.lanes.arrayElements[lane].enable < + source.lanes.arrayElements[lane].enable, + ], + ]; + }, + (p) { + final target = p.get(input); + final previous = p.get(input, -1); + return [ + target.opcode < previous.opcode, + for (var lane = 0; + lane < target.lanes.arrayElements.length; + lane++) ...[ + target.lanes.arrayElements[lane].data < + previous.lanes.arrayElements[lane].data, + target.lanes.arrayElements[lane].enable < + previous.lanes.arrayElements[lane].enable, + ], + ]; + }, + ], + ); + final output = pipeline.get(input); + unawaited(Simulator.run()); + + expect(pipeline.stageCount, 3); + expect(output, isA<_TypedPacket>()); + expect(output.lanes, isA>()); + + reset.inject(1); + stall.inject(0); + await clk.nextPosedge; + reset.inject(0); + + input.opcode.inject(2); + input.lanes.arrayElements[0].data.inject(0x45); + await clk.nextPosedge; + input.opcode.inject(4); + input.lanes.arrayElements[0].data.inject(0x67); + await clk.nextPosedge; + expect(output.opcode.value.toInt(), 5); + expect(output.lanes.arrayElements[0].data.value.toInt(), 0x67); + + stall.inject(1); + input.opcode.inject(1); + await clk.nextPosedge; + expect(output.opcode.value.toInt(), 5); + await Simulator.endSimulation(); + }); + + test( + 'Pipeline preserves heterogeneous identifiers through relative and ' + 'absolute access', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + final stall = Logic(); + final packet = _TypedPacket(name: 'heterogeneous_packet'); + final array = TypedLogicArray<_TypedLane, LogicValue>( + [1], + _TypedLane.new, + name: 'heterogeneous_array', + ); + final state = _operationEnum(name: 'heterogeneous_state'); + final enable = Logic(name: 'heterogeneous_enable'); + + final pipeline = Pipeline( + clk, + reset: reset, + resetValues: { + packet: 0, + array: 0, + state: _OperationEnum.idle, + enable: 0, + }, + stalls: [null, stall], + stages: [ + (p) { + final sourcePacket = p.get(packet); + final targetPacket = p.get(packet); + final sourceArray = p.get(array); + final targetArray = p.get(array); + final targetState = p.get(state); + final sourceEnable = p.get(enable); + final targetEnable = p.get(enable); + final stateCondition = p.get(enable); + return [ + targetPacket.opcode < sourcePacket.opcode + 1, + targetPacket.lanes < sourcePacket.lanes, + targetArray < sourceArray, + If( + stateCondition, + then: [targetState < _OperationEnum.active], + orElse: [targetState < _OperationEnum.idle], + ), + targetEnable < ~sourceEnable, + ]; + }, + (p) { + final targetPacket = p.get(packet); + final priorPacket = p.get(packet, -1); + final firstPacket = p.getAbs(packet, 0); + final targetArray = p.get(array); + final firstArray = p.getAbs(array, 0); + final targetState = p.get(state); + final firstState = p.getAbs(state, 0); + final targetEnable = p.get(enable); + final firstEnable = p.getAbs(enable, 0); + return [ + targetPacket.opcode < priorPacket.opcode, + targetPacket.lanes < firstPacket.lanes, + targetArray < firstArray, + targetState < firstState, + targetEnable < firstEnable, + ]; + }, + ], + ); + + final pipedPacket = pipeline.get(packet); + final pipedArray = pipeline.get(array); + final pipedState = pipeline.get(state); + final pipedEnable = pipeline.get(enable); + unawaited(Simulator.run()); + + reset.inject(1); + stall.inject(0); + await clk.nextPosedge; + expect(pipedState.valueEnum, _OperationEnum.idle); + expect(pipedEnable.value, LogicValue.zero); + reset.inject(0); + + packet.opcode.inject(1); + packet.lanes.arrayElements[0].data.inject(0x10); + array.arrayElements[0].data.inject(0x20); + state.inject(_OperationEnum.active); + enable.inject(0); + await clk.nextPosedge; + + packet.opcode.inject(3); + packet.lanes.arrayElements[0].data.inject(0x30); + array.arrayElements[0].data.inject(0x40); + state.inject(_OperationEnum.active); + enable.inject(1); + await clk.nextPosedge; + expect(pipedPacket.opcode.value.toInt(), 4); + expect(pipedPacket.lanes.arrayElements[0].data.value.toInt(), 0x30); + expect(pipedArray.arrayElements[0].data.value.toInt(), 0x40); + expect(pipedState.valueEnum, _OperationEnum.active); + expect(pipedEnable.value, LogicValue.zero); + + stall.inject(1); + packet.opcode.inject(6); + array.arrayElements[0].data.inject(0x60); + await clk.nextPosedge; + expect(pipedPacket.opcode.value.toInt(), 4); + expect(pipedArray.arrayElements[0].data.value.toInt(), 0x40); + await Simulator.endSimulation(); + }); + + test('Pipeline treats constant stalls as active-high', () async { + final clk = SimpleClockGenerator(10).clk; + final input = _TypedPacket(name: 'constant_stall_input'); + final running = Pipeline( + clk, + stalls: [Const(0)], + stages: [ + (p) => [p.get(input) < p.get(input)], + ], + ); + final stalled = Pipeline( + clk, + stalls: [Const(1)], + stages: [ + (p) => [p.get(input) < p.get(input)], + ], + ); + final runningOutput = running.get(input); + final stalledOutput = stalled.get(input); + unawaited(Simulator.run()); + + input.opcode.inject(6); + await clk.nextPosedge; + expect(runningOutput.opcode.value.toInt(), 6); + expect(stalledOutput.opcode.value.isValid, isFalse); + await Simulator.endSimulation(); + }); + + test('ReadyValidPipeline preserves structured signals under backpressure', + () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + final valid = Logic(); + final ready = Logic(); + final input = _TypedPacket(name: 'ready_valid_pipeline_input'); + final pipeline = ReadyValidPipeline( + clk, + valid, + ready, + reset: reset, + resetValues: {input: 0}, + stages: [ + (p) { + final source = p.get(input); + final target = p.get(input); + return [ + target.opcode < source.opcode + 1, + target.lanes < source.lanes, + ]; + }, + (p) { + final source = p.get(input); + final target = p.get(input); + return [ + target.opcode < source.opcode + 1, + target.lanes < source.lanes, + ]; + }, + ], + ); + final output = pipeline.get(input); + unawaited(Simulator.run()); + + reset.inject(1); + valid.inject(0); + ready.inject(1); + await clk.nextPosedge; + reset.inject(0); + + input.opcode.inject(2); + input.lanes.arrayElements[0].data.inject(0x45); + valid.inject(1); + await clk.nextPosedge; + expect(pipeline.validPipeOut.value.toInt(), 0); + + await clk.nextPosedge; + expect(pipeline.validPipeOut.value.toInt(), 1); + expect(output.opcode.value.toInt(), 4); + expect(output.lanes.arrayElements[0].data.value.toInt(), 0x45); + + ready.inject(0); + input.opcode.inject(5); + await clk.nextPosedge; + expect(pipeline.validPipeOut.value.toInt(), 1); + expect(output.opcode.value.toInt(), 4); + + ready.inject(1); + await clk.nextPosedge; + expect(output.opcode.value.toInt(), 4); + await clk.nextPosedge; + expect(output.opcode.value.toInt(), 7); + await Simulator.endSimulation(); + }); + + test('ReadyValidPipeline preserves scalar signals', () async { + final clk = SimpleClockGenerator(10).clk; + final reset = Logic(); + final valid = Logic(); + final ready = Logic(); + final input = Logic(width: 4); + final pipeline = ReadyValidPipeline( + clk, + valid, + ready, + reset: reset, + stages: [ + (p) => [p.get(input) < p.get(input)], + ], + ); + final output = pipeline.get(input); + unawaited(Simulator.run()); + + reset.inject(1); + valid.inject(0); + ready.inject(1); + await clk.nextPosedge; + reset.inject(0); + + input.inject(0xa); + valid.inject(1); + await clk.nextPosedge; + expect(pipeline.validPipeOut.value.toInt(), 1); + expect(output.value.toInt(), 0xa); + await Simulator.endSimulation(); + }); + + test('all structure-preserving operations compile to SystemVerilog', + () async { + final harness = _TypedOperationsSynthesisHarness( + Logic(), + Logic(), + Logic(), + Logic(width: 2), + _TypedPacket(name: 'first'), + _TypedPacket(name: 'second'), + ); + await harness.build(); + + SimCompare.checkIverilogVector(harness, const [], buildOnly: true); + }); +} diff --git a/test/typed_port_test.dart b/test/typed_port_test.dart index 2f05069f2f..d834b43970 100644 --- a/test/typed_port_test.dart +++ b/test/typed_port_test.dart @@ -162,6 +162,19 @@ class StructWithConst extends LogicStructure { StructWithConst clone({String? name}) => StructWithConst(name: name); } +class StructWithConstAndDriveableClone extends LogicStructure { + Logic get data => elements.single; + + StructWithConstAndDriveableClone.constant({String? name}) + : this._(Const(1), name: name); + + StructWithConstAndDriveableClone._(Logic data, {super.name}) : super([data]); + + @override + StructWithConstAndDriveableClone clone({String? name}) => + StructWithConstAndDriveableClone._(Logic(), name: name); +} + class ModuleWithPartialAssignInlineAndOutReuseModule extends Module { ModuleWithPartialAssignInlineAndOutReuseModule(MyStruct inp) { inp = addTypedInput('inp', inp); @@ -347,6 +360,15 @@ void main() { expect(failed, portType == 'inOut'); }); } + + test('input with const structure and driveable clone', () { + final source = StructWithConstAndDriveableClone.constant(); + final input = DummyModule().addTypedInput('p', source); + + expect(input, isA()); + expect(input.hasConsts, isFalse); + expect(input.data.value, LogicValue.one); + }); }); group('simple struct module with nets', () { diff --git a/test/unassignable_test.dart b/test/unassignable_test.dart index dcbe039619..26eb1f36f1 100644 --- a/test/unassignable_test.dart +++ b/test/unassignable_test.dart @@ -63,7 +63,7 @@ void main() { out <= Logic(); fail('Should have thrown an exception'); } on UnassignableException catch (e) { - expect(e.toString(), contains('${out.parentModule.runtimeType}')); + expect(e.toString(), contains(out.parentModule!.definitionName)); } }); } diff --git a/test/verilator_compilation_test.dart b/test/verilator_compilation_test.dart index db5fcaea33..758c7694d9 100644 --- a/test/verilator_compilation_test.dart +++ b/test/verilator_compilation_test.dart @@ -37,10 +37,10 @@ endmodule class VectorArrayFixture extends Module { VectorArrayFixture() { final array = addInputArray( - 'array', LogicArray([3], 4, numUnpackedDimensions: 1), - dimensions: [3], elementWidth: 4, numUnpackedDimensions: 1); + 'array', LogicArray([2], 2, numUnpackedDimensions: 1), + dimensions: [2], elementWidth: 2, numUnpackedDimensions: 1); addOutputArray('observed', - dimensions: [3], elementWidth: 4, numUnpackedDimensions: 1) <= + dimensions: [2], elementWidth: 2, numUnpackedDimensions: 1) <= array; } } @@ -100,8 +100,8 @@ void main() { final module = VectorArrayFixture(); await module.build(); SimCompare.checkVerilatorVector(module, [ - Vector({'array': 0x123}, {'observed': 0x123}), - Vector({'array': 0xabc}, {'observed': 0xabc}), + Vector({'array': 0x3}, {'observed': 0x3}), + Vector({'array': 0xc}, {'observed': 0xc}), Vector({'array': 0}, {'observed': 0}), ]); }); @@ -112,7 +112,7 @@ void main() { SimCompare.checkVerilatorVector( module, [ - Vector({'array': 0x123}, {'observed': 0x321}), + Vector({'array': 0x3}, {'observed': 0x2}), ], buildOnly: true); }); @@ -126,8 +126,8 @@ void main() { TestFailure? failure; try { SimCompare.checkVerilatorVector(module, [ - Vector({'array': 0xabc}, {'observed': 0xabc}), - Vector({'array': 0x123}, {'observed': 0x321}), + Vector({'array': 0xc}, {'observed': 0xc}), + Vector({'array': 0x3}, {'observed': 0x2}), ]); } on TestFailure catch (error) { failure = error; @@ -333,8 +333,8 @@ esac if (!capture.run(() => SimCompare.checkVerilatorVector( module, [ - Vector({'array': 0x123}, {'observed': 0x123}), - Vector({'array': 0xabc}, {'observed': 0xabc}), + Vector({'array': 0x3}, {'observed': 0x3}), + Vector({'array': 0xc}, {'observed': 0xc}), ], buildOnly: buildOnly, dumpWaves: true,