Skip to content
6 changes: 6 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
{
"image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04",

"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "24"
}
},

"updateContentCommand": "tool/gh_codespaces/run_setup.sh",

"customizations": {
Expand Down
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
## Next Release

- Added structure-preserving typed operations for multiplexing, flip-flops,
case and indexed selection, pipelines, naming, cloning, and pass-through
modules. Nested `LogicArray` and `LogicArrayOf` boundaries remain available
through the concrete output type while existing scalar operations retain
their `Logic` APIs.
- Added the `ModuleService` API for module-scoped generation, capture, and
inspection services. `ModuleServices` registers and looks up services for a
built module hierarchy, and `hierarchyJson` exposes its hierarchy as JSON.
- Added `ArtifactProducingService` and `ModuleServiceArtifact` for
transport-neutral output. Artifact-producing services default
`outputDirectory` to the current directory and `outputBaseName` to the
module definition name, and expose named, media-typed byte streams without
requiring filesystem output.
- Added `SystemVerilogService` for configured SystemVerilog synthesis,
in-memory source output, artifact inspection, and explicit directory writes.
Added `WaveformService` for in-memory waveform capture with optional file
writing through `writeToFile`.
- Added legacy-compatible `Module.dumpSystemVerilog` and `Module.dumpWaves`
convenience methods. `dumpSystemVerilog()` returns simple in-memory output;
`dumpWaves()` provides standard VCD capture as the replacement for
`WaveDumper`. `WaveDumper` and `generateSynth` are deprecated in favor of
these `Module` methods.

## 0.6.10

- Improved `Logic.replicate(1)` and same-width `signExtend` to return the original signal, eliminating redundant replication modules and generated SystemVerilog (<https://github.com/intel/rohd/pull/689>).
Expand Down
95 changes: 94 additions & 1 deletion doc/user_guide/_docs/A19-logic-structures.md
Original file line number Diff line number Diff line change
@@ -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
---

Expand All @@ -17,6 +17,79 @@ 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

`StructureMux`, `StructureFlipFlop`, and `StructurePassthrough` preserve a
concrete `LogicStructure` type when their structure operands match:

```dart
final selected = StructureMux(select, packet1, packet0).out;
final registered =
StructureFlipFlop(clk, selected, reset: reset).q;
final forwarded = StructurePassthrough(registered).out;

// All three values have type Packet.
forwarded.valid <= selected.valid;
```

The same behavior applies to `LogicArray` and `LogicArrayOf<T>`, including nested typed arrays. Both mux operands must have the same concrete type and recursive shape: field widths, array dimensions, and packing hints must match.

Typed 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.

The existing `Mux`, `FlipFlop`, and `Passthrough` APIs always produce ordinary
`Logic`. They accept structures as packed inputs when widths match, but do not
preserve named fields:

```dart
final Logic selected = Mux(select, packet1, packet0).out;
final Logic registered = FlipFlop(clk, selected).q;
```

### 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 `typedCases` when the operation should construct and return a value while
preserving its concrete type:

```dart
final Packet selected = typedCases(
selector,
{0: packet0, 1: packet1},
defaultValue: fallback,
);
```

All structured values passed to `typedCases` must have the same concrete type
and recursive shape. The legacy `cases` helper accepts structures as packed
values but always returns an ordinary `Logic`.

Use `selectIndexTyped` and `selectFromTyped` for structure-preserving indexed
selection. `StructurePipeline<T>` preserves the same concrete type at every
registered pipeline boundary. Specify `T` when creating a pipeline with inline
stage transforms so Dart can type the transform parameter.

## 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.
Expand All @@ -41,6 +114,26 @@ rvStruct.elements[0] <= ready;
rvStruct.elements[1] <= valid;
```

## Promoting nested fields

Use `flattenOuter` to promote fields from direct child structures into a new generic `LogicStructure`. The new fields are clones connected to their original sources, so the original structure remains unchanged. By default, promoted names are prefixed with the direct child structure name to avoid collisions.

```dart
final config = LogicStructure([
Logic(name: 'mode', width: 2),
Logic(name: 'valid'),
], name: 'config');
final control = LogicStructure([
Logic(name: 'enable'),
config,
], name: 'control');

final flattened = control.flattenOuter();
// Field names: enable, config_mode, config_valid.
```

Only direct non-array child structures are promoted. Nested grandchildren remain structures, and a duplicate resulting field name throws `LogicConstructionException`.

## Making your own structure

Referencing elements by index is often not ideal for named signals. We can do better by building our own structure that inherits from `LogicStructure`.
Expand Down
101 changes: 100 additions & 1 deletion doc/user_guide/_docs/A20-logic-arrays.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "Logic Arrays"
permalink: /docs/logic-arrays/
last_modified_at: 2022-6-5
last_modified_at: 2026-9-8
toc: true
---

Expand All @@ -22,6 +22,87 @@ 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.

## 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 `LogicArrayOf<T>` when every leaf has the same specialized `Logic` type. It preserves the normal array dimensions while exposing typed leaves with `typedLeafElements` and `elementAt`. `LogicArray` is `LogicArrayOf<Logic>`, so it retains its existing construction, port, clone, naming, and array APIs while also identifying its leaves as `Logic`. For example, this creates a two-dimensional array of samples with separate data and valid fields:

```dart
class Sample extends LogicStructure {
final Logic data;
final Logic valid;

factory Sample({String? name}) => Sample._(
Logic(name: 'data', width: 8),
Logic(name: 'valid'),
name: name ?? 'sample',
);

Sample._(this.data, this.valid, {required String name})
: super([data, valid], name: name);

@override
Sample clone({String? name}) => Sample(name: name ?? this.name);
}

final samples = LogicArrayOf<Sample>(
[2, 3],
Sample.new,
dimensionNames: ['row_', 'column_'],
);

final bottomRightData = samples.elementAt([1, 2]).data;
```

When typed array leaves are themselves arrays, use `flattenNestedDimensions<U>()` to create one rectangular `LogicArrayOf<U>` with all nested dimensions concatenated. The full address is preserved: `nested.elementAt(outerIndex).elementAt(innerIndex)` maps to `flattened.elementAt([...outerIndex, ...innerIndex])`. Every sibling nested array must have matching dimensions and leaf width.

Use `LogicValueArray` for fixed-width array data outside the hardware graph. It keeps values in row-major order and supports indexing, reshaping, transposition, and slice operations. `LogicValueArrayOf` adds a codec so application-level values can use the same operations while converting to and from packed `LogicValue`s.

```dart
final values = LogicValueArray.fromInts([2, 3], 8, [1, 2, 3, 4, 5, 6]);
final transposed = values.transpose2D(); // Dimensions: [3, 2]

final signals = values.toLogicArray(name: 'values');
```

`LogicValueArray.putInto` drives a compatible `LogicArray` or `LogicArrayOf`, while `LogicArrayOf.logicValues` captures its current packed values. Use `LogicArrayOf.valueArrayOf` and `putValueArrayOf` when a `LogicValueCodec` converts typed value-domain data at the hardware boundary.

## Unpacked arrays

In SystemVerilog, there is a concept of "packed" vs. "unpacked" arrays which have different use cases and capabilities. In ROHD, all arrays act the same and you get the best of both worlds. You can indicate when constructing a `LogicArray` that some number of the dimensions should be "unpacked" as a hint to `Synthesizer`s. Marking an array with a non-zero `numUnpackedDimensions`, for example, will make that many of the dimensions "unpacked" in generated SystemVerilog signal declarations.
Expand All @@ -43,6 +124,24 @@ You can declare ports of `Module`s as being arrays (including with some dimensio

Array ports in generated SystemVerilog will match dimensions (including unpacked) as specified when the port is created.

Use `addTypedInput` and `addTypedOutput` for `LogicArrayOf` ports. These methods preserve the array's specialized leaf type, allowing the module to access fields such as `samples.elementAt([1, 2]).data` directly.

## Type-preserving operations

Use `StructureMux`, `StructureFlipFlop`, and `StructurePassthrough` when the
output must retain the array's concrete type, dimensions, and specialized leaf
type:

```dart
final selected = StructureMux(select, samplesA, samplesB).out;
final delayed = StructureFlipFlop(clk, selected, reset: reset).q;
final forwarded = StructurePassthrough(delayed).out;

final bottomRightData = forwarded.elementAt([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 `typedCases`, `selectIndexTyped`, or `selectFromTyped` when selecting one complete typed array from multiple choices. Specify the array type parameter on `StructurePipeline<T>` when its stages use inline transforms.

## Elements of arrays

To iterate through or access elements of a `LogicArray` (or bits of a simple `Logic`), use [`elements`](https://intel.github.io/rohd/rohd/Logic/elements.html). Using the normal `[n]` accessors will return the `n`th bit regardless for `LogicArray` and `Logic` to maintain API consistency.
Expand Down
91 changes: 53 additions & 38 deletions lib/src/interfaces/interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,43 +85,53 @@ class Interface<TagType extends Enum> {
if (inputTags != null) {
for (final port in getPorts(inputTags).values) {
port <=
(port is LogicArray
? module.addInputArray(
uniquify(port.name),
srcInterface.port(port.name),
dimensions: port.dimensions,
elementWidth: port.elementWidth,
numUnpackedDimensions: port.numUnpackedDimensions,
)
: module.addInput(
uniquify(port.name),
srcInterface.port(port.name),
width: port.width,
));
switch (port) {
LogicArray() => module.addInputArray(
uniquify(port.name),
srcInterface.port(port.name),
dimensions: port.dimensions,
elementWidth: port.elementWidth,
numUnpackedDimensions: port.numUnpackedDimensions,
),
BaseLogicArray() => module.addTypedInput(
uniquify(port.name),
srcInterface.port(port.name) as BaseLogicArray,
),
_ => module.addInput(
uniquify(port.name),
srcInterface.port(port.name),
width: port.width,
),
};
}
}

if (outputTags != null) {
for (final port in getPorts(outputTags).values) {
final output = (port is LogicArray
? module.addOutputArray(
uniquify(port.name),
dimensions: port.dimensions,
elementWidth: port.elementWidth,
numUnpackedDimensions: port.numUnpackedDimensions,
)
: module.addOutput(
uniquify(port.name),
width: port.width,
));
final output = switch (port) {
LogicArray() => module.addOutputArray(
uniquify(port.name),
dimensions: port.dimensions,
elementWidth: port.elementWidth,
numUnpackedDimensions: port.numUnpackedDimensions,
),
BaseLogicArray() => module.addTypedOutput(
uniquify(port.name),
port.clone,
),
_ => module.addOutput(
uniquify(port.name),
width: port.width,
),
};
output <= port;
srcInterface.port(port.name) <= output;
}
}

if (inOutTags != null) {
for (final port in getPorts(inOutTags).values) {
if (port is LogicArray) {
if (port is BaseLogicArray) {
if (!port.isNet) {
throw PortTypeException(
port, 'LogicArray nets must be used for inOut array ports.');
Expand All @@ -132,19 +142,24 @@ class Interface<TagType extends Enum> {
}

port <=
(port is LogicArray
? module.addInOutArray(
uniquify(port.name),
srcInterface.port(port.name),
dimensions: port.dimensions,
elementWidth: port.elementWidth,
numUnpackedDimensions: port.numUnpackedDimensions,
)
: module.addInOut(
uniquify(port.name),
srcInterface.port(port.name),
width: port.width,
));
switch (port) {
LogicArray() => module.addInOutArray(
uniquify(port.name),
srcInterface.port(port.name),
dimensions: port.dimensions,
elementWidth: port.elementWidth,
numUnpackedDimensions: port.numUnpackedDimensions,
),
BaseLogicArray() => module.addTypedInOut(
uniquify(port.name),
srcInterface.port(port.name) as BaseLogicArray,
),
_ => module.addInOut(
uniquify(port.name),
srcInterface.port(port.name),
width: port.width,
),
};
}
}
}
Expand Down
Loading
Loading