Skip to content

Commit eb381bb

Browse files
authored
Fix nested array naming in typed structure port synthesis (#698)
1 parent a4ca8b3 commit eb381bb

2 files changed

Lines changed: 227 additions & 3 deletions

File tree

lib/src/synthesizers/utilities/synth_module_definition.dart

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,23 @@ class SynthModuleDefinition {
320320
/// definition.
321321
final List<Module> supportingModules = [];
322322

323+
/// Retains [signal] and every array ancestor needed to name and declare it.
324+
void _retainInternalSignal(SynthLogic signal) {
325+
final signalAndAncestors = <SynthLogic>[signal];
326+
while (signal is SynthLogicArrayElement) {
327+
signal = signal.parentArray.resolved;
328+
signalAndAncestors.add(signal);
329+
}
330+
331+
for (final retainedSignal in signalAndAncestors.reversed) {
332+
if (!inputs.contains(retainedSignal) &&
333+
!outputs.contains(retainedSignal) &&
334+
!inOuts.contains(retainedSignal)) {
335+
internalSignals.add(retainedSignal);
336+
}
337+
}
338+
}
339+
323340
/// Takes all the leaf elements of [port] and drives [port] with them, each
324341
/// with a partial assignment.
325342
///
@@ -334,7 +351,7 @@ class SynthModuleDefinition {
334351
var idx = 0;
335352
for (final leafElement in port.leafElements) {
336353
final leafSynth = getSynthLogic(leafElement)!;
337-
internalSignals.add(leafSynth);
354+
_retainInternalSignal(leafSynth);
338355
assignments.add(
339356
PartialSynthAssignment(
340357
leafSynth,
@@ -358,7 +375,7 @@ class SynthModuleDefinition {
358375
var idx = 0;
359376
for (final leafElement in port.leafElements) {
360377
final leafSynth = getSynthLogic(leafElement)!;
361-
internalSignals.add(leafSynth);
378+
_retainInternalSignal(leafSynth);
362379

363380
// this is DISCONNECTED, just a module used for synthesizing
364381
final subsetMod = _BusSubsetForStructSlice(
@@ -541,7 +558,7 @@ class SynthModuleDefinition {
541558
!inOuts.contains(synthReceiver),
542559
'Internal signals should not be ports also.',
543560
);
544-
internalSignals.add(synthReceiver);
561+
_retainInternalSignal(synthReceiver);
545562
}
546563

547564
final receiverIsSubmoduleInOut =
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// Copyright (C) 2026 Intel Corporation
2+
// SPDX-License-Identifier: BSD-3-Clause
3+
//
4+
// nested_array_struct_port_synthesis_test.dart
5+
// Tests synthesis of nested LogicArray fields in typed LogicStructure ports.
6+
//
7+
// 2026 August 18
8+
// Author: Max Korbel <max.korbel@intel.com>
9+
10+
import 'package:rohd/rohd.dart';
11+
import 'package:rohd/src/utilities/simcompare.dart';
12+
import 'package:test/test.dart';
13+
14+
class ArrayRecord extends LogicStructure {
15+
final Logic header;
16+
final LogicArray entries;
17+
final Logic trailer;
18+
19+
factory ArrayRecord({
20+
String name = 'arrayRecord',
21+
List<int> dimensions = const [3],
22+
int numUnpackedDimensions = 0,
23+
}) =>
24+
ArrayRecord._(
25+
Logic(name: 'header', width: 2),
26+
LogicArray(
27+
dimensions,
28+
4,
29+
name: 'entries',
30+
numUnpackedDimensions: numUnpackedDimensions,
31+
),
32+
Logic(name: 'trailer', width: 3),
33+
name: name,
34+
);
35+
36+
ArrayRecord._(
37+
this.header,
38+
this.entries,
39+
this.trailer, {
40+
required String name,
41+
}) : super([header, entries, trailer], name: name);
42+
43+
@override
44+
ArrayRecord clone({String? name}) => ArrayRecord(
45+
name: name ?? this.name,
46+
dimensions: entries.dimensions,
47+
numUnpackedDimensions: entries.numUnpackedDimensions,
48+
);
49+
}
50+
51+
class ArrayRecordProducer extends Module {
52+
late final ArrayRecord outputData;
53+
54+
ArrayRecordProducer({
55+
required List<int> dimensions,
56+
required int numUnpackedDimensions,
57+
}) {
58+
outputData = addTypedOutput(
59+
'outputData',
60+
({name = 'outputData'}) => ArrayRecord(
61+
name: name,
62+
dimensions: dimensions,
63+
numUnpackedDimensions: numUnpackedDimensions,
64+
),
65+
);
66+
67+
outputData.header <= Const(0, width: outputData.header.width);
68+
for (final entry in outputData.entries.leafElements) {
69+
entry <= Const(0, width: entry.width);
70+
}
71+
outputData.trailer <= Const(0, width: outputData.trailer.width);
72+
}
73+
}
74+
75+
class ArrayRecordConsumer extends Module {
76+
late final Logic observed;
77+
78+
ArrayRecordConsumer(ArrayRecord source, {required bool consumeInput}) {
79+
final inputData = addTypedInput('inputData', source);
80+
81+
observed = addOutput('observed')
82+
..gets(
83+
consumeInput ? inputData.entries.leafElements.first[0] : Const(0),
84+
);
85+
}
86+
}
87+
88+
class ArrayRecordHierarchy extends Module {
89+
ArrayRecordHierarchy({
90+
List<int> dimensions = const [3],
91+
int numUnpackedDimensions = 0,
92+
bool consumeInput = false,
93+
bool connectProducerDirectly = false,
94+
}) {
95+
final producer = ArrayRecordProducer(
96+
dimensions: dimensions,
97+
numUnpackedDimensions: numUnpackedDimensions,
98+
);
99+
final source = connectProducerDirectly
100+
? producer.outputData
101+
: ArrayRecord(
102+
name: 'intermediate',
103+
dimensions: dimensions,
104+
numUnpackedDimensions: numUnpackedDimensions,
105+
);
106+
final consumer = ArrayRecordConsumer(
107+
source,
108+
consumeInput: consumeInput,
109+
);
110+
111+
if (!connectProducerDirectly) {
112+
source.gets(producer.outputData);
113+
}
114+
addOutput('observed').gets(consumer.observed);
115+
}
116+
}
117+
118+
class RootArrayPassThrough extends Module {
119+
late final LogicArray outputData;
120+
121+
RootArrayPassThrough(LogicArray source) {
122+
final inputData = addTypedInput('inputData', source);
123+
outputData = addTypedOutput('outputData', inputData.clone)..gets(inputData);
124+
}
125+
}
126+
127+
void main() {
128+
tearDown(Simulator.reset);
129+
130+
group('nested array structure input synthesis', () {
131+
final testCases = [
132+
(
133+
name: 'unused one-dimensional packed array',
134+
dimensions: const [3],
135+
numUnpackedDimensions: 0,
136+
consumeInput: false,
137+
connectProducerDirectly: false,
138+
expectedDeclaration: 'logic [2:0][3:0] inputData_entries;',
139+
),
140+
(
141+
name: 'unused multi-dimensional packed array',
142+
dimensions: const [2, 3],
143+
numUnpackedDimensions: 0,
144+
consumeInput: false,
145+
connectProducerDirectly: false,
146+
expectedDeclaration: 'logic [1:0][2:0][3:0] inputData_entries;',
147+
),
148+
(
149+
name: 'unused array with an unpacked dimension',
150+
dimensions: const [2, 3],
151+
numUnpackedDimensions: 1,
152+
consumeInput: false,
153+
connectProducerDirectly: false,
154+
expectedDeclaration: 'logic [2:0][3:0] inputData_entries [1:0];',
155+
),
156+
(
157+
name: 'internally consumed nested array input',
158+
dimensions: const [3],
159+
numUnpackedDimensions: 0,
160+
consumeInput: true,
161+
connectProducerDirectly: false,
162+
expectedDeclaration: null,
163+
),
164+
(
165+
name: 'producer output connected directly',
166+
dimensions: const [3],
167+
numUnpackedDimensions: 0,
168+
consumeInput: false,
169+
connectProducerDirectly: true,
170+
expectedDeclaration: null,
171+
),
172+
];
173+
174+
for (final testCase in testCases) {
175+
test(testCase.name, () async {
176+
final module = ArrayRecordHierarchy(
177+
dimensions: testCase.dimensions,
178+
numUnpackedDimensions: testCase.numUnpackedDimensions,
179+
consumeInput: testCase.consumeInput,
180+
connectProducerDirectly: testCase.connectProducerDirectly,
181+
);
182+
await module.build();
183+
184+
final generated = module.generateSynth();
185+
186+
expect(generated, contains('module ArrayRecordHierarchy'));
187+
expect(generated, contains('.inputData('));
188+
if (testCase.expectedDeclaration case final expectedDeclaration?) {
189+
expect(generated, contains(expectedDeclaration));
190+
}
191+
SimCompare.checkIverilogVector(module, [], buildOnly: true);
192+
});
193+
}
194+
});
195+
196+
test('root typed LogicArray ports retain their declaration shape', () async {
197+
final module = RootArrayPassThrough(LogicArray([2, 3], 4));
198+
await module.build();
199+
200+
final generated = module.generateSynth();
201+
202+
expect(generated, contains('input logic [1:0][2:0][3:0] inputData'));
203+
expect(generated, contains('output logic [1:0][2:0][3:0] outputData'));
204+
expect(generated, contains('assign outputData = inputData;'));
205+
SimCompare.checkIverilogVector(module, [], buildOnly: true);
206+
});
207+
}

0 commit comments

Comments
 (0)