From 75e7736019111565ecf31f8245aeed6dd80bbf84 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 5 Jul 2026 15:17:29 -0700 Subject: [PATCH 1/6] Add ROHD hierarchy package --- analysis_options.yaml | 1 + packages/rohd_hierarchy/LICENSE | 28 + packages/rohd_hierarchy/README.md | 243 ++++ packages/rohd_hierarchy/analysis_options.yaml | 1 + .../rohd_hierarchy/lib/rohd_hierarchy.dart | 50 + .../lib/src/base_hierarchy_adapter.dart | 80 ++ .../lib/src/hierarchy_models.dart | 15 + .../lib/src/hierarchy_occurrence.dart | 253 ++++ .../lib/src/hierarchy_query.dart | 152 +++ .../lib/src/hierarchy_search_controller.dart | 216 +++ .../lib/src/hierarchy_search_result.dart | 60 + .../lib/src/hierarchy_service.dart | 872 ++++++++++++ .../lib/src/netlist_hierarchy_adapter.dart | 224 ++++ .../lib/src/occurrence_address.dart | 142 ++ .../lib/src/occurrence_search_result.dart | 45 + .../rohd_hierarchy/lib/src/prefix_query.dart | 60 + .../rohd_hierarchy/lib/src/regex_query.dart | 176 +++ .../lib/src/signal_occurrence.dart | 273 ++++ .../lib/src/signal_search_result.dart | 49 + packages/rohd_hierarchy/pubspec.yaml | 19 + .../test/adapter_search_parity_test.dart | 285 ++++ .../test/address_conversion_test.dart | 286 ++++ .../test/coverage_gaps_test.dart | 120 ++ .../test/devtools_search_flow_test.dart | 214 +++ .../test/filter_bank_integration_test.dart | 710 ++++++++++ .../test/fixtures/filter_bank.json | 1183 +++++++++++++++++ .../hierarchy_path_vs_signal_id_test.dart | 194 +++ .../test/hierarchy_query_test.dart | 655 +++++++++ .../hierarchy_search_controller_test.dart | 505 +++++++ .../test/module_search_test.dart | 325 +++++ .../test/occurrence_address_test.dart | 335 +++++ .../test/regex_search_test.dart | 546 ++++++++ .../test/rohd_signal_resolve_test.dart | 72 + .../test/signal_search_result_test.dart | 236 ++++ 34 files changed, 8625 insertions(+) create mode 100644 packages/rohd_hierarchy/LICENSE create mode 100644 packages/rohd_hierarchy/README.md create mode 100644 packages/rohd_hierarchy/analysis_options.yaml create mode 100644 packages/rohd_hierarchy/lib/rohd_hierarchy.dart create mode 100644 packages/rohd_hierarchy/lib/src/base_hierarchy_adapter.dart create mode 100644 packages/rohd_hierarchy/lib/src/hierarchy_models.dart create mode 100644 packages/rohd_hierarchy/lib/src/hierarchy_occurrence.dart create mode 100644 packages/rohd_hierarchy/lib/src/hierarchy_query.dart create mode 100644 packages/rohd_hierarchy/lib/src/hierarchy_search_controller.dart create mode 100644 packages/rohd_hierarchy/lib/src/hierarchy_search_result.dart create mode 100644 packages/rohd_hierarchy/lib/src/hierarchy_service.dart create mode 100644 packages/rohd_hierarchy/lib/src/netlist_hierarchy_adapter.dart create mode 100644 packages/rohd_hierarchy/lib/src/occurrence_address.dart create mode 100644 packages/rohd_hierarchy/lib/src/occurrence_search_result.dart create mode 100644 packages/rohd_hierarchy/lib/src/prefix_query.dart create mode 100644 packages/rohd_hierarchy/lib/src/regex_query.dart create mode 100644 packages/rohd_hierarchy/lib/src/signal_occurrence.dart create mode 100644 packages/rohd_hierarchy/lib/src/signal_search_result.dart create mode 100644 packages/rohd_hierarchy/pubspec.yaml create mode 100644 packages/rohd_hierarchy/test/adapter_search_parity_test.dart create mode 100644 packages/rohd_hierarchy/test/address_conversion_test.dart create mode 100644 packages/rohd_hierarchy/test/coverage_gaps_test.dart create mode 100644 packages/rohd_hierarchy/test/devtools_search_flow_test.dart create mode 100644 packages/rohd_hierarchy/test/filter_bank_integration_test.dart create mode 100644 packages/rohd_hierarchy/test/fixtures/filter_bank.json create mode 100644 packages/rohd_hierarchy/test/hierarchy_path_vs_signal_id_test.dart create mode 100644 packages/rohd_hierarchy/test/hierarchy_query_test.dart create mode 100644 packages/rohd_hierarchy/test/hierarchy_search_controller_test.dart create mode 100644 packages/rohd_hierarchy/test/module_search_test.dart create mode 100644 packages/rohd_hierarchy/test/occurrence_address_test.dart create mode 100644 packages/rohd_hierarchy/test/regex_search_test.dart create mode 100644 packages/rohd_hierarchy/test/rohd_signal_resolve_test.dart create mode 100644 packages/rohd_hierarchy/test/signal_search_result_test.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index 2b2098177..46241ddcb 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -8,6 +8,7 @@ analyzer: strict-raw-types: true exclude: - doc/tutorials/chapter_9/rohd_vf_example + - packages/rohd_hierarchy - rohd_devtools_extension # keep up to date, matching https://dart.dev/tools/linter-rules/all diff --git a/packages/rohd_hierarchy/LICENSE b/packages/rohd_hierarchy/LICENSE new file mode 100644 index 000000000..cfbbee995 --- /dev/null +++ b/packages/rohd_hierarchy/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (C) 2021-2023 Intel Corporation + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/rohd_hierarchy/README.md b/packages/rohd_hierarchy/README.md new file mode 100644 index 000000000..4b03c519f --- /dev/null +++ b/packages/rohd_hierarchy/README.md @@ -0,0 +1,243 @@ +# rohd_hierarchy + +An incremental design dictionary for hardware module hierarchies. + +## Motivation + +A remote agent — a debugger, a waveform viewer, a schematic renderer, an +AI assistant — needs to understand the structure of a hardware design in order +to ask useful questions about it. Transferring the full design every time is +wasteful. What both sides of a link really need is a shared **dictionary** of +the design: the modules, occurrences, and signals that make it up, plus +a compact way to refer to any object by address. + +Once both sides share the same dictionary, communication becomes cheap: +either side can request data about a specific object by its address alone, +without re-transmitting structural context. + +### What is a design dictionary? + +A design dictionary captures the **hierarchy and connectivity** of a +hardware design: + +- **Occurrences** — unfolded instances of module definitions in the + hierarchy tree. Each has a `name`, an optional `definition` (the + module type), child occurrences, and signals. +- **Signals** — named wires within an occurrence. Each has a `name`, + `width`, optional `direction` (input/output/inout), and optional + `value`. + +The full "unfolded" view of a design is its **address space**: every +occurrence and every signal reachable by walking the hierarchy tree. + +### Compact, canonical addressing + +`rohd_hierarchy` assigns each object a **canonical address** — a short +sequence of child indices (e.g. `0.2.4`) that uniquely identifies it within +the tree. + +Addresses are **relative within each occurrence**: an occurrence's address +table maps local indices to its children and signals without relying on any +global namespace. This locality property is what makes the dictionary +**incrementally expandable** — a remote agent can: + +1. Request the top-level dictionary table (the root occurrence's children + and signals). +2. Drill into any child by requesting that child's dictionary table. +3. Continue expanding only the parts of the hierarchy it actually needs. + +At each step, both sides agree on the addresses, so subsequent data +requests (waveform samples, signal values, schematic fragments) carry +only the compact address, not the full path or structural description. + +## Package overview + +`rohd_hierarchy` is a source-agnostic Dart package that implements this +dictionary model. It provides data models, search utilities, and adapter +interfaces that work independently of any particular HDL toolchain or +transport layer. + +### Data models + +- **`HierarchyOccurrence`** — An occurrence of a module definition in the + unfolded hierarchy tree, with children, signals, name, an optional + `definition` (module type), and a primitive flag. Call `buildAddresses()` + to assign a canonical `OccurrenceAddress` to every occurrence and signal + in O(n). Use `signalCount` and `computedSignalCount` for efficient + subtree counts. +- **`OccurrenceAddress`** — An immutable, index-based path through the + tree (e.g. `[0, 2, 4]`). Supports conversion to/from dot-separated + strings. Works as an O(1) cache key. +- **`SignalOccurrence`** — Signal metadata: name, width, optional + direction, and optional value. Signals with a `direction` serve as + ports (input, output, inout). + +### Services & adapters + +- **`HierarchyService`** — A mixin providing tree-walking search and + navigation: `searchSignals()`, `searchOccurrences()`, + `autocompletePaths()`, regex/glob search (`searchSignalsRegex()`, + `searchOccurrencesRegex()`), and address↔pathname conversion. +- **`BaseHierarchyAdapter`** — An abstract class wrapping a + `HierarchyOccurrence` tree with `HierarchyService`. Use + `BaseHierarchyAdapter.fromTree()` to wrap an existing tree. +- **`NetlistHierarchyAdapter`** — A concrete adapter that parses netlist + JSON into a `HierarchyOccurrence` tree. + +### Search queries + +- **`HierarchyQuery`** — Abstract base class for pluggable search + strategies. The matching logic is decoupled from tree traversal. +- **`PrefixQuery`** — Prefix-substring matching. Segments split on `/` + or `.` are matched case-insensitively via `startsWith` (signals) or + `contains` (occurrences). Created via `HierarchyQuery.prefix()`. +- **`RegexQuery`** — Regex/glob matching. Each segment is compiled as a + regex. Supports `*` (any chars), `?` (one char), `**` (zero or more + hierarchy levels), character classes (`[0-9]`), alternation + (`(clk|reset)`), and quantifiers. Created via `HierarchyQuery.regex()`. + +### Search controller + +- **`HierarchySearchController`** — A pure-Dart controller for + keyboard-navigable search result lists, with `updateQuery()`, + `selectNext()` / `selectPrevious()`, `tabComplete()`, and scroll-offset + helpers. Factories `forSignals()` and `forOccurrences()` cover the + common cases. + +## Usage + +### Building a dictionary from a netlist + +```dart +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; + +final dict = NetlistHierarchyAdapter.fromJson(netlistJsonString); +final root = dict.root; // the top-level dictionary table +``` + +### Wrapping an existing tree + +When you already have a `HierarchyOccurrence` tree (e.g. from a VCD +parser, a ROHD simulation, or any other source), wrap it to gain search +and address resolution: + +```dart +final dict = BaseHierarchyAdapter.fromTree(rootNode); +``` + +### Incremental expansion by a remote agent + +A remote agent does not need the full tree up front. It can expand the +dictionary one level at a time: + +```dart +// Agent receives the root table +final root = dict.root; + +// Agent picks a child to expand (e.g. child 2) +final child = root.children[2]; + +// The child's own children and signals are its local dictionary table. +// The agent now knows addresses 2.0, 2.1, ... for that subtree. +``` + +### Compact address-based communication + +Once both sides share the dictionary, data requests use addresses only: + +```dart +// Resolve a human-readable pathname to a canonical address +final addr = dict.pathnameToAddress('Counter/clk'); + +// Send the compact address over the wire: "0.1" +final wire = addr!.toDotString(); + +// The other side resolves it back +final resolved = dict.occurrenceByAddress(OccurrenceAddress.fromDotString(wire)); +final pathname = dict.addressToPathname(addr!); +``` + +### Searching the dictionary + +#### Prefix search (default) + +Segments are split on `/` or `.` and matched as case-insensitive +substrings: + +```dart +// Find all signals whose path contains 'cpu' then 'clk' +final signals = dict.searchSignals('cpu/clk'); + +// Find occurrences containing 'counter' +final modules = dict.searchOccurrences('counter'); + +// Tab-completion for partial paths +final completions = dict.autocompletePaths('Top/CPU/'); +``` + +#### Regex / glob search + +Each segment is a regex anchored to the full name. Glob wildcards `*` +and `?` are auto-converted. Use `**` to match across hierarchy levels: + +```dart +// All 'clk' signals anywhere in the design +final clocks = dict.searchSignalsRegex('Top/**/clk'); + +// Signals named d0–d15 in any regfile +final data = dict.searchSignalsRegex('Top/**/regfile/d[0-9]+'); + +// Either 'clk' or 'reset' anywhere +final resets = dict.searchSignalsRegex('Top/**/(clk|reset)'); + +// All cache channels ch0–ch2 +final channels = dict.searchOccurrencesRegex('Top/mem_ctrl/ch[0-2]'); + +// Signals containing 'mux' in their name +final muxed = dict.searchSignalsRegex('Top/**/.*mux.*'); + +// All signals in a specific module +final all = dict.searchSignalsRegex('Top/CPU/ALU/*'); +``` + +### Constructing occurrences manually + +```dart +final root = HierarchyOccurrence( + name: 'Counter', + definition: 'Counter', + signals: [ + SignalOccurrence(name: 'clk', width: 1, direction: 'input'), + SignalOccurrence(name: 'count', width: 8, direction: 'output'), + ], + children: [ + HierarchyOccurrence( + name: 'adder', + definition: 'Adder', + signals: [ + SignalOccurrence(name: 'a', width: 8), + SignalOccurrence(name: 'b', width: 8), + SignalOccurrence(name: 'sum', width: 8), + ], + ), + ], +); + +// Assign canonical addresses +root.buildAddresses(); + +// Now every occurrence and signal has an address +print(root.children.first.path()); // 'Counter/adder' +print(root.signals.first.path()); // 'Counter/clk' +``` + +## Design principles + +| Principle | How it is achieved | +|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Source-agnostic** | The data model is independent of any HDL toolchain. `NetlistHierarchyAdapter` handles netlist JSON; `BaseHierarchyAdapter.fromTree()` wraps any tree. | +| **Incremental** | Addresses are relative within each occurrence. A remote agent expands only the subtrees it needs, one dictionary table at a time. | +| **Compact** | `OccurrenceAddress` is a short index path (e.g. `0.2.4`), not a full dotted pathname. Both sides resolve it locally. | +| **Canonical** | `buildAddresses()` assigns deterministic indices in tree order. The same design always produces the same addresses. | +| **No global namespace** | Each occurrence's address table is self-contained. Adding or removing a sibling subtree does not invalidate addresses in unrelated parts of the tree. | +| **Transport-independent** | The package defines the dictionary model, not the wire protocol. Any transport (VM service, JSON-RPC, gRPC, WebSocket) can carry the compact addresses. | diff --git a/packages/rohd_hierarchy/analysis_options.yaml b/packages/rohd_hierarchy/analysis_options.yaml new file mode 100644 index 000000000..f04c6cf0f --- /dev/null +++ b/packages/rohd_hierarchy/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/packages/rohd_hierarchy/lib/rohd_hierarchy.dart b/packages/rohd_hierarchy/lib/rohd_hierarchy.dart new file mode 100644 index 000000000..86ed7c336 --- /dev/null +++ b/packages/rohd_hierarchy/lib/rohd_hierarchy.dart @@ -0,0 +1,50 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// rohd_hierarchy.dart +// Main library export for rohd_hierarchy package. +// +// 2026 January +// Author: Desmond Kirkpatrick + +/// Generic hierarchy data models for hardware module navigation. +/// +/// This library provides source-agnostic data models for representing +/// hardware module hierarchies: +/// +/// ## Core Data Models +/// - `OccurrenceAddress` - Efficient index-based addressing for tree navigation +/// - `HierarchyOccurrence` - An occurrence of a module definition in the tree +/// - `SignalOccurrence` - A signal in the hierarchy +/// +/// ## Search & Navigation +/// - `SignalSearchResult` - Result of a signal search with enriched metadata +/// - `OccurrenceSearchResult` - Result of an occurrence search with metadata +/// - `HierarchyService` - Abstract interface for hierarchy navigation +/// - `HierarchySearchController` - Pure Dart search state controller +/// +/// ## Adapters +/// - `BaseHierarchyAdapter` - Base class with shared adapter implementation +/// - `NetlistHierarchyAdapter` - Adapter for netlist JSON format +/// +/// This package has no dependencies and can be used standalone by any +/// application that needs to navigate hardware hierarchies. +/// +/// ## Quick Start +/// ```dart +/// 1. Create hierarchy +/// final root = HierarchyOccurrence(id: 'top', name: 'top'); +/// root.buildAddresses(); // Enable address-based navigation +/// +/// 2. Search +/// final service = BaseHierarchyAdapter.fromTree(root); +/// final results = service.searchSignals('clk'); +/// ``` +library; + +export 'src/base_hierarchy_adapter.dart'; +export 'src/hierarchy_models.dart'; +export 'src/hierarchy_query.dart'; +export 'src/hierarchy_search_controller.dart'; +export 'src/hierarchy_service.dart'; +export 'src/netlist_hierarchy_adapter.dart'; diff --git a/packages/rohd_hierarchy/lib/src/base_hierarchy_adapter.dart b/packages/rohd_hierarchy/lib/src/base_hierarchy_adapter.dart new file mode 100644 index 000000000..772007497 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/base_hierarchy_adapter.dart @@ -0,0 +1,80 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// base_hierarchy_adapter.dart +// Base class with shared implementation for hierarchy adapters. +// +// 2026 January +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/src/hierarchy_models.dart'; +import 'package:rohd_hierarchy/src/hierarchy_service.dart'; + +/// Base class providing shared implementation for hierarchy adapters. +/// +/// The [HierarchyOccurrence] tree rooted at [root] is the single source of +/// truth. Children and signals are read directly from each occurrence's +/// [HierarchyOccurrence.children] and [HierarchyOccurrence.signals] lists. +/// Lookups use [OccurrenceAddress]-based navigation. +/// +/// Concrete adapters should: +/// 1. Extend this class +/// 2. Build a complete [HierarchyOccurrence] tree (with children and signals +/// populated on each occurrence) +/// 3. Set the [root] occurrence +/// +/// Search, autocomplete, and signal lookup are implemented by +/// [HierarchyService] via recursive tree walking. +abstract class BaseHierarchyAdapter with HierarchyService { + HierarchyOccurrence? _root; + + /// Creates a [BaseHierarchyAdapter]. + BaseHierarchyAdapter(); + + /// Creates an adapter wrapping an existing [HierarchyOccurrence] tree. + /// + /// The tree itself is the single source of truth — children and signals + /// are read directly from the [HierarchyOccurrence] lists. + /// + /// Example usage: + /// ```dart + /// final treeRoot = await dataSource.evalModuleTree(); + /// final service = BaseHierarchyAdapter.fromTree(treeRoot); + /// final paths = service.searchSignalPaths('clk'); + /// ``` + factory BaseHierarchyAdapter.fromTree( + HierarchyOccurrence rootNode, + ) = _TreeBackedAdapter; + + /// Sets the root occurrence. Call this once during initialisation. + set root(HierarchyOccurrence node) { + _root = node; + } + + // ───────────────────────────────────────────────────────────────────────── + // HierarchyService concrete accessors — all tree-walking, no flat maps + // ───────────────────────────────────────────────────────────────────────── + + @override + HierarchyOccurrence get root { + if (_root == null) { + throw StateError( + 'Root occurrence not set. Call setRoot() during initialization.'); + } + return _root!; + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Tree-backed implementation returned by BaseHierarchyAdapter.fromTree() +// ───────────────────────────────────────────────────────────────────────────── + +/// Private adapter that wraps an existing [HierarchyOccurrence] tree. +/// +/// Children and signals are read directly from the tree occurrences. +class _TreeBackedAdapter extends BaseHierarchyAdapter { + _TreeBackedAdapter(HierarchyOccurrence rootNode) { + root = rootNode; + rootNode.buildAddresses(); + } +} diff --git a/packages/rohd_hierarchy/lib/src/hierarchy_models.dart b/packages/rohd_hierarchy/lib/src/hierarchy_models.dart new file mode 100644 index 000000000..6d686e053 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/hierarchy_models.dart @@ -0,0 +1,15 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// hierarchy_models.dart +// Barrel file re-exporting all hierarchy data model classes. +// +// 2026 January +// Author: Desmond Kirkpatrick + +export 'hierarchy_occurrence.dart'; +export 'hierarchy_search_result.dart'; +export 'occurrence_address.dart'; +export 'occurrence_search_result.dart'; +export 'signal_occurrence.dart'; +export 'signal_search_result.dart'; diff --git a/packages/rohd_hierarchy/lib/src/hierarchy_occurrence.dart b/packages/rohd_hierarchy/lib/src/hierarchy_occurrence.dart new file mode 100644 index 000000000..5d991f391 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/hierarchy_occurrence.dart @@ -0,0 +1,253 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// hierarchy_occurrence.dart +// An occurrence of a module definition in the unfolded hierarchy tree. +// +// 2026 May +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd_hierarchy/src/occurrence_address.dart'; +import 'package:rohd_hierarchy/src/signal_occurrence.dart'; + +/// An occurrence of a module definition in the unfolded hierarchy tree. +/// +/// This is the core structural data model, independent of waveform data. +/// Path strings are computed on demand from parent references rather than +/// stored — call [path] with your desired separator. +class HierarchyOccurrence { + /// Display name of this occurrence (instance name within its parent). + final String name; + + /// Definition (module) name for this occurrence. + final String? definition; + + /// Whether this occurrence is a primitive cell (gate, operator, register, + /// etc.) whose internal structure is not useful for design navigation. + /// + /// Set by the parser/adapter that creates the occurrence. The netlist + /// adapter sets this for cells that lack a module definition in the JSON or + /// whose definition starts with `$` (netlist built-in primitives). + /// Tool-specific primitives (e.g. ROHD's FlipFlop → `$dff`) are handled by + /// the synthesizer mapping them to `$`-prefixed definitions before the JSON + /// is written. + final bool isPrimitive; + + /// Signals within this occurrence (includes both internal signals and + /// ports). Empty for leaf occurrences. + final List signals; + + /// Child occurrences. Populated from sub-modules in the hierarchy. + final List children; + + /// Hierarchical address for this occurrence. + /// Assigned by [buildAddresses] to enable efficient navigation. + /// Format: [child0, child1, ..., childN] for nested occurrences. + OccurrenceAddress? get address => _address; + OccurrenceAddress? _address; + + /// Parent occurrence, or `null` for the root. + /// Set by [buildAddresses]. + HierarchyOccurrence? get parent => _parent; + HierarchyOccurrence? _parent; + + /// Creates a [HierarchyOccurrence] with the given properties. + HierarchyOccurrence({ + required this.name, + this.definition, + this.isPrimitive = false, + List? signals, + List? children, + }) : signals = signals ?? [], + children = children ?? []; + + /// Compute the full hierarchical path by walking up the parent chain. + /// + /// Uses [separator] between path segments (default `/`). + /// Returns just [name] for the root (no parent). + String path({String separator = '/'}) { + if (_parent == null) { + return name; + } + final parts = []; + HierarchyOccurrence? cur = this; + while (cur != null) { + parts.add(cur.name); + cur = cur._parent; + } + return parts.reversed.join(separator); + } + + /// Returns only signals that are ports (have a direction). + List get ports => signals.where((s) => s.isPort).toList(); + + // ───────────────── Name → offset (index) lookups ───────────────── + + /// Lazily-built index: child name → offset in [children]. + Map? _childNameIndex; + + /// Lazily-built index: signal name → offset in [signals]. + Map? _signalNameIndex; + + /// Return the offset (index) of the child with [name] in [children], + /// or -1 if not found. Case-sensitive. + /// O(1) after first call (lazily builds index). + int childIndexByName(String name) { + _childNameIndex ??= { + for (var i = 0; i < children.length; i++) children[i].name: i, + }; + return _childNameIndex![name] ?? -1; + } + + /// Return the offset (index) of the signal with [name] in [signals], + /// or -1 if not found. Case-sensitive. + /// O(1) after first call (lazily builds index). + int signalIndexByName(String name) { + _signalNameIndex ??= { + for (var i = 0; i < signals.length; i++) signals[i].name: i, + }; + return _signalNameIndex![name] ?? -1; + } + + /// Whether [cellType] represents a netlist built-in primitive cell type. + /// + /// Returns `true` for `$`-prefixed types (`$mux`, `$dff`, `$and`, etc.) + /// which are netlist built-in operators and primitives. + /// + /// Tool-specific primitive types (e.g. ROHD's `FlipFlop`) should be + /// handled by the producer: the synthesizer should map them to + /// `$`-prefixed cell types in the JSON output, or the adapter should + /// set [isPrimitive] on the occurrence at construction time. + /// + /// Use this before a [HierarchyOccurrence] exists (e.g. when deciding + /// whether to recurse into a netlist cell definition). For an existing + /// occurrence, use the getter [isPrimitiveCell] instead. + static bool isPrimitiveType(String cellType) => cellType.startsWith(r'$'); + + /// Whether this occurrence represents a primitive cell that should be hidden + /// from the occurrence tree. + /// + /// Checks the [isPrimitive] field (set by the adapter at construction time) + /// and falls back to [isPrimitiveType] on the occurrence's [definition]. + bool get isPrimitiveCell => + isPrimitive || (definition != null && isPrimitiveType(definition!)); + + /// Returns only input signals. + List get inputs => + signals.where((s) => s.direction == 'input').toList(); + + /// Returns only output signals. + List get outputs => + signals.where((s) => s.direction == 'output').toList(); + + /// Number of port signals in this occurrence. + int get portCount => signals.where((s) => s.isPort).length; + + /// Finds the sub-field [SignalOccurrence] entries for a struct/array signal. + /// + /// Given a `parentSignal` that has `logicType` metadata (struct fields or + /// array dims), looks up the expected sub-field signal names in this + /// occurrence's signal list using the Namer/Sanitizer naming convention: + /// `{parentSignalName}_{fieldName}` + /// + /// Returns a list of resolved sub-field signals in field order. + /// Entries may be null if a particular sub-field signal wasn't found + /// (e.g. the netlist didn't emit it, or it was optimized away). + List<({SignalOccurrence? signal, String fieldLabel, int width, int startBit})> + findSubFieldSignals(SignalOccurrence parentSignal) { + final descriptors = parentSignal.subFieldDescriptors; + if (descriptors.isEmpty) { + return const []; + } + + return descriptors.map((d) { + final idx = signalIndexByName(d.expectedName); + final sig = idx >= 0 ? signals[idx] : null; + return ( + signal: sig, + fieldLabel: d.fieldLabel, + width: d.width, + startBit: d.startBit, + ); + }).toList(); + } + + /// Collect all signals under this occurrence in depth-first order. + /// + /// Visits this occurrence's [signals] first, then recurses into + /// [children] in order. Useful for flat iteration or counting, but + /// signals should always be identified by their [OccurrenceAddress] or + /// path — never by a positional index in this list. + /// + /// Production code should use [signalCount], [computedSignalCount], or + /// a recursive visitor instead of materializing the full list. + @visibleForTesting + List depthFirstSignals() => + [...signals, ...children.expand((c) => c.depthFirstSignals())]; + + /// Total number of signals in this subtree (O(n) recursive count). + /// + /// Equivalent to `depthFirstSignals().length` but avoids allocating the + /// intermediate list. + int get signalCount => + signals.length + children.fold(0, (sum, c) => sum + c.signalCount); + + /// Number of computed signals in this subtree. + /// + /// Equivalent to + /// `depthFirstSignals().where((s) => s.isComputed).length` + /// but avoids allocating the intermediate list. + int get computedSignalCount => + signals.where((s) => s.isComputed).length + + children.fold(0, (sum, c) => sum + c.computedSignalCount); + + /// Build hierarchical addresses for this occurrence and all descendants. + /// + /// This performs a single O(n) tree traversal to assign [OccurrenceAddress] + /// to every occurrence and signal in the tree. Call this once after tree + /// construction to enable efficient address-based navigation. + /// + /// **Signal address ordering**: ports (signals with a non-null + /// [SignalOccurrence.direction]) are assigned indices first + /// (`0 .. portCount-1`), followed by internal signals + /// (`portCount .. signals.length-1`). Within each group the + /// original list order is preserved. + /// + /// This means a port's [SignalOccurrence.portIndex] always equals its + /// signal address index, which consumers (e.g. schematic hyperedges) can + /// rely on remaining stable across incremental expansion. + /// + /// Example: + /// ```dart + /// root.buildAddresses(); // Assign addresses to all occurrences/signals + /// final signalAddr = signals[0].address; // Now available + /// ``` + void buildAddresses([OccurrenceAddress startAddr = OccurrenceAddress.root]) { + _address = startAddr; + + // Assign ports first, then internal signals, so that port indices + // are stable across incremental hierarchy expansion. + var idx = 0; + for (final s in signals) { + if (s.isPort) { + s + ..address = startAddr.signal(idx++) + ..parent = this; + } + } + for (final s in signals) { + if (!s.isPort) { + s + ..address = startAddr.signal(idx++) + ..parent = this; + } + } + + for (final (i, c) in children.indexed) { + c + .._parent = this + ..buildAddresses(startAddr.child(i)); + } + } +} diff --git a/packages/rohd_hierarchy/lib/src/hierarchy_query.dart b/packages/rohd_hierarchy/lib/src/hierarchy_query.dart new file mode 100644 index 000000000..c350a2391 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/hierarchy_query.dart @@ -0,0 +1,152 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// hierarchy_query.dart +// Pluggable search query abstraction for hierarchy search. +// +// 2026 May +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/src/hierarchy_occurrence.dart'; +import 'package:rohd_hierarchy/src/hierarchy_service.dart'; +import 'package:rohd_hierarchy/src/prefix_query.dart'; +import 'package:rohd_hierarchy/src/regex_query.dart'; + +// Re-export so callers importing hierarchy_query.dart get the concrete types. +export 'package:rohd_hierarchy/src/prefix_query.dart'; +export 'package:rohd_hierarchy/src/regex_query.dart'; + +/// What kind of hierarchy elements a query should match. +enum SearchTarget { + /// Match only [HierarchyOccurrence] nodes (modules, instances). + occurrences, + + /// Match only signals within occurrences. + signals, + + /// Match both occurrences and signals. + both, +} + +/// Abstract base class for hierarchy search queries. +/// +/// A [HierarchyQuery] encapsulates the *matching strategy* (how names are +/// compared) independently of the *tree traversal* (which is always +/// performed by [HierarchyService]). +/// +/// ## Contract with [HierarchyService] +/// +/// The service walks the [HierarchyOccurrence] tree depth-first. +/// At each node it calls: +/// +/// 1. [matchOccurrence] — does this occurrence name satisfy the query at the +/// current match state? Returns a set of successor states (empty = +/// prune this branch). +/// 2. [matchSignal] — does this signal name satisfy the query at the +/// current match state? +/// 3. [isComplete] — have all parts of the query been consumed at the +/// given state? +/// +/// "Match state" is an opaque integer that the query owns. It typically +/// tracks how many segments/tokens of the query have been consumed so far. +/// The initial state is always `0`. +/// +/// ## Crossing hierarchy boundaries +/// +/// If [crossesBoundaries] is true the service will, at each depth, +/// additionally try advancing with the *current* state even when the +/// occurrence doesn't match — allowing matches to span across +/// intermediate hierarchy levels (like `**` in glob patterns). +/// +/// ## Subclassing +/// +/// Implement a concrete query by overriding at least [matchOccurrence], +/// [matchSignal], [isComplete], and [segmentCount]. +/// +/// The factory [HierarchyQuery.prefix] creates the default +/// prefix-substring query. [HierarchyQuery.regex] creates a +/// regex/glob query. +/// +/// ```dart +/// // Custom fuzzy query +/// class FuzzyQuery extends HierarchyQuery { +/// FuzzyQuery(String rawQuery) +/// : super(rawQuery, target: SearchTarget.signals); +/// ... +/// } +/// ``` +abstract class HierarchyQuery { + /// The original user-supplied query string. + final String rawQuery; + + /// What this query matches — occurrences, signals, or both. + final SearchTarget target; + + /// Whether this query can match across hierarchy boundaries. + /// + /// When true, the tree walker will try the current match state at + /// deeper levels even when intermediate occurrences don't match. + /// Conceptually equivalent to an implicit `**` between segments. + final bool crossesBoundaries; + + /// Creates a query from [rawQuery]. + /// + /// Subclasses should parse/compile the query in their constructor. + const HierarchyQuery( + this.rawQuery, { + this.target = SearchTarget.signals, + this.crossesBoundaries = false, + }); + + /// Number of logical segments in the parsed query. + /// + /// Used by the tree walker to know when the query is fully consumed. + int get segmentCount; + + /// Whether the query is empty / trivial (should return no results). + bool get isEmpty => rawQuery.trim().isEmpty; + + /// Try matching an occurrence name at match state [stateIndex]. + /// + /// Returns a set of successor states. Multiple successors arise when + /// the query is ambiguous at this point (e.g. a glob-star `**` can + /// consume zero or more levels). + /// + /// An empty set means "no match — prune this subtree". + Set matchOccurrence(String occurrenceName, int stateIndex); + + /// Whether [signalName] matches the query at state [stateIndex]. + /// + /// Only called when [target] includes signals. + bool matchSignal(String signalName, int stateIndex); + + /// Whether the query is fully consumed at [stateIndex]. + /// + /// Returns true when all segments have been matched and the current + /// tree position is a valid result. + bool isComplete(int stateIndex); + + // ──────────────── Built-in query factories ──────────────── + + /// Create a **prefix-substring** query. + /// + /// The query is split on `/` or `.` into segments. Each segment is + /// matched via `startsWith` (for signals) or + /// `contains` (for occurrences) against names at successive depths. + factory HierarchyQuery.prefix( + String rawQuery, { + SearchTarget target, + }) = PrefixQuery; + + /// Create a **regex/glob** query. + /// + /// Segments are separated by `/`. Each segment is compiled as a + /// case-sensitive regex anchored to the full name. The special + /// segment `**` matches zero or more hierarchy levels. + /// + /// Glob wildcards `*` and `?` are auto-converted to regex equivalents. + factory HierarchyQuery.regex( + String rawQuery, { + SearchTarget target, + }) = RegexQuery; +} diff --git a/packages/rohd_hierarchy/lib/src/hierarchy_search_controller.dart b/packages/rohd_hierarchy/lib/src/hierarchy_search_controller.dart new file mode 100644 index 000000000..a82375b4b --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/hierarchy_search_controller.dart @@ -0,0 +1,216 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// hierarchy_search_controller.dart +// Pure Dart controller for hierarchy search list navigation. +// +// 2026 February +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; + +/// Pure Dart controller for hierarchy search list navigation. +/// +/// Manages search results and keyboard-style list selection without +/// any Flutter dependency. Widgets call controller methods, then +/// refresh their own UI (e.g. `setState`). +/// +/// Generic over the result type [R] — typically [SignalSearchResult] +/// or [OccurrenceSearchResult]. +/// +/// ```dart +/// // In a Flutter widget: +/// final controller = HierarchySearchController.forSignals(hierarchy); +/// +/// void _onSearchChanged() { +/// controller.updateQuery(_textController.text); +/// setState(() {}); +/// } +/// ``` +class HierarchySearchController { + /// The search function that produces results from a normalised query. + final List Function(String normalizedQuery) _searchFn; + + /// Normalises a raw user query (e.g. replaces `.` with `/`). + final String Function(String rawQuery) _normalizeFn; + + List _results = []; + int _selectedIndex = 0; + + /// Create a controller with custom search and normalise functions. + HierarchySearchController({ + required List Function(String normalizedQuery) searchFn, + required String Function(String rawQuery) normalizeFn, + }) : _searchFn = searchFn, + _normalizeFn = normalizeFn; + + /// Create a controller for **signal** search on the given + /// [HierarchyService]. + /// + /// When the query contains glob/regex metacharacters, normalisation + /// is skipped so that `.` keeps its regex meaning (use `/` as the + /// hierarchy separator in regex patterns). + factory HierarchySearchController.forSignals( + HierarchyService hierarchy, + ) => + HierarchySearchController( + searchFn: (q) => hierarchy.searchSignals(q) as List, + normalizeFn: (q) => HierarchyService.hasRegexChars(q) + ? q + : HierarchySearchResult.normalizeQuery(q), + ); + + /// Create a controller for **occurrence** search on the given + /// [HierarchyService]. + /// + /// When the query contains glob/regex metacharacters, normalisation + /// is skipped so that `.` keeps its regex meaning (use `/` as the + /// hierarchy separator in regex patterns). + factory HierarchySearchController.forOccurrences( + HierarchyService hierarchy, + ) => + HierarchySearchController( + searchFn: (q) => hierarchy.searchOccurrences(q) as List, + normalizeFn: (q) => HierarchyService.hasRegexChars(q) + ? q + : HierarchySearchResult.normalizeQuery(q), + ); + + // ─────────────── State accessors ─────────────── + + /// The current search results. + List get results => _results; + + /// Index of the currently highlighted result. + int get selectedIndex => _selectedIndex; + + /// Whether there are any results. + bool get hasResults => _results.isNotEmpty; + + /// A human-readable counter string, e.g. `"3/12"`, or empty when + /// there are no results. + String get counterText => + hasResults ? '${_selectedIndex + 1}/${_results.length}' : ''; + + /// The currently selected result, or `null` if the list is empty. + R? get currentSelection => _results.isEmpty ? null : _results[_selectedIndex]; + + // ─────────────── Mutations ─────────────── + + /// Update search results for [rawQuery]. + /// + /// Normalises the query, runs the search function, and resets the + /// selection to the first result. The caller should rebuild its UI + /// after calling this. + void updateQuery(String rawQuery) { + if (rawQuery.isEmpty) { + _results = []; + _selectedIndex = 0; + return; + } + final normalized = _normalizeFn(rawQuery); + _results = _searchFn(normalized); + _selectedIndex = 0; + } + + /// Move selection to the next result, wrapping around. + void selectNext() { + if (_results.isEmpty) { + return; + } + _selectedIndex = (_selectedIndex + 1) % _results.length; + } + + /// Move selection to the previous result, wrapping around. + void selectPrevious() { + if (_results.isEmpty) { + return; + } + _selectedIndex = (_selectedIndex - 1 + _results.length) % _results.length; + } + + /// Move selection to a specific [index]. + /// + /// Clamps to valid range. Useful for tap-to-select in a list view. + void selectAt(int index) { + if (_results.isEmpty) { + return; + } + _selectedIndex = index.clamp(0, _results.length - 1); + } + + /// Clear all results and reset the selection index. + void clear() { + _results = []; + _selectedIndex = 0; + } + + // ─────────────── Tab-completion ─────────────── + + /// Compute the tab-completion expansion for [currentQuery]. + /// + /// Finds the longest common prefix of all current result display paths + /// and returns it if it is strictly longer than [currentQuery]. + /// Returns `null` when there is nothing to expand. + /// + /// [displayPath] extracts the comparable path string from each result. + /// The default implementation handles [SignalSearchResult] and + /// [OccurrenceSearchResult] automatically; pass a custom extractor for + /// other result types. + String? tabComplete( + String currentQuery, { + String Function(R result)? displayPath, + }) { + if (_results.isEmpty) { + return null; + } + + final extractor = displayPath ?? _defaultDisplayPath; + final paths = _results.map(extractor).toList(); + final prefix = HierarchyService.longestCommonPrefix(paths); + if (prefix == null) { + return null; + } + + // Normalise the query the same way UpdateQuery does so lengths are + // comparable (e.g. dots → slashes). + final normalizedQuery = _normalizeFn(currentQuery); + if (prefix.length <= normalizedQuery.length) { + return null; + } + return prefix; + } + + /// Default display-path extractor for the well-known result types. + static String _defaultDisplayPath(T result) { + if (result is HierarchySearchResult) { + return result.displayPath; + } + return result.toString(); + } + + // ─────────────── Scroll helper ─────────────── + + /// Compute the scroll offset needed to reveal the selected item in a + /// fixed-height list. + /// + /// Returns `null` if the item is already visible. The caller should + /// call `scrollController.jumpTo(offset)` with the returned value. + /// + /// This is a pure calculation with no Flutter dependency. + static double? scrollOffsetToReveal({ + required int selectedIndex, + required double itemHeight, + required double viewportHeight, + required double currentOffset, + }) { + final target = selectedIndex * itemHeight; + if (target < currentOffset) { + return target; + } + if (target + itemHeight > currentOffset + viewportHeight) { + return target + itemHeight - viewportHeight; + } + return null; + } +} diff --git a/packages/rohd_hierarchy/lib/src/hierarchy_search_result.dart b/packages/rohd_hierarchy/lib/src/hierarchy_search_result.dart new file mode 100644 index 000000000..3f40b4c82 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/hierarchy_search_result.dart @@ -0,0 +1,60 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// hierarchy_search_result.dart +// Base class for hierarchy search results. +// +// 2026 May +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; + +/// Base class for hierarchy search results. +/// +/// Holds the common fields shared by signal and occurrence search +/// results: a canonical ID string, pre-split path segments, and display +/// helpers that strip the top-level module name. +@immutable +abstract class HierarchySearchResult { + /// The full hierarchical path that was found. + /// Example: `"Top/counter/clk"` or `"Top/CPU/ALU"`. + final String id; + + /// The hierarchical path segments. + /// Example: `["Top", "counter", "clk"]`. + final List path; + + /// Creates a hierarchy search result. + const HierarchySearchResult({required this.id, required this.path}); + + /// The leaf name (last path segment). + String get name => path.isNotEmpty ? path.last : id; + + // ───────────────────── Display helpers ───────────────────── + + /// Display path with the top-level module name stripped. + /// + /// For `Top/counter/clk` this returns `counter/clk`. + /// For a single-segment path returns the original [id]. + String get displayPath => displaySegments.join('/'); + + /// Path segments with the top-level module name stripped. + /// + /// For `["Top", "counter", "clk"]` returns `["counter", "clk"]`. + List get displaySegments => path.length > 1 ? path.sublist(1) : path; + + /// Normalize a user query for hierarchy search. + /// + /// Converts common separators (`.`) to the canonical `/` separator. + static String normalizeQuery(String query) => query.replaceAll('.', '/'); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is HierarchySearchResult && + runtimeType == other.runtimeType && + id == other.id; + + @override + int get hashCode => id.hashCode; +} diff --git a/packages/rohd_hierarchy/lib/src/hierarchy_service.dart b/packages/rohd_hierarchy/lib/src/hierarchy_service.dart new file mode 100644 index 000000000..08c2add90 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/hierarchy_service.dart @@ -0,0 +1,872 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// hierarchy_service.dart +// Abstract interface for source-agnostic hardware hierarchy navigation. +// +// 2026 January +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/src/hierarchy_models.dart'; + +/// Default path separator used when constructing paths from the tree. +const String _hierarchySeparator = '/'; + +/// A source-agnostic interface for navigating hardware hierarchy. +/// +/// All search and navigation is driven by walking the [HierarchyOccurrence] +/// tree. Occurrences hold their [HierarchyOccurrence.name], +/// [HierarchyOccurrence.children], and [HierarchyOccurrence.signals]. Full +/// paths are constructed on the fly by joining names with [_hierarchySeparator] +/// — no pre-baked path strings are needed for search. +/// +/// Key methods: +/// - [searchSignals] — incremental signal search +/// - [searchOccurrences] — find occurrences by name +/// - [matchOccurrences] — find occurrences, returning [HierarchyOccurrence] +/// objects +/// - [autocompletePaths] — incremental path completion +abstract mixin class HierarchyService { + /// The root occurrence for the hierarchy. + HierarchyOccurrence get root; + + /// Maximum number of results returned by search methods when no explicit + /// `limit` is provided. + static const int _defaultSearchLimit = 100; + + // ───────────── Address-based occurrence/signal lookup ──────────────── + + /// Find an occurrence by its [OccurrenceAddress]. O(depth). + HierarchyOccurrence? occurrenceByAddress(OccurrenceAddress address) => + address.path.fold( + root, + (node, idx) => node != null && idx >= 0 && idx < node.children.length + ? node.children[idx] + : null); + + /// Find a signal by its [OccurrenceAddress]. + /// + /// The parent portion of [address] navigates to the owning occurrence; + /// the last index selects the signal within that occurrence. O(depth). + SignalOccurrence? signalByAddress(OccurrenceAddress address) { + if (address.path.isEmpty) { + return null; + } + final node = occurrenceByAddress( + OccurrenceAddress(address.path.sublist(0, address.path.length - 1))); + final sigIdx = address.path.last; + return (node != null && sigIdx >= 0 && sigIdx < node.signals.length) + ? node.signals[sigIdx] + : null; + } + + // ───────────── Address ↔ pathname conversion ────────────────── + + /// Convert a pathname (e.g. `"Top/sub/clk"` or `"Top.sub.clk"`) to a + /// [OccurrenceAddress] by walking the tree. + /// + /// Delegates to [OccurrenceAddress.tryFromPathname]. + OccurrenceAddress? pathnameToAddress(String pathname) => + OccurrenceAddress.tryFromPathname(pathname, root); + + /// Resolve a `/`-separated pathname to a [HierarchyOccurrence]. + /// + /// Convenience that composes [pathnameToAddress] and [occurrenceByAddress]. + /// Returns `null` when [pathname] does not match any occurrence in the + /// tree. + HierarchyOccurrence? occurrenceByPathname(String pathname) { + final addr = pathnameToAddress(pathname); + return addr == null ? null : occurrenceByAddress(addr); + } + + /// Convert a [OccurrenceAddress] back to a `/`-separated pathname by + /// walking the tree using child indices. + /// + /// Returns `null` if the address doesn't resolve in the current tree + /// (e.g. out-of-bounds indices). O(depth). + /// + /// For signal addresses, the last index is resolved as a signal within + /// the parent occurrence. For pure occurrence addresses, every index + /// is a child. + /// + /// Set [asSignal] to `true` when you know the address points to a signal + /// (the last index is a signal offset rather than a child offset). + /// When `false` (default), all indices are treated as child offsets. + String? addressToPathname(OccurrenceAddress address, + {bool asSignal = false}) { + if (address.path.isEmpty) { + return root.name; + } + + final indices = address.path; + final moduleEndIdx = asSignal ? indices.length - 1 : indices.length; + + final walked = indices + .sublist(0, moduleEndIdx) + .fold<({List parts, HierarchyOccurrence node})?>(( + parts: [root.name], + node: root, + ), (cur, idx) { + if (cur == null || idx < 0 || idx >= cur.node.children.length) { + return null; + } + final child = cur.node.children[idx]; + return (parts: [...cur.parts, child.name], node: child); + }); + if (walked == null) { + return null; + } + + if (asSignal && indices.isNotEmpty) { + final sigIdx = indices.last; + return (sigIdx >= 0 && sigIdx < walked.node.signals.length) + ? [...walked.parts, walked.node.signals[sigIdx].name] + .join(_hierarchySeparator) + : null; + } + return walked.parts.join(_hierarchySeparator); + } + + /// Resolve a waveform-style ID (dot-separated, e.g. `"dut.adder.clk"`) + /// to a [OccurrenceAddress]. + /// + /// Normalises `.` → `/` then delegates to [pathnameToAddress]. + OccurrenceAddress? waveformIdToAddress(String waveformId) => + pathnameToAddress(waveformId); + + // ───────────────────── Search / autocomplete ───────────────────── + + /// Find hierarchical signal paths matching [query]. + /// + /// Walks the tree, matching name segments incrementally. When the last + /// query segment partially matches a signal name at or below the current + /// node the full path is returned (e.g. `Top/block/signal`). + /// + /// Returns up to [limit] results. + List searchSignalPaths(String query, {int? limit}) { + final effectiveLimit = limit ?? _defaultSearchLimit; + if (query.trim().isEmpty) { + return const []; + } + final parts = _splitPath(query); + final results = []; + _searchSignalsRecursive( + root, [root.name], parts, 0, results, effectiveLimit); + return results; + } + + /// Whether [query] contains glob or regex metacharacters that should + /// trigger the regex search engine instead of the plain substring search. + static bool hasRegexChars(String query) => + query.contains('*') || + query.contains('?') || + query.contains('[') || + query.contains('(') || + query.contains('|') || + query.contains('+'); + + /// Check if an occurrence or any of its descendants match [searchTerm]. + /// + /// The search term is split on `/` or `.` into hierarchical segments. + /// Each segment is matched via substring containment + /// against occurrence names at successive depths. + /// + /// Returns `true` if [searchTerm] is null/empty, or if the occurrence + /// (or a descendant) matches all segments in order. + /// + /// This is useful for tree-view filtering: show an occurrence only when + /// it or one of its descendants matches the user's query. + static bool isOccurrenceMatching( + HierarchyOccurrence node, String? searchTerm) { + if (searchTerm == null || searchTerm.isEmpty) { + return true; + } + + final normalizedQuery = searchTerm.replaceAll('.', '/'); + final queryParts = normalizedQuery + .split('/') + .map((s) => s.trim()) + .where((s) => s.isNotEmpty) + .toList(); + + return _isOccurrenceMatchingRecursive(node, queryParts, 0); + } + + static bool _isOccurrenceMatchingRecursive( + HierarchyOccurrence node, List queryParts, int queryIdx) { + if (queryIdx >= queryParts.length) { + return true; + } + + final currentQueryPart = queryParts[queryIdx]; + final nodeName = node.name; + + final matched = nodeName.contains(currentQueryPart); + final nextQueryIdx = matched ? queryIdx + 1 : queryIdx; + + if (nextQueryIdx >= queryParts.length) { + return true; + } + + return node.children.any((child) => + _isOccurrenceMatchingRecursive(child, queryParts, nextQueryIdx)); + } + + /// Search for signals and return enriched [SignalSearchResult] objects. + /// + /// Automatically dispatches to [searchSignalsRegex] when the query + /// contains glob or regex metacharacters (`*`, `?`, `[`, `(`, `|`, + /// `+`). Otherwise uses [searchSignalPaths] for prefix-based matching. + List searchSignals(String query, {int? limit}) { + final effectiveLimit = limit ?? _defaultSearchLimit; + if (hasRegexChars(query)) { + final pattern = (query.startsWith('**/') || query.startsWith('*/')) + ? query + : '*/$query'; + return searchSignalsRegex(pattern, limit: effectiveLimit); + } + return _toSignalResults(searchSignalPaths(query, limit: effectiveLimit)); + } + + /// Find hierarchical occurrence paths matching [query]. + /// + /// Similar to [searchSignalPaths] but for occurrences instead of + /// signals. Walks the tree, matching name segments incrementally. When + /// the query segments match occurrence names at or below the current + /// level the full path is returned (e.g. `Top/CPU/ALU`). + /// + /// Returns up to [limit] results. + List searchOccurrencePaths(String query, {int? limit}) { + final effectiveLimit = limit ?? _defaultSearchLimit; + if (query.trim().isEmpty) { + return const []; + } + final parts = _splitPath(query); + final results = []; + _searchOccurrencePathsRecursive( + root, [root.name], parts, 0, results, effectiveLimit); + return results; + } + + /// Find hierarchy occurrences whose path matches [query]. + /// + /// Like [searchOccurrencePaths] but returns the [HierarchyOccurrence] objects + /// themselves instead of path strings. + List matchOccurrences(String query, {int? limit}) { + final effectiveLimit = limit ?? _defaultSearchLimit; + if (query.trim().isEmpty) { + return const []; + } + final parts = _splitPath(query); + final results = []; + _matchOccurrencesRecursive(root, parts, 0, results, effectiveLimit); + return results; + } + + /// Autocomplete suggestions for a partial hierarchical path. + /// + /// The partial path is split into segments. Completed segments navigate + /// down the tree; the final (possibly empty) segment is used as a prefix + /// filter on children at that level. Returns up to [limit] full paths + /// (with `/` appended for nodes that have children). + List autocompletePaths(String partialPath, {int? limit}) { + final effectiveLimit = limit ?? _defaultSearchLimit; + final normalized = partialPath.replaceAll('.', _hierarchySeparator); + final endsWithSep = normalized.endsWith(_hierarchySeparator); + final parts = _splitPath(partialPath); + + // Navigate to the deepest complete segment. + var current = root; + final completedParts = [root.name]; + + final navParts = endsWithSep || parts.isEmpty + ? parts + : parts.sublist(0, parts.length - 1); + for (final seg in navParts) { + // If the segment matches the current node name, stay at this level + // (handles the root name appearing as the first path segment). + if (current.name == seg) { + continue; + } + final child = current.children.where((c) => c.name == seg).firstOrNull; + if (child == null) { + return const []; + } + current = child; + completedParts.add(child.name); + } + + // The trailing prefix to filter on (empty if path ends with separator). + final prefix = (endsWithSep || parts.isEmpty) ? '' : parts.last; + + final suggestions = []; + + // When the prefix matches the current (root-level) node itself and we + // haven't navigated past it, suggest the root path so that typing a + // partial root name produces a completion. + if (prefix.isNotEmpty && + completedParts.length == 1 && + current == root && + current.name.startsWith(prefix)) { + final rootPath = current.name; + suggestions.add(current.children.isNotEmpty + ? '$rootPath$_hierarchySeparator' + : rootPath); + } + + for (final child in current.children) { + if (prefix.isEmpty || child.name.startsWith(prefix)) { + final path = [...completedParts, child.name].join(_hierarchySeparator); + suggestions.add( + child.children.isNotEmpty ? '$path$_hierarchySeparator' : path); + if (suggestions.length >= effectiveLimit) { + break; + } + } + } + return suggestions; + } + + /// Search for occurrences and return enriched + /// [OccurrenceSearchResult] objects. + /// + /// Automatically dispatches to [searchOccurrencesRegex] when the query + /// contains glob or regex metacharacters (`*`, `?`, `[`, `(`, `|`, + /// `+`). Otherwise uses [searchOccurrencePaths] for prefix-based matching. + List searchOccurrences(String query, {int? limit}) { + final effectiveLimit = limit ?? _defaultSearchLimit; + if (hasRegexChars(query)) { + final pattern = (query.startsWith('**/') || query.startsWith('*/')) + ? query + : '**/$query'; + return searchOccurrencesRegex(pattern, limit: effectiveLimit); + } + return _toOccurrenceResults( + searchOccurrencePaths(query, limit: effectiveLimit)); + } + + // ───────────────── Regex search ───────────────── + + /// Search for signals whose hierarchical path matches a regex [pattern]. + /// + /// The pattern is split on `/` or `.` into segments. Each segment is + /// compiled as a [RegExp] and matched against the + /// corresponding depth in the hierarchy tree. Special segments: + /// + /// - `**` — matches zero or more hierarchy levels (glob-star). Use this + /// to search across hierarchy boundaries, e.g. `Top/**/clk` finds + /// `Top/CPU/ALU/clk`, `Top/Memory/clk`, etc. + /// - Any other string is compiled as a regex anchored to the full name + /// (`^…$`). Plain names therefore match exactly and regex meta- + /// characters like `.*`, `[0-9]+`, etc. work as expected. + /// + /// Returns up to [limit] full hierarchical signal paths. + /// + /// Examples: + /// ```text + /// 'Top/CPU/clk' — exact match at each level + /// 'Top/CPU/.*' — all signals in Top/CPU + /// 'Top/.*/clk' — clk signal one level below Top + /// 'Top/**/clk' — clk signal at any depth below Top + /// 'Top/**/c.*' — signals starting with 'c' at any depth + /// '**/(clk|reset)' — clk or reset anywhere in hierarchy + /// 'Top/CPU/d[0-9]+' — signals like d0, d1, d12 in Top/CPU + /// ``` + List searchSignalPathsRegex(String pattern, {int? limit}) { + final effectiveLimit = limit ?? _defaultSearchLimit; + if (pattern.trim().isEmpty) { + return const []; + } + final segments = _splitRegexPattern(pattern); + final compiled = _compileSegments(segments); + final results = []; + _searchSignalsRegex( + root, [root.name], compiled, 0, results, effectiveLimit); + return results; + } + + /// Search for signals by regex pattern and return enriched results. + List searchSignalsRegex(String pattern, {int? limit}) => + _toSignalResults(searchSignalPathsRegex(pattern, limit: limit)); + + /// Search for occurrence paths matching a regex [pattern]. + /// + /// Same segment syntax as [searchSignalPathsRegex] but matches + /// occurrences instead of signals. + /// + /// Returns up to [limit] full hierarchical occurrence paths. + List searchOccurrencePathsRegex(String pattern, {int? limit}) { + final effectiveLimit = limit ?? _defaultSearchLimit; + if (pattern.trim().isEmpty) { + return const []; + } + final segments = _splitRegexPattern(pattern); + final compiled = _compileSegments(segments); + final results = []; + _matchOccurrencesRegex( + root, [root.name], compiled, 0, results, effectiveLimit); + return results; + } + + /// Search for occurrences by regex pattern and return enriched results. + List searchOccurrencesRegex(String pattern, + {int? limit}) => + _toOccurrenceResults(searchOccurrencePathsRegex(pattern, limit: limit)); + + // ─────────────────── Utility helpers ─────────────────── + + /// Returns the longest common prefix shared by all [paths]. + /// + /// Comparison is case-sensitive. Returns `null` when [paths] is empty + /// or no common prefix exists. + static String? longestCommonPrefix(List paths) { + if (paths.isEmpty) { + return null; + } + final prefix = paths.skip(1).fold(paths.first, (pre, s) { + if (pre == null || pre.isEmpty) { + return null; + } + final end = pre.length < s.length ? pre.length : s.length; + final j = + Iterable.generate(end).takeWhile((i) => pre[i] == s[i]).length; + return j > 0 ? pre.substring(0, j) : null; + }); + return prefix; + } + + // ─────────────────── Private helpers ─────────────────── + + /// Split a query or path on `/` or `.` into non-empty segments. + static List _splitPath(String input) => input + .replaceAll('.', _hierarchySeparator) + .split(_hierarchySeparator) + .map((s) => s.trim()) + .where((s) => s.isNotEmpty) + .toList(); + + /// Split a path on `/` or `.` into non-empty segments, preserving case. + /// + /// Use this when the result is for display or building [SignalSearchResult] + /// path parts — not for matching. + static List _splitPathPreserveCase(String input) => input + .replaceAll('.', _hierarchySeparator) + .split(_hierarchySeparator) + .where((s) => s.isNotEmpty) + .toList(); + + /// Enrich signal paths into [SignalSearchResult] objects. + List _toSignalResults(List paths) => + paths.map((fullPath) { + final addr = OccurrenceAddress.tryFromPathname(fullPath, root); + return SignalSearchResult( + signalId: fullPath, + path: _splitPathPreserveCase(fullPath), + signal: addr != null ? signalByAddress(addr) : null, + ); + }).toList(); + + /// Enrich occurrence paths into [OccurrenceSearchResult] objects. + List _toOccurrenceResults(List paths) => + paths.map((fullPath) { + final addr = OccurrenceAddress.tryFromPathname(fullPath, root); + return OccurrenceSearchResult( + occurrenceId: fullPath, + path: _splitPathPreserveCase(fullPath), + occurrence: (addr != null ? occurrenceByAddress(addr) : null) ?? root, + ); + }).toList(); + + /// Recursively search for signals matching query parts. + /// + /// Walks the tree maintaining the path of names. When the accumulated match + /// depth reaches the query length, checks signals at that node. Partial + /// last-segment matching also checks signals at partially-matched nodes. + /// + /// Uses [HierarchyOccurrence.children] and [HierarchyOccurrence.signals] + /// directly. + void _searchSignalsRecursive( + HierarchyOccurrence node, + List pathSoFar, + List queryParts, + int qIdx, + List results, + int limit, + ) { + if (results.length >= limit) { + return; + } + + // Try matching current node name against current query part + final nodeName = node.name; + final currentQuery = qIdx < queryParts.length ? queryParts[qIdx] : null; + final matched = currentQuery != null && nodeName.startsWith(currentQuery); + final nextIdx = matched ? qIdx + 1 : qIdx; + + // Determine how many query parts remain after any node-name match. + final remaining = queryParts.length - nextIdx; + + // If 0 or 1 query parts remain, search signals at this node. + if (remaining <= 1) { + // When the current node consumed the last segment (remaining==0, + // matched==true), reuse that segment as the signal filter so that + // e.g. "a" doesn't return every signal under a module named "alu". + // When remaining==0 because we're recursing into a subtree where + // a parent already consumed all segments, use empty (return all). + final signalQuery = remaining == 1 + ? queryParts[nextIdx] + : (matched && qIdx < queryParts.length ? queryParts[qIdx] : ''); + for (final signal in node.signals) { + if (results.length >= limit) { + return; + } + if (signalQuery.isEmpty || signal.name.startsWith(signalQuery)) { + final fullPath = + [...pathSoFar, signal.name].join(_hierarchySeparator); + results.add(fullPath); + } + } + } + + // Recurse into children + for (final child in node.children) { + if (results.length >= limit) { + return; + } + _searchSignalsRecursive( + child, + [...pathSoFar, child.name], + queryParts, + nextIdx, + results, + limit, + ); + } + } + + /// Recursively search for occurrences matching query parts. + /// + /// Similar to [_searchSignalsRecursive] but matches occurrences instead + /// of signals. Walks the tree maintaining the path of names. When the + /// query segments match occurrence names, adds them to results. + void _searchOccurrencePathsRecursive( + HierarchyOccurrence node, + List pathSoFar, + List queryParts, + int qIdx, + List results, + int limit, + ) { + if (results.length >= limit) { + return; + } + + // Try matching current node name against current query part + final nodeName = node.name; + final currentQuery = qIdx < queryParts.length ? queryParts[qIdx] : null; + final matched = currentQuery != null && nodeName.contains(currentQuery); + final nextIdx = matched ? qIdx + 1 : qIdx; + + // If all query parts are matched, this node is a result + if (nextIdx >= queryParts.length) { + final fullPath = pathSoFar.join(_hierarchySeparator); + results.add(fullPath); + if (results.length >= limit) { + return; + } + } + + // Recurse into children + for (final child in node.children) { + if (results.length >= limit) { + return; + } + _searchOccurrencePathsRecursive( + child, + [...pathSoFar, child.name], + queryParts, + nextIdx, + results, + limit, + ); + } + } + + /// Recursively search for occurrences matching query parts, returning + /// the occurrences. + void _matchOccurrencesRecursive( + HierarchyOccurrence node, + List queryParts, + int qIdx, + List results, + int limit) { + if (results.length >= limit) { + return; + } + + final matched = + qIdx < queryParts.length && node.name.contains(queryParts[qIdx]); + final nextIdx = matched ? qIdx + 1 : qIdx; + + if (nextIdx >= queryParts.length) { + results.add(node); + if (results.length >= limit) { + return; + } + } + + for (final child in node.children) { + _matchOccurrencesRecursive(child, queryParts, nextIdx, results, limit); + if (results.length >= limit) { + return; + } + } + } + + // ─────────────── Regex search helpers ─────────────── + + /// A compiled regex segment. `isGlobStar` indicates a `**` segment that + /// matches zero or more hierarchy levels. + static const _globStarSentinel = '**'; + + /// Split `pattern` into segments on `/` only. + /// + /// Unlike [_splitPath] (which also splits on `.`), regex patterns use only + /// `/` as the hierarchy separator because `.` has meaning inside regular + /// expressions (e.g. `.*`, `a.b`). + List _splitRegexPattern(String input) => + input.split('/').map((s) => s.trim()).where((s) => s.isNotEmpty).toList(); + + /// Convert glob-style `*` and `?` wildcards to regex equivalents. + /// + /// A standalone `*` (not preceded/followed by another regex metachar) + /// becomes `.*` (match anything). `?` becomes `.` (match one char). + /// This lets users write natural patterns like `*m`, `clk*`, `*data*` + /// without needing to know regex syntax. + String _globToRegex(String segment) { + final buf = StringBuffer(); + for (var i = 0; i < segment.length; i++) { + final c = segment[i]; + if (c == '*') { + // If already preceded by `.` (i.e. user wrote `.*`), skip conversion. + if (buf.toString().endsWith('.')) { + buf.write('*'); + } else { + buf.write('.*'); + } + } else if (c == '?') { + // If already preceded by a valid quantifier target, keep literal `?`. + // Otherwise treat as single-char wildcard `.`. + if (i > 0 && !'.?*+'.contains(segment[i - 1])) { + buf.write('?'); + } else { + buf.write('.'); + } + } else { + buf.write(c); + } + } + return buf.toString(); + } + + /// Compile string segments into [_RegexSegment] list. + /// + /// Each segment is first run through [_globToRegex] so that glob-style + /// wildcards (`*`, `?`) work alongside full regex syntax. + List<_RegexSegment> _compileSegments(List segments) => + segments.map((s) { + if (s == _globStarSentinel) { + return _RegexSegment.globStar(); + } + final pattern = _globToRegex(s); + // Anchor the regex to match the full name. + return _RegexSegment(RegExp('^$pattern\$')); + }).toList(); + + /// Recursive signal search driven by compiled regex segments. + /// + /// [segIdx] is the index into [segments] that we are currently trying to + /// match at this tree depth. + void _searchSignalsRegex( + HierarchyOccurrence node, + List pathSoFar, + List<_RegexSegment> segments, + int segIdx, + List results, + int limit, + ) { + if (results.length >= limit) { + return; + } + + // Determine how many segments remain after consuming the current node. + final consumed = _matchNode(node.name, segments, segIdx); + + for (final nextIdx in consumed) { + if (results.length >= limit) { + return; + } + + // Try to match signals at this node. + // Find all indices reachable from nextIdx by skipping glob-stars + // where a signal-level regex (or end-of-pattern) can be applied. + for (final sigIdx in _signalReachableIndices(segments, nextIdx)) { + if (results.length >= limit) { + return; + } + if (sigIdx >= segments.length) { + // All segments consumed: collect all signals at this node. + for (final signal in node.signals) { + if (results.length >= limit) { + return; + } + results.add([...pathSoFar, signal.name].join(_hierarchySeparator)); + } + } else { + // sigIdx points to a non-** regex that should match signal names. + final sigSeg = segments[sigIdx]; + // Only use as signal-level match if this is the last non-** segment + // (possibly followed by more **'s that can match zero levels). + if (_allGlobStarAfter(segments, sigIdx + 1)) { + for (final signal in node.signals) { + if (results.length >= limit) { + return; + } + if (sigSeg.regex!.hasMatch(signal.name)) { + results + .add([...pathSoFar, signal.name].join(_hierarchySeparator)); + } + } + } + } + } + + // Recurse into children. + for (final child in node.children) { + if (results.length >= limit) { + return; + } + _searchSignalsRegex( + child, + [...pathSoFar, child.name], + segments, + nextIdx, + results, + limit, + ); + } + } + } + + /// Recursive occurrence search driven by compiled regex segments. + void _matchOccurrencesRegex( + HierarchyOccurrence node, + List pathSoFar, + List<_RegexSegment> segments, + int segIdx, + List results, + int limit, + ) { + if (results.length >= limit) { + return; + } + + final consumed = _matchNode(node.name, segments, segIdx); + + for (final nextIdx in consumed) { + if (results.length >= limit) { + return; + } + + // All segments consumed (or only trailing **'s remain) → match. + if (_allGlobStarAfter(segments, nextIdx)) { + results.add(pathSoFar.join(_hierarchySeparator)); + if (results.length >= limit) { + return; + } + } + + // Recurse into children. + for (final child in node.children) { + if (results.length >= limit) { + return; + } + _matchOccurrencesRegex( + child, + [...pathSoFar, child.name], + segments, + nextIdx, + results, + limit, + ); + } + } + } + + /// Try to match [nodeName] against the segment at [segIdx]. + /// + /// Returns a set of possible next-segment indices (branching is needed + /// because `**` can consume zero or more levels). + Set _matchNode( + String nodeName, List<_RegexSegment> segments, int segIdx) { + final results = {}; + if (segIdx >= segments.length) { + // No more segments to match — nothing to advance to. + return results; + } + + final seg = segments[segIdx]; + + if (seg.isGlobStar) { + // ** matches zero levels (skip the **) … + results + ..addAll(_matchNode(nodeName, segments, segIdx + 1)) + // … or consumes this node and stays at ** (one-or-more levels). + ..add(segIdx); + } else if (seg.regex!.hasMatch(nodeName)) { + results.add(segIdx + 1); + } + // If the segment doesn't match at all, return empty → prune this branch. + return results; + } + + /// Returns indices in [segments] reachable from [fromIdx] by skipping + /// consecutive `**` glob-star segments. Always includes [fromIdx] itself + /// if it is in range (or == segments.length, meaning "past the end"). + Set _signalReachableIndices(List<_RegexSegment> segments, int fromIdx) { + final result = {}; + var i = fromIdx; + // Walk forward: each time we see a **, we can skip it (zero levels). + while (i < segments.length) { + if (segments[i].isGlobStar) { + // ** can match zero levels → skip and also record i (stay at **). + result.add(i + 1); // skip the ** + i++; + } else { + result.add(i); + break; // stop at first non-** segment + } + } + // If we walked past the end, record that too. + if (i >= segments.length) { + result.add(segments.length); + } + return result; + } + + /// Returns true if all segments from [fromIdx] onward are glob-stars + /// (or if [fromIdx] >= length, i.e. no more segments). + bool _allGlobStarAfter(List<_RegexSegment> segments, int fromIdx) => + segments.skip(fromIdx).every((s) => s.isGlobStar); +} + +/// Internal representation of a compiled regex segment. +class _RegexSegment { + final RegExp? regex; + final bool isGlobStar; + + _RegexSegment(this.regex) : isGlobStar = false; + _RegexSegment.globStar() + : regex = null, + isGlobStar = true; +} diff --git a/packages/rohd_hierarchy/lib/src/netlist_hierarchy_adapter.dart b/packages/rohd_hierarchy/lib/src/netlist_hierarchy_adapter.dart new file mode 100644 index 000000000..6ec675293 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/netlist_hierarchy_adapter.dart @@ -0,0 +1,224 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// netlist_hierarchy_adapter.dart +// Hierarchy adapter for netlist JSON format (derived from Yosys JSON) +// using rohd_hierarchy. +// +// 2026 January +// Author: Desmond Kirkpatrick + +import 'dart:convert'; + +import 'package:rohd_hierarchy/src/base_hierarchy_adapter.dart'; +import 'package:rohd_hierarchy/src/hierarchy_models.dart'; + +/// Adapter that exposes a netlist as a source-agnostic hierarchy. +/// +/// Extends [BaseHierarchyAdapter] from rohd_hierarchy package, using the shared +/// implementation for search, autocomplete, and lookup methods. +/// Only the netlist format-specific +/// JSON parsing logic is implemented here. +/// +/// Features: +/// - Parses ports, netnames, and cells from netlist JSON +/// - Filters auto-generated netnames (`hide_name`, `$`-prefixed, port dupes) +/// - Extracts `port_directions` on primitive cells for signal visibility +/// - Supports optional root-name override for VCD name alignment +class NetlistHierarchyAdapter extends BaseHierarchyAdapter { + NetlistHierarchyAdapter._(); + + /// Convenience factory to parse a netlist JSON string directly. + /// + /// [rootNameOverride] replaces the top-module name derived from the JSON. + /// Use this when VCD scopes use instance names that differ from the + /// definition names in the netlist output (often capitalized). + factory NetlistHierarchyAdapter.fromJson( + String netlistJson, { + String? rootNameOverride, + }) { + final obj = jsonDecode(netlistJson); + if (obj is! Map) { + throw const FormatException('Invalid netlist JSON root'); + } + return NetlistHierarchyAdapter.fromMap( + obj, + rootNameOverride: rootNameOverride, + ); + } + + /// Factory to parse a pre-decoded netlist JSON map. + /// + /// [netlistJson] must contain a top-level `modules` key. + /// [rootNameOverride] optionally replaces the detected top-module name. + factory NetlistHierarchyAdapter.fromMap( + Map netlistJson, { + String? rootNameOverride, + }) { + final adapter = NetlistHierarchyAdapter._() + .._buildFromNetlist(netlistJson, rootNameOverride: rootNameOverride); + return adapter; + } + + void _buildFromNetlist( + Map netlistJson, { + String? rootNameOverride, + }) { + final modules = netlistJson['modules'] as Map?; + if (modules == null || modules.isEmpty) { + throw const FormatException('Netlist JSON contained no modules'); + } + + // Find top module or default to first + final topName = modules.entries + .where( + (e) => + ((e.value as Map)['attributes'] + as Map?)?['top'] == + 1, + ) + .map((e) => e.key) + .firstOrNull ?? + modules.keys.first; + + final resolvedRootName = rootNameOverride ?? topName; + + final rootNode = _parseModule( + name: resolvedRootName, + definition: topName, + moduleData: modules[topName] as Map, + allModules: modules, + ); + root = rootNode; + rootNode.buildAddresses(); + } + + /// Parse a module definition and return the created + /// [HierarchyOccurrence]. + HierarchyOccurrence _parseModule({ + required String name, + required String definition, + required Map moduleData, + required Map allModules, + }) { + // Ports (signals with direction) + final portsData = moduleData['ports'] as Map?; + final signalsList = [ + if (portsData != null) + ...portsData.entries.indexed.map((entry) { + final (idx, kv) = entry; + final p = kv.value as Map; + final dir = p['direction']?.toString() ?? 'inout'; + final bits = (p['bits'] as List?)?.length ?? 0; + final logicType = p['logic_type'] as Map?; + return SignalOccurrence( + name: kv.key, + direction: dir, + width: bits > 0 ? bits : 1, + portIndex: idx, + logicType: logicType, + ); + }), + ]; + + // Netnames (internal signals without direction). + // Netlist `netnames` contains ALL named signals including port-connected + // ones. We skip names already covered by `ports` above, as well as + // auto-generated names (hide_name=1 or $-prefixed). + final netsData = moduleData['netnames'] as Map?; + if (netsData != null) { + final portNames = portsData?.keys.toSet() ?? {}; + signalsList.addAll( + netsData.entries + .where( + (entry) => + !portNames.contains(entry.key) && + !entry.key.startsWith(r'$') && + () { + final h = (entry.value as Map)['hide_name']; + return h != 1 && h != '1'; + }(), + ) + .map((entry) { + final netData = entry.value as Map; + final bits = (netData['bits'] as List?)?.length ?? 0; + final attrs = netData['attributes'] as Map?; + final isComputed = + attrs?['computed'] == 1 || attrs?['computed'] == true; + final logicType = netData['logic_type'] as Map?; + return SignalOccurrence( + name: entry.key, + width: bits > 0 ? bits : 1, + isComputed: isComputed, + logicType: logicType, + ); + }), + ); + } + + // Cells -> submodules or instances + final childNodes = []; + final cells = moduleData['cells'] as Map?; + if (cells != null) { + for (final entry in cells.entries) { + final cellName = entry.key; + final cellData = entry.value as Map; + final cellType = cellData['type']?.toString() ?? ''; + + if (allModules.containsKey(cellType) && + !HierarchyOccurrence.isPrimitiveType(cellType)) { + final childNode = _parseModule( + name: cellName, + definition: cellType, + moduleData: allModules[cellType] as Map, + allModules: allModules, + ); + childNodes.add(childNode); + } else { + // Primitive cell — create leaf occurrence. + // Extract port signals from `port_directions` when available so + // that primitive I/O appears in signal search results. + final isCellComputed = cellType.startsWith(r'$'); + final portDirections = + cellData['port_directions'] as Map?; + final connections = cellData['connections'] as Map?; + final portWidths = cellData['port_widths'] as Map?; + final cellSignals = [ + if (portDirections != null) + ...portDirections.entries.indexed.map((pEntry) { + final (pIdx, kv) = pEntry; + final pName = kv.key; + final pDir = kv.value.toString(); + final bits = (connections?[pName] as List?)?.length ?? + (portWidths?[pName] as int?) ?? + 1; + return SignalOccurrence( + name: pName, + direction: pDir, + width: bits, + isComputed: isCellComputed, + portIndex: pIdx, + ); + }), + ]; + + final instNode = HierarchyOccurrence( + name: cellName, + definition: cellType, + isPrimitive: true, + signals: cellSignals, + ); + childNodes.add(instNode); + } + } + } + + // Create the occurrence with children and signals embedded + return HierarchyOccurrence( + name: name, + definition: definition, + signals: signalsList, + children: childNodes, + ); + } +} diff --git a/packages/rohd_hierarchy/lib/src/occurrence_address.dart b/packages/rohd_hierarchy/lib/src/occurrence_address.dart new file mode 100644 index 000000000..b8a9c8bba --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/occurrence_address.dart @@ -0,0 +1,142 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// occurrence_address.dart +// Efficient hierarchical address using indices instead of strings. +// +// 2026 May +// Author: Desmond Kirkpatrick + +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; + +import 'package:rohd_hierarchy/src/hierarchy_occurrence.dart'; + +/// Efficient hierarchical address using indices instead of strings. +/// +/// Format: [index0, index1, ...] or [] for root. +/// Example: [0, 2, 4] means root's 0th child, then 2nd child of that, then +/// the 4th child (occurrence) or 4th signal, depending on context. +/// +/// Advantages: +/// - O(1) address creation (just append index) +/// - O(depth) tree navigation (direct array indexing) +/// - Deterministic serialization (no parsing needed) +/// - Natural alignment with waveform dictionary (integer indices) +/// - Supports hierarchical queries (ancestor matching, batching by prefix) +/// +/// This replaces string-based path lookups with typed, semantic addressing. +@immutable +class OccurrenceAddress { + /// Path through tree as indices stored as immutable list. + /// Empty list represents the root occurrence. + /// Non-empty list: indices navigate through the hierarchy. The last index + /// refers to either a child occurrence or a signal, depending on context. + final List path; + + /// Create a hierarchy address from a path list. + const OccurrenceAddress(this.path); + + /// Root address (empty path). + static const OccurrenceAddress root = OccurrenceAddress([]); + + /// Create a child address by appending an occurrence index. + /// Use this when navigating to a child occurrence. + OccurrenceAddress child(int childIndex) => + OccurrenceAddress([...path, childIndex]); + + /// Create a signal address by appending signal index. + /// Use this when addressing a signal within current occurrence. + OccurrenceAddress signal(int signalIndex) => + OccurrenceAddress([...path, signalIndex]); + + /// Serialize to a dot-separated string suitable for use as a JSON key. + /// + /// Examples: `""` (root), `"0"`, `"0.2.4"`. + /// Round-trips with [OccurrenceAddress.fromDotString]. + String toDotString() => path.join('.'); + + /// Deserialize from a dot-separated string produced by [toDotString]. + /// + /// An empty string returns [root]. + factory OccurrenceAddress.fromDotString(String s) { + if (s.isEmpty) { + return root; + } + return OccurrenceAddress(s.split('.').map(int.parse).toList()); + } + + @override + String toString() { + if (path.isEmpty) { + return '[ROOT]'; + } + return '[${path.join(".")}]'; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is OccurrenceAddress && + const ListEquality().equals(path, other.path); + + @override + int get hashCode => Object.hashAll(path); + + /// Resolve a pathname string (e.g. `"Top/counter/clk"` or + /// `"Top.counter.clk"`) to a [OccurrenceAddress] by walking [root]. + /// + /// Supports both `/` and `.` as separators. If the first segment + /// matches [root]'s name, it is skipped — the root + /// occurrence is always at the empty address. + /// + /// The last segment is first tried as a **signal** name within the + /// current occurrence; if that fails it is tried as a **child** + /// occurrence name. + /// This mirrors the pathname convention where a signal path has one more + /// segment than its parent module path. + /// + /// Returns `null` if any segment cannot be resolved. + /// + /// ```dart + /// final addr = OccurrenceAddress.tryFromPathname('Top/cpu/clk', root); + /// if (addr != null) { + /// final signal = service.signalByAddress(addr); + /// } + /// ``` + static OccurrenceAddress? tryFromPathname( + String pathname, + HierarchyOccurrence root, + ) { + final rootAddr = root.address ?? OccurrenceAddress.root; + final parts = pathname + .replaceAll('.', '/') + .split('/') + .where((s) => s.isNotEmpty) + .toList(); + + // Skip leading segment that matches the root name. + final segments = + parts.isNotEmpty && parts.first == root.name ? parts.skip(1) : parts; + + ({HierarchyOccurrence node, OccurrenceAddress addr})? step( + ({HierarchyOccurrence node, OccurrenceAddress addr})? cur, + String segment, + ) { + if (cur == null) { + return null; + } + final si = cur.node.signalIndexByName(segment); + if (identical(segment, segments.last) && si >= 0) { + return (node: cur.node, addr: cur.addr.signal(si)); + } + final ci = cur.node.childIndexByName(segment); + return ci >= 0 + ? (node: cur.node.children[ci], addr: cur.addr.child(ci)) + : null; + } + + return segments.fold<({HierarchyOccurrence node, OccurrenceAddress addr})?>( + (node: root, addr: rootAddr), step)?.addr; + } +} diff --git a/packages/rohd_hierarchy/lib/src/occurrence_search_result.dart b/packages/rohd_hierarchy/lib/src/occurrence_search_result.dart new file mode 100644 index 000000000..fafe8623c --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/occurrence_search_result.dart @@ -0,0 +1,45 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// occurrence_search_result.dart +// Result of a module/node search with enriched metadata. +// +// 2026 May +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; + +import 'package:rohd_hierarchy/src/hierarchy_occurrence.dart'; +import 'package:rohd_hierarchy/src/hierarchy_search_result.dart'; + +/// Result of an occurrence search with enriched metadata. +/// +/// Contains the occurrence's full path, parsed path segments, and the full +/// [HierarchyOccurrence] object. This mirrors `SignalSearchResult` for +/// occurrences and provides a consistent search results interface. +@immutable +class OccurrenceSearchResult extends HierarchySearchResult { + /// Alias for [id] — the occurrence's full hierarchical path. + String get occurrenceId => id; + + /// The underlying [HierarchyOccurrence] from the hierarchy service. + /// Contains the occurrence's name, type, children, and signals. + final HierarchyOccurrence occurrence; + + /// Creates an occurrence search result. + const OccurrenceSearchResult({ + required String occurrenceId, + required super.path, + required this.occurrence, + }) : super(id: occurrenceId); + + /// Whether this occurrence has sub-hierarchy (i.e. is not a primitive + /// leaf). + bool get isModule => !occurrence.isPrimitive; + + /// Number of direct child occurrences. + int get childCount => occurrence.children.length; + + @override + String toString() => 'OccurrenceSearchResult($id)'; +} diff --git a/packages/rohd_hierarchy/lib/src/prefix_query.dart b/packages/rohd_hierarchy/lib/src/prefix_query.dart new file mode 100644 index 000000000..2ce617683 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/prefix_query.dart @@ -0,0 +1,60 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// prefix_query.dart +// Prefix-substring query implementation for hierarchy search. +// +// 2026 May +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/src/hierarchy_query.dart'; + +/// Prefix-substring query: segments are matched via `startsWith` (signals) +/// or `contains` (occurrences) at successive hierarchy depths. +class PrefixQuery extends HierarchyQuery { + /// Non-empty segments parsed from the raw query. + late final List segments; + + /// Create a prefix query from [rawQuery]. + PrefixQuery( + super.rawQuery, { + super.target = SearchTarget.signals, + }) : super(crossesBoundaries: false) { + segments = rawQuery + .replaceAll('.', '/') + .split('/') + .map((s) => s.trim()) + .where((s) => s.isNotEmpty) + .toList(); + } + + @override + int get segmentCount => segments.length; + + @override + Set matchOccurrence(String occurrenceName, int stateIndex) { + if (stateIndex >= segments.length) { + return {stateIndex}; + } + final name = occurrenceName; + if (name.contains(segments[stateIndex])) { + return {stateIndex + 1}; + } + return const {}; + } + + @override + bool matchSignal(String signalName, int stateIndex) { + if (stateIndex >= segments.length) { + return true; + } + // Only the last segment can match a signal name. + if (stateIndex != segments.length - 1) { + return false; + } + return signalName.startsWith(segments[stateIndex]); + } + + @override + bool isComplete(int stateIndex) => stateIndex >= segments.length; +} diff --git a/packages/rohd_hierarchy/lib/src/regex_query.dart b/packages/rohd_hierarchy/lib/src/regex_query.dart new file mode 100644 index 000000000..6c0ebfe41 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/regex_query.dart @@ -0,0 +1,176 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// regex_query.dart +// Regex/glob query implementation for hierarchy search. +// +// 2026 May +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/src/hierarchy_query.dart'; + +/// Regex/glob query: each segment is a compiled regex, with `**` support +/// for crossing hierarchy boundaries. +/// +/// ## Segment syntax +/// +/// The query string is split on `/` into segments. Each segment is +/// independently compiled as a case-insensitive [RegExp] anchored to the +/// full occurrence or signal name (`^…$`). This means: +/// +/// - **Plain names** match exactly: `Top/CPU/clk`. +/// - **Glob wildcards** are auto-converted before compilation: +/// - `*` → `.*` (match any characters) +/// - `?` → `.` (match one character) +/// - These compose naturally: `clk*` matches `clk`, `clk_gated`, +/// `clk_div2`, etc. +/// - **Full regex** is supported within each segment since the string +/// is passed to [RegExp]: +/// - `d[0-9]+` — signals named `d0`, `d1`, `d12`, … +/// - `(clk|reset)` — either `clk` or `reset` +/// - `data_[a-z]{2}` — `data_ab`, `data_xy`, … +/// - `.*mux.*` — any name containing `mux` +/// - `ch[0-3]` — `ch0`, `ch1`, `ch2`, `ch3` +/// - `r[0-9]{1,2}` — `r0` through `r99` +/// - **`**`** (double-star, as its own segment) matches zero or more +/// hierarchy levels, allowing searches to cross boundaries: +/// - `Top/**/clk` — `clk` at any depth below `Top` +/// - `**/d[0-9]+` — any signal like `d0` anywhere +/// - `Top/**/ch*/data_*` — `data_*` signals inside `ch*` modules +/// +/// ## Interaction between glob and regex +/// +/// Glob conversion happens *before* regex compilation, so `*` and `?` +/// are always expanded. If you need a literal `*` or `?` in the regex, +/// escape them: `\*`, `\?`. All other regex metacharacters (`.`, `+`, +/// `|`, `(`, `)`, `[`, `]`, `{`, `}`, `^`, `$`) work as-is inside +/// each segment. +/// +/// ## Examples +/// +/// ```text +/// Query Matches +/// ───────────────────────────────────────────────────────────── +/// Top/CPU/clk exact: Top → CPU → clk +/// Top/CPU/* all signals in Top/CPU +/// Top/*/clk clk one level below Top +/// Top/**/clk clk at any depth below Top +/// Top/**/c.* signals starting with 'c' anywhere +/// **/clk clk anywhere in hierarchy +/// **/(clk|reset) clk or reset anywhere +/// Top/CPU/d[0-9]+ d0, d1, d12, … in Top/CPU +/// Top/**/ch[0-3]/data_* data_* in ch0–ch3 at any depth +/// Top/mem_*/addr[0-9]* addr0, addr1, … in mem_* modules +/// **/.*mux.* any name containing 'mux' anywhere +/// ``` +class RegexQuery extends HierarchyQuery { + /// Compiled segments — either a regex or a glob-star sentinel. + late final List segments; + + /// Create a regex query from [rawQuery]. + /// + /// A standalone `*` is converted to `.*`, `?` to `.`. The segment + /// `**` matches zero or more hierarchy levels. + RegexQuery( + super.rawQuery, { + super.target = SearchTarget.signals, + }) : super(crossesBoundaries: false) { + final parts = rawQuery + .split('/') + .map((s) => s.trim()) + .where((s) => s.isNotEmpty) + .toList(); + segments = parts.map((s) { + if (s == '**') { + return RegexSegment.globStar(); + } + final pattern = _globToRegex(s); + return RegexSegment(RegExp('^$pattern\$')); + }).toList(); + } + + @override + int get segmentCount => segments.length; + + @override + Set matchOccurrence(String occurrenceName, int stateIndex) { + if (stateIndex >= segments.length) { + return const {}; + } + final seg = segments[stateIndex]; + final results = {}; + if (seg.isGlobStar) { + // ** matches zero levels (skip) … + results + ..addAll(matchOccurrence(occurrenceName, stateIndex + 1)) + // … or consumes this node and stays at ** (one-or-more levels). + ..add(stateIndex); + } else if (seg.regex!.hasMatch(occurrenceName)) { + results.add(stateIndex + 1); + } + return results; + } + + @override + bool matchSignal(String signalName, int stateIndex) { + // Walk past any trailing **'s to find the signal-matching segment. + var i = stateIndex; + while (i < segments.length && segments[i].isGlobStar) { + i++; + } + if (i >= segments.length) { + return true; // all consumed + } + // The segment at i must be the last real regex. + if (!_allGlobStarAfter(i + 1)) { + return false; + } + return segments[i].regex!.hasMatch(signalName); + } + + @override + bool isComplete(int stateIndex) => + stateIndex >= segments.length || + segments.skip(stateIndex).every((s) => s.isGlobStar); + + /// Check if all segments from [fromIdx] onward are glob-stars. + bool _allGlobStarAfter(int fromIdx) => + segments.skip(fromIdx).every((s) => s.isGlobStar); + + /// Convert glob wildcards to regex equivalents. + static String _globToRegex(String segment) { + final buf = StringBuffer(); + for (var i = 0; i < segment.length; i++) { + final c = segment[i]; + if (c == '*') { + if (buf.toString().endsWith('.')) { + buf.write('*'); + } else { + buf.write('.*'); + } + } else if (c == '?') { + buf.write('.'); + } else { + buf.write(c); + } + } + return buf.toString(); + } +} + +/// A compiled regex segment for [RegexQuery]. +class RegexSegment { + /// The compiled regex, or null for glob-star segments. + final RegExp? regex; + + /// Whether this segment is a `**` glob-star. + final bool isGlobStar; + + /// Create a regex segment. + RegexSegment(this.regex) : isGlobStar = false; + + /// Create a glob-star segment (`**`). + RegexSegment.globStar() + : regex = null, + isGlobStar = true; +} diff --git a/packages/rohd_hierarchy/lib/src/signal_occurrence.dart b/packages/rohd_hierarchy/lib/src/signal_occurrence.dart new file mode 100644 index 000000000..d780cff8d --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/signal_occurrence.dart @@ -0,0 +1,273 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// signal_occurrence.dart +// A signal in the hardware occurrence hierarchy. +// +// 2026 May +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; +import 'package:rohd_hierarchy/src/hierarchy_occurrence.dart'; +import 'package:rohd_hierarchy/src/occurrence_address.dart'; + +/// Signals are the fundamental data carriers in hardware. A signal can be: +/// - An internal signal within an occurrence +/// - A port on an occurrence interface (has direction: input/output/inout) +/// +/// This is a structural model without waveform data. Path strings are +/// computed on demand from the parent occurrence reference — call [path] +/// with your desired separator. +class SignalOccurrence { + /// The name of the signal (bare name within its scope). + /// + /// Used for display, search, and local lookups within an occurrence. + /// Not guaranteed unique across the full hierarchy — use [path] for + /// unique keying. + final String name; + + /// The bit width of the signal. + final int width; + + /// Direction of the signal if it's a port. + /// Null for internal signals. + /// "input", "output", or "inout" for ports. + final String? direction; + + /// Current runtime value of the signal (if available). + /// Typically a hex or binary string representation. + final String? value; + + /// Whether this signal's value is computed/derivable (e.g. constant, + /// gate output, InlineSystemVerilog result) rather than directly tracked + /// by the waveform service. + final bool isComputed; + + /// Stable ordering index among ports in the parent occurrence. + /// + /// Set by the adapter that creates the signal. For ports (signals with + /// a [direction]), this records the deterministic position from the + /// original source (netlist JSON iteration order, ROHD module port + /// declaration order, etc.). Internal signals have `null`. + /// + /// [HierarchyOccurrence.buildAddresses] places ports before internal + /// signals when assigning [OccurrenceAddress] indices, so a port with + /// `portIndex == k` will receive signal address index `k`. + /// + /// Consumers that store connectivity by `(nodeId, portIndex)` tuples + /// (e.g. schematic hyperedges) rely on this value remaining stable + /// across incremental hierarchy expansion. + final int? portIndex; + + /// Type metadata from the netlist `logic_type` JSON field. + /// + /// For a **LogicStructure** (non-array), the format is: + /// ```json + /// {"typeName": "FloatingPoint", "fields": [ + /// {"name": "mantissa", "width": 4, "bits": [0,1,2,3]}, + /// {"name": "exponent", "width": 4, "bits": [4,5,6,7]}, + /// {"name": "sign", "width": 1, "bits": [8]} + /// ]} + /// ``` + /// + /// For a **LogicArray**, the format is: + /// ```json + /// {"width": 80, "arrayDims": [10], "elementWidth": 8} + /// ``` + /// + /// For a plain signal: `{"width": N}` or `null`. + /// + /// Nested structs have a recursive `"type"` key in their field entries. + Map? logicType; + + /// Hierarchical address for this signal. Assigned by + /// [HierarchyOccurrence.buildAddresses] to enable efficient navigation. + /// Format: [...occurrenceIndices, signalIndex] + OccurrenceAddress? get address => _address; + OccurrenceAddress? _address; + + /// Sets the address. Only for use by [HierarchyOccurrence.buildAddresses]. + @internal + set address(OccurrenceAddress? value) => _address = value; + + /// Parent occurrence containing this signal. Set by + /// [HierarchyOccurrence.buildAddresses]. + HierarchyOccurrence? get parent => _parent; + HierarchyOccurrence? _parent; + + /// Sets the parent. Only for use by [HierarchyOccurrence.buildAddresses]. + @internal + set parent(HierarchyOccurrence? value) => _parent = value; + + /// Creates a [SignalOccurrence] with the given properties. + SignalOccurrence({ + required this.name, + required this.width, + this.direction, + this.value, + this.isComputed = false, + this.portIndex, + this.logicType, + }); + + /// Whether this signal is a LogicStructure (has named sub-fields). + bool get isStruct => logicType != null && logicType!.containsKey('fields'); + + /// Whether this signal is a LogicArray (has indexed elements). + bool get isArray => logicType != null && logicType!.containsKey('arrayDims'); + + /// The struct type name (e.g. "FloatingPoint"), or null if not a struct. + String? get typeName => logicType?['typeName'] as String?; + + /// The struct field descriptors, or empty list if not a struct. + /// + /// Each field is `{"name": ..., "width": ..., "bits": [...]}` with an + /// optional `"type"` key for nested structs/arrays. + List> get structFields => + (logicType?['fields'] as List?)?.cast>() ?? + const []; + + /// Array dimensions (e.g. `[10]` for 1D, `[10, 2]` for 2D), or null. + List? get arrayDims => + (logicType?['arrayDims'] as List?)?.cast(); + + /// Element width for arrays, or null if not an array. + int? get arrayElementWidth => logicType?['elementWidth'] as int?; + + /// Returns the expected sub-field signal names derived from [logicType]. + /// + /// For structs, the synthesizer creates separate netnames for each field + /// following the Namer/Sanitizer conventions: + /// `Sanitizer.sanitizeSV(structureName)` → `{parentName}_{fieldName}` + /// + /// For example, signal `fp` with fields `mantissa`, `exponent`, `sign` + /// produces sub-field signal names: `fp_mantissa`, `fp_exponent`, `fp_sign`. + /// + /// These become separate [SignalOccurrence] entries in the same parent + /// module. Use `HierarchyOccurrence.findSubFieldSignals` to look + /// them up. + /// + /// Returns a list of `(expectedName, fieldLabel, width, startBit, + /// subLogicType)` for direct children. `expectedName` follows the + /// `{parentSignalName}_{fieldName}` convention. + /// `subLogicType` is non-null when the child is itself a sub-array + /// (remaining dimensions) and can be further expanded. + /// Empty if this is not a struct/array with known sub-fields. + List< + ({ + String expectedName, + String fieldLabel, + int width, + int startBit, + Map? subLogicType, + })> get subFieldDescriptors { + if (logicType == null) { + return const []; + } + return subFieldDescriptorsForType(logicType!, name); + } + + /// Compute sub-field descriptors for an arbitrary [logicType] map. + /// + /// [parentName] is used to derive expected signal names. + /// This is static so it can be called recursively for nested arrays + /// without needing a full [SignalOccurrence]. + static List< + ({ + String expectedName, + String fieldLabel, + int width, + int startBit, + Map? subLogicType, + })> subFieldDescriptorsForType( + Map logicType, + String parentName, + ) { + final fields = logicType['fields'] as List?; + if (fields != null) { + return fields.map((f) { + final field = f as Map; + final fieldName = field['name'] as String? ?? '?'; + final width = field['width'] as int? ?? 1; + final bits = field['bits'] as List?; + final startBit = bits != null && bits.isNotEmpty + ? (bits.cast().reduce((a, b) => a < b ? a : b)) + : 0; + // Naming convention: Sanitizer.sanitizeSV("$parentName.$fieldName") + // which produces "$parentName_$fieldName" + final expectedName = '${parentName}_$fieldName'; + return ( + expectedName: expectedName, + fieldLabel: fieldName, + width: width, + startBit: startBit, + subLogicType: field['type'] as Map?, + ); + }).toList(); + } + + final arrayDims = logicType['arrayDims'] as List?; + if (arrayDims != null && arrayDims.isNotEmpty) { + final leafWidth = (logicType['elementWidth'] as int?) ?? 1; + final outerDim = arrayDims.first as int; + // For multi-dimensional arrays, each outer element spans all + // remaining dimensions times the leaf element width. + final remainingDims = + arrayDims.length > 1 ? arrayDims.sublist(1).cast() : []; + final elementWidth = remainingDims.isEmpty + ? leafWidth + : remainingDims.fold(leafWidth, (acc, d) => acc * d); + + // Build sub-logicType for remaining dimensions (if any). + final subLogicType = remainingDims.isEmpty + ? null + : { + 'width': elementWidth, + 'arrayDims': remainingDims, + 'elementWidth': leafWidth, + }; + + return List.generate(outerDim, (i) { + // Naming convention: Sanitizer.sanitizeSV("$parentName[$i]") + // which produces "$parentName_${i}_" + final expectedName = '${parentName}_${i}_'; + return ( + expectedName: expectedName, + fieldLabel: '[$i]', + width: elementWidth, + startBit: i * elementWidth, + subLogicType: subLogicType, + ); + }); + } + + return const []; + } + + /// Compute the full hierarchical path for this signal. + /// + /// Joins the parent occurrence's path with this signal's [name] using + /// [separator]. Falls back to just [name] if parent is not yet set + /// (e.g. in test fixtures before `buildAddresses`). + String path({String separator = '/'}) { + if (_parent == null) { + return name; + } + return '${_parent!.path(separator: separator)}$separator$name'; + } + + /// Returns true if this signal is a port (has a direction). + bool get isPort => direction != null; + + /// Returns true if this is an input port. + bool get isInput => direction == 'input'; + + /// Returns true if this is an output port. + bool get isOutput => direction == 'output'; + + /// Returns true if this is a bidirectional port. + bool get isInout => direction == 'inout'; + + @override + String toString() => '$name (width=$width${isPort ? ', $direction' : ''})'; +} diff --git a/packages/rohd_hierarchy/lib/src/signal_search_result.dart b/packages/rohd_hierarchy/lib/src/signal_search_result.dart new file mode 100644 index 000000000..970855f74 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/signal_search_result.dart @@ -0,0 +1,49 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// signal_search_result.dart +// Result of a signal search with enriched metadata. +// +// 2026 May +// Author: Desmond Kirkpatrick + +import 'package:meta/meta.dart'; + +import 'package:rohd_hierarchy/src/hierarchy_search_result.dart'; +import 'package:rohd_hierarchy/src/signal_occurrence.dart'; + +/// Result of a signal search with enriched metadata. +/// +/// Contains the signal's full path, parsed path segments, and the full +/// [SignalOccurrence] object if available. This is the hierarchy-only portion +/// of search results; UI layers can use the pre-computed display helpers +/// directly without re-parsing paths. +@immutable +class SignalSearchResult extends HierarchySearchResult { + /// Alias for [id] — the signal's full hierarchical path. + String get signalId => id; + + /// The underlying [SignalOccurrence] from the hierarchy service (if + /// available). Contains width, direction, and other signal metadata. + final SignalOccurrence? signal; + + /// Creates a signal search result. + const SignalSearchResult({ + required String signalId, + required super.path, + this.signal, + }) : super(id: signalId); + + /// Occurrence names that need to be expanded to reveal this signal. + /// + /// These are the intermediate path segments between the top occurrence + /// and the signal name — i.e. everything except the first (top + /// occurrence) and last (signal name) segments. + /// + /// For `Top/sub1/sub2/clk` this returns `["sub1", "sub2"]`. + List get intermediateOccurrenceNames => + path.length > 2 ? path.sublist(1, path.length - 1) : const []; + + @override + String toString() => 'SignalSearchResult($id, width=${signal?.width ?? "?"})'; +} diff --git a/packages/rohd_hierarchy/pubspec.yaml b/packages/rohd_hierarchy/pubspec.yaml new file mode 100644 index 000000000..68c9bc4c8 --- /dev/null +++ b/packages/rohd_hierarchy/pubspec.yaml @@ -0,0 +1,19 @@ +name: rohd_hierarchy +description: "Generic hierarchy data models for hardware module navigation - HierarchyNode, Port, and HierarchyService." +homepage: https://intel.github.io/rohd-website/ +repository: https://github.com/intel/rohd +version: 0.1.0 +issue_tracker: https://github.com/intel/rohd/issues + +publish_to: none + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + collection: ^1.15.0 + meta: ^1.9.0 + +dev_dependencies: + lints: ^3.0.0 + test: ^1.17.3 diff --git a/packages/rohd_hierarchy/test/adapter_search_parity_test.dart b/packages/rohd_hierarchy/test/adapter_search_parity_test.dart new file mode 100644 index 000000000..cde3a301a --- /dev/null +++ b/packages/rohd_hierarchy/test/adapter_search_parity_test.dart @@ -0,0 +1,285 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// adapter_search_parity_test.dart +// Baseline tests verifying that search produces identical results +// regardless of which adapter populated the HierarchyService. +// +// 2026 April +// Author: Desmond Kirkpatrick + +// This is the key contract: once a HierarchyService is built, callers +// cannot tell whether the data came from VCD (BaseHierarchyAdapter.fromTree), +// netlist JSON (NetlistHierarchyAdapter), or any other source. + +import 'dart:convert'; + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +/// Concrete subclass that does NOT set root, so we can test the +/// StateError thrown by uninitialized access. +class _UnsetAdapter extends BaseHierarchyAdapter {} + +/// Resolve a pathname to a [SignalOccurrence] via +/// [OccurrenceAddress.tryFromPathname]. +SignalOccurrence? _resolve(HierarchyService svc, String path) { + final addr = OccurrenceAddress.tryFromPathname(path, svc.root); + if (addr == null) { + return null; + } + return svc.signalByAddress(addr); +} + +// ────────────────────────────────────────────────────────────────────── +// Build the SAME design via two different adapter paths +// ────────────────────────────────────────────────────────────────────── + +/// VCD-style: HierarchyNode tree with children/signals populated inline. +/// This is what `wellen` produces when loading a VCD/FST file. +BaseHierarchyAdapter _buildVcdAdapter() => BaseHierarchyAdapter.fromTree( + HierarchyOccurrence( + name: 'Abcd', + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'resetn', width: 1), + SignalOccurrence(name: 'arvalid_s', width: 1), + ], + children: [ + HierarchyOccurrence( + name: 'lab', + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'reset', width: 1), + SignalOccurrence(name: 'fromUpstream_request__st', width: 64), + ], + children: [ + HierarchyOccurrence( + name: 'cam', + signals: [ + SignalOccurrence(name: 'hit', width: 1), + SignalOccurrence(name: 'entry', width: 32), + ], + ), + ], + ), + ], + ), + ); + +/// Netlist JSON-style: flat-map adapter (like what DevTools/schematic viewer +/// builds from ROHD inspector JSON or netlist JSON). +/// Children and signals live in the adapter's flat maps, NOT inside +/// the HierarchyNode objects. +NetlistHierarchyAdapter _buildJsonAdapter() => + NetlistHierarchyAdapter.fromJson(jsonEncode({ + 'modules': { + 'Abcd': { + 'attributes': {'top': 1}, + 'ports': { + 'clk': { + 'direction': 'input', + 'bits': [1] + }, + 'resetn': { + 'direction': 'input', + 'bits': [2] + }, + 'arvalid_s': { + 'direction': 'input', + 'bits': [3] + }, + }, + 'netnames': {}, + 'cells': { + 'lab': { + 'type': 'Lab', + 'connections': {}, + }, + }, + }, + 'Lab': { + 'ports': { + 'clk': { + 'direction': 'input', + 'bits': [10] + }, + 'reset': { + 'direction': 'input', + 'bits': [11] + }, + 'fromUpstream_request__st': { + 'direction': 'input', + 'bits': List.generate(64, (i) => 100 + i) + }, + }, + 'netnames': {}, + 'cells': { + 'cam': { + 'type': 'Cam', + 'connections': {}, + }, + }, + }, + 'Cam': { + 'ports': { + 'hit': { + 'direction': 'input', + 'bits': [200] + }, + 'entry': { + 'direction': 'input', + 'bits': List.generate(32, (i) => 300 + i) + }, + }, + 'netnames': {}, + 'cells': {}, + }, + }, + })); + +void main() { + late HierarchyService vcdService; + late HierarchyService jsonService; + + setUp(() { + vcdService = _buildVcdAdapter(); + jsonService = _buildJsonAdapter(); + }); + + // ── The two services must be interchangeable for all search ops ── + // Case-insensitivity, dot separators, controller state, and + // search semantics are covered in address_conversion_test, + // hierarchy_search_controller_test, and regex_search_test. + // This file focuses exclusively on *parity* between adapters. + + group('Adapter search parity — both sources produce same results', () { + test('root name matches', () { + expect(vcdService.root.name, 'Abcd'); + expect(jsonService.root.name, 'Abcd'); + }); + + test('root.children returns same module names', () { + final vcdChildren = vcdService.root.children.map((c) => c.name).toSet(); + final jsonChildren = jsonService.root.children.map((c) => c.name).toSet(); + expect(vcdChildren, jsonChildren); + }); + + test('root.signals returns same signal names at root', () { + final vcdSigs = vcdService.root.signals.map((s) => s.name).toSet(); + final jsonSigs = jsonService.root.signals.map((s) => s.name).toSet(); + expect(vcdSigs, jsonSigs); + }); + + test('nested node signals() returns same signal names', () { + final vcdLab = vcdService.root.children.first; + final jsonLab = jsonService.root.children.first; + final vcdSigs = vcdLab.signals.map((s) => s.name).toSet(); + final jsonSigs = jsonLab.signals.map((s) => s.name).toSet(); + expect(vcdSigs, jsonSigs); + }); + + test('signalByAddress works on both — top level', () { + final vcdClk = _resolve(vcdService, 'Abcd/clk'); + final jsonClk = _resolve(jsonService, 'Abcd/clk'); + expect(vcdClk, isNotNull, reason: 'VCD: Abcd/clk'); + expect(jsonClk, isNotNull, reason: 'JSON: Abcd/clk'); + expect(vcdClk!.name, 'clk'); + expect(jsonClk!.name, 'clk'); + }); + + test('signalByAddress works on both — nested', () { + final vcdHit = _resolve(vcdService, 'Abcd/lab/cam/hit'); + final jsonHit = _resolve(jsonService, 'Abcd/lab/cam/hit'); + expect(vcdHit, isNotNull, reason: 'VCD: Abcd/lab/cam/hit'); + expect(jsonHit, isNotNull, reason: 'JSON: Abcd/lab/cam/hit'); + expect(vcdHit!.name, 'hit'); + expect(jsonHit!.name, 'hit'); + }); + + test('searchSignals plain query — same result names', () { + final vcdResults = + vcdService.searchSignals('clk').map((r) => r.name).toSet(); + final jsonResults = + jsonService.searchSignals('clk').map((r) => r.name).toSet(); + expect(vcdResults, isNotEmpty); + expect(vcdResults, jsonResults); + }); + + test('searchSignals glob query — same result names', () { + final vcdResults = + vcdService.searchSignals('**/clk').map((r) => r.name).toSet(); + final jsonResults = + jsonService.searchSignals('**/clk').map((r) => r.name).toSet(); + expect(vcdResults, isNotEmpty); + expect(vcdResults, jsonResults); + }); + + test('searchSignals path query — same result names', () { + final vcdResults = + vcdService.searchSignals('lab/clk').map((r) => r.name).toSet(); + final jsonResults = + jsonService.searchSignals('lab/clk').map((r) => r.name).toSet(); + expect(vcdResults, isNotEmpty); + expect(vcdResults, jsonResults); + }); + + test('searchModules — same module names', () { + final vcdNodes = vcdService + .searchOccurrences('lab') + .map((r) => r.occurrence.name) + .toSet(); + final jsonNodes = jsonService + .searchOccurrences('lab') + .map((r) => r.occurrence.name) + .toSet(); + expect(vcdNodes, isNotEmpty); + expect(vcdNodes, jsonNodes); + }); + + test('searchModules nested — same module names', () { + final vcdNodes = vcdService + .searchOccurrences('cam') + .map((r) => r.occurrence.name) + .toSet(); + final jsonNodes = jsonService + .searchOccurrences('cam') + .map((r) => r.occurrence.name) + .toSet(); + expect(vcdNodes, isNotEmpty); + expect(vcdNodes, jsonNodes); + }); + }); + + // ── Verify the external-hierarchy handoff works ── + // Individual search/address semantics are covered elsewhere. + // This group tests the adapter re-wrapping contract. + + group('External hierarchy flow (simulates DevTools → wave viewer)', () { + test('BaseHierarchyAdapter.fromTree produces identical search results', () { + final rewrapped = BaseHierarchyAdapter.fromTree(jsonService.root); + + final results = rewrapped.searchSignals('clk'); + expect(results, isNotEmpty); + expect( + results.map((r) => r.name).toSet(), + jsonService.searchSignals('clk').map((r) => r.name).toSet(), + ); + }); + + test('BaseHierarchyAdapter.fromTree preserves signalByAddress', () { + final rewrapped = BaseHierarchyAdapter.fromTree(jsonService.root); + + final hit = _resolve(rewrapped, 'Abcd/lab/cam/hit'); + expect(hit, isNotNull); + expect(hit!.name, 'hit'); + }); + }); + + group('BaseHierarchyAdapter.root', () { + test('throws StateError when root is not set', () { + final adapter = _UnsetAdapter(); + expect(() => adapter.root, throwsStateError); + }); + }); +} diff --git a/packages/rohd_hierarchy/test/address_conversion_test.dart b/packages/rohd_hierarchy/test/address_conversion_test.dart new file mode 100644 index 000000000..2caabdef6 --- /dev/null +++ b/packages/rohd_hierarchy/test/address_conversion_test.dart @@ -0,0 +1,286 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// address_conversion_test.dart +// Tests for HierarchyService address ↔ pathname conversion methods. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +void main() { + group('Address ↔ pathname conversion', () { + late HierarchyService service; + late HierarchyOccurrence root; + + // Build a test hierarchy: + // Top + // ├─ cpu (child 0) + // │ ├─ signals: clk, rst + // │ └─ alu (child 0 of cpu) + // │ └─ signals: a, b, out + // └─ mem (child 1) + // └─ signals: addr, data + + setUpAll(() { + final alu = HierarchyOccurrence( + name: 'alu', + signals: [ + SignalOccurrence( + name: 'a', + width: 1, + ), + SignalOccurrence( + name: 'b', + width: 1, + ), + SignalOccurrence( + name: 'out', + width: 1, + ), + ], + ); + + final cpu = HierarchyOccurrence( + name: 'cpu', + signals: [ + SignalOccurrence( + name: 'clk', + width: 1, + ), + SignalOccurrence( + name: 'rst', + width: 1, + ), + ], + children: [alu], + ); + + final mem = HierarchyOccurrence( + name: 'mem', + signals: [ + SignalOccurrence( + name: 'addr', + width: 1, + ), + SignalOccurrence( + name: 'data', + width: 1, + ), + ], + ); + + root = HierarchyOccurrence( + name: 'Top', + children: [cpu, mem], + )..buildAddresses(); + + service = BaseHierarchyAdapter.fromTree(root); + }); + + group('pathnameToAddress', () { + test('root name resolves to root address', () { + final addr = service.pathnameToAddress('Top'); + expect(addr, isNotNull); + expect(addr!.path, equals([])); + }); + + test('module path resolves correctly', () { + final addr = service.pathnameToAddress('Top/cpu'); + expect(addr, isNotNull); + expect(addr!.path, equals([0])); + }); + + test('nested module path resolves correctly', () { + final addr = service.pathnameToAddress('Top/cpu/alu'); + expect(addr, isNotNull); + expect(addr!.path, equals([0, 0])); + }); + + test('second child module resolves correctly', () { + final addr = service.pathnameToAddress('Top/mem'); + expect(addr, isNotNull); + expect(addr!.path, equals([1])); + }); + + test('signal path resolves correctly', () { + final addr = service.pathnameToAddress('Top/cpu/clk'); + expect(addr, isNotNull); + expect(addr!.path, equals([0, 0])); // cpu[0], signal clk[0] + }); + + test('second signal resolves correctly', () { + final addr = service.pathnameToAddress('Top/cpu/rst'); + expect(addr, isNotNull); + expect(addr!.path, equals([0, 1])); // cpu[0], signal rst[1] + }); + + test('nested signal resolves correctly', () { + final addr = service.pathnameToAddress('Top/cpu/alu/out'); + expect(addr, isNotNull); + expect(addr!.path, equals([0, 0, 2])); // cpu[0], alu[0], out[2] + }); + + test('dot-separated paths work too', () { + final addr = service.pathnameToAddress('Top.cpu.alu.b'); + expect(addr, isNotNull); + expect(addr!.path, equals([0, 0, 1])); // cpu[0], alu[0], b[1] + }); + + test('non-existent path returns null', () { + expect(service.pathnameToAddress('Top/nonexistent'), isNull); + }); + + test('non-existent signal returns null', () { + expect(service.pathnameToAddress('Top/cpu/nonexistent'), isNull); + }); + + test('empty string returns root', () { + final addr = service.pathnameToAddress(''); + expect(addr, isNotNull); + expect(addr!.path, isEmpty); + }); + }); + + group('addressToPathname', () { + test('root address returns root name', () { + expect( + service.addressToPathname(OccurrenceAddress.root), + equals('Top'), + ); + }); + + test('module address resolves correctly', () { + expect( + service.addressToPathname(const OccurrenceAddress([0])), + equals('Top/cpu'), + ); + }); + + test('nested module address resolves correctly', () { + expect( + service.addressToPathname(const OccurrenceAddress([0, 0])), + equals('Top/cpu/alu'), + ); + }); + + test('signal address resolves with asSignal flag', () { + expect( + service.addressToPathname( + const OccurrenceAddress([0, 0]), + asSignal: true, + ), + equals('Top/cpu/clk'), + ); + }); + + test('nested signal address resolves with asSignal flag', () { + expect( + service.addressToPathname( + const OccurrenceAddress([0, 0, 2]), + asSignal: true, + ), + equals('Top/cpu/alu/out'), + ); + }); + + test('out-of-bounds child returns null', () { + expect( + service.addressToPathname(const OccurrenceAddress([5])), + isNull, + ); + }); + + test('out-of-bounds signal returns null', () { + expect( + service.addressToPathname( + const OccurrenceAddress([0, 99]), + asSignal: true, + ), + isNull, + ); + }); + }); + + group('nodeByAddress', () { + test('root address returns root', () { + final node = service.occurrenceByAddress(OccurrenceAddress.root); + expect(node?.name, equals('Top')); + }); + + test('child address returns correct child', () { + final node = service.occurrenceByAddress(const OccurrenceAddress([0])); + expect(node?.name, equals('cpu')); + }); + + test('nested address returns correct node', () { + final node = + service.occurrenceByAddress(const OccurrenceAddress([0, 0])); + expect(node?.name, equals('alu')); + }); + + test('out-of-bounds returns null', () { + expect( + service.occurrenceByAddress(const OccurrenceAddress([99])), + isNull, + ); + }); + }); + + group('signalByAddress', () { + test('signal address returns correct signal', () { + // cpu's first signal (clk) has address [0, 0] + final clkAddr = root.children[0].signals[0].address!; + final sig = service.signalByAddress(clkAddr); + expect(sig?.name, equals('clk')); + }); + + test('nested signal address returns correct signal', () { + // alu's third signal (out) has address [0, 0, 2] + final outAddr = root.children[0].children[0].signals[2].address!; + final sig = service.signalByAddress(outAddr); + expect(sig?.name, equals('out')); + }); + + test('root address returns null (not a signal)', () { + expect(service.signalByAddress(OccurrenceAddress.root), isNull); + }); + }); + + group('waveformIdToAddress', () { + test('dot-separated waveform ID resolves', () { + final addr = service.waveformIdToAddress('Top.cpu.alu.a'); + expect(addr, isNotNull); + expect(addr!.path, equals([0, 0, 0])); // cpu[0], alu[0], a[0] + }); + }); + + group('round-trip', () { + test('pathname → address → pathname preserves module path', () { + const path = 'Top/cpu/alu'; + final addr = service.pathnameToAddress(path); + expect(addr, isNotNull); + final roundTripped = service.addressToPathname(addr!); + expect(roundTripped, equals(path)); + }); + + test('pathname → address → pathname preserves signal path', () { + const path = 'Top/cpu/alu/out'; + final addr = service.pathnameToAddress(path); + expect(addr, isNotNull); + final roundTripped = service.addressToPathname(addr!, asSignal: true); + expect(roundTripped, equals(path)); + }); + + test('address → pathname → address preserves module address', () { + const addr = OccurrenceAddress([0, 0]); + final path = service.addressToPathname(addr); + expect(path, isNotNull); + final roundTripped = service.pathnameToAddress(path!); + expect(roundTripped?.path, equals(addr.path)); + }); + }); + }); +} diff --git a/packages/rohd_hierarchy/test/coverage_gaps_test.dart b/packages/rohd_hierarchy/test/coverage_gaps_test.dart new file mode 100644 index 000000000..1e29bcfe5 --- /dev/null +++ b/packages/rohd_hierarchy/test/coverage_gaps_test.dart @@ -0,0 +1,120 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// coverage_gaps_test.dart +// Tests for API surface not covered by other test files: +// - BaseHierarchyAdapter.root StateError on uninitialized access +// - SignalOccurrence as port +// - HierarchyOccurrence.parent +// - SignalOccurrence.value +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +/// Concrete subclass that does NOT set root, so we can test the +/// StateError thrown by uninitialized access. +class _UnsetAdapter extends BaseHierarchyAdapter {} + +void main() { + group('BaseHierarchyAdapter.root', () { + test('throws StateError when root is not set', () { + final adapter = _UnsetAdapter(); + expect(() => adapter.root, throwsStateError); + }); + }); + + group('SignalOccurrence as port', () { + test('creates a port signal with defaults', () { + final p = SignalOccurrence(name: 'clk', width: 1, direction: 'input'); + expect(p.name, 'clk'); + expect(p.direction, 'input'); + expect(p.width, 1); + expect(p.isPort, isTrue); + expect(p.isInput, isTrue); + }); + + test('creates a port signal with explicit overrides', () { + final p = SignalOccurrence( + name: 'data', + direction: 'output', + width: 32, + isComputed: true, + ); + HierarchyOccurrence(name: 'Top', signals: [p]).buildAddresses(); + expect(p.name, 'data'); + expect(p.width, 32); + expect(p.direction, 'output'); + expect(p.path(), 'Top/data'); + expect(p.parent!.path(), 'Top'); + expect(p.isComputed, isTrue); + expect(p.isOutput, isTrue); + }); + }); + + group('HierarchyOccurrence.parent', () { + test('parent is null for root', () { + final root = HierarchyOccurrence(name: 'Top')..buildAddresses(); + expect(root.parent, isNull); + }); + + test('parent is set for child nodes after buildAddresses', () { + final child = HierarchyOccurrence(name: 'sub'); + final root = HierarchyOccurrence(name: 'Top', children: [child]) + ..buildAddresses(); + expect(child.parent, same(root)); + expect(child.path(), 'Top/sub'); + }); + }); + + group('SignalOccurrence.value', () { + test('value is null by default', () { + final s = SignalOccurrence(name: 'a', width: 1); + expect(s.value, isNull); + }); + + test('value stores the provided runtime value', () { + final s = SignalOccurrence(name: 'a', width: 8, value: 'ff'); + expect(s.value, 'ff'); + }); + }); + + group('HierarchyOccurrence.definition', () { + test('type is null when not provided', () { + final n = HierarchyOccurrence(name: 'a'); + expect(n.definition, isNull); + }); + + test('type is stored when provided', () { + final n = HierarchyOccurrence(name: 'a', definition: 'Counter'); + expect(n.definition, 'Counter'); + }); + }); + + group('SignalOccurrence.parent', () { + test('parent is null before buildAddresses', () { + final s = SignalOccurrence(name: 'a', width: 1); + expect(s.parent, isNull); + }); + + test('parent is set after buildAddresses', () { + final s = SignalOccurrence(name: 'a', width: 1); + HierarchyOccurrence( + name: 'Top', + children: [ + HierarchyOccurrence(name: 'sub', signals: [s]) + ], + ).buildAddresses(); + expect(s.parent!.path(), 'Top/sub'); + }); + }); + + group('isPrimitive on nodes', () { + test('default isPrimitive is false', () { + final n = HierarchyOccurrence(name: 'sub'); + expect(n.isPrimitive, isFalse); + }); + }); +} diff --git a/packages/rohd_hierarchy/test/devtools_search_flow_test.dart b/packages/rohd_hierarchy/test/devtools_search_flow_test.dart new file mode 100644 index 000000000..ac5f11ad9 --- /dev/null +++ b/packages/rohd_hierarchy/test/devtools_search_flow_test.dart @@ -0,0 +1,214 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// devtools_search_flow_test.dart +// Tests that simulate the DevTools embedding flow with local signal IDs: +// HierarchyNode tree → BaseHierarchyAdapter.fromTree → search +// +// 2026 April +// Author: Desmond Kirkpatrick + +// The test verifies that search works correctly with local signal IDs +// (as opposed to VCD-style full-path IDs), catching any assumption +// mismatches in the search engine. + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +/// Build the test hierarchy tree directly, matching the structure +/// that would be produced from ROHD inspector JSON. +/// Signals have local IDs and full qualified paths. +HierarchyOccurrence _buildTestHierarchy() { + final cam = HierarchyOccurrence( + name: 'cam', + signals: [ + SignalOccurrence( + name: 'clk', + width: 1, + direction: 'input', + ), + SignalOccurrence( + name: 'hit', + width: 1, + direction: 'input', + ), + SignalOccurrence( + name: 'entry', + width: 32, + direction: 'input', + ), + SignalOccurrence( + name: 'match_out', + width: 1, + direction: 'output', + ), + ], + ); + + final lab = HierarchyOccurrence( + name: 'lab', + children: [cam], + signals: [ + SignalOccurrence( + name: 'clk', + width: 1, + direction: 'input', + ), + SignalOccurrence( + name: 'reset', + width: 1, + direction: 'input', + ), + SignalOccurrence( + name: 'fromUpstream_request__st', + width: 64, + direction: 'input', + ), + SignalOccurrence( + name: 'toUpstream_response__st', + width: 64, + direction: 'output', + ), + ], + ); + + final dmaEngine = HierarchyOccurrence( + name: 'engine', + signals: [ + SignalOccurrence( + name: 'clk', + width: 1, + direction: 'input', + ), + SignalOccurrence( + name: 'enable', + width: 1, + direction: 'input', + ), + SignalOccurrence( + name: 'data_in', + width: 64, + direction: 'input', + ), + SignalOccurrence( + name: 'data_out', + width: 64, + direction: 'output', + ), + SignalOccurrence( + name: 'done', + width: 1, + direction: 'output', + ), + ], + ); + + return HierarchyOccurrence( + name: 'Abcd', + children: [lab, dmaEngine], + signals: [ + SignalOccurrence( + name: 'clk', + width: 1, + direction: 'input', + ), + SignalOccurrence( + name: 'resetn', + width: 1, + direction: 'input', + ), + SignalOccurrence( + name: 'araddr_s', + width: 32, + direction: 'input', + ), + SignalOccurrence( + name: 'rdata_s', + width: 32, + direction: 'output', + ), + ], + ); +} + +void main() { + late BaseHierarchyAdapter service; + + setUp(() { + final root = _buildTestHierarchy()..buildAddresses(); + service = BaseHierarchyAdapter.fromTree(root); + }); + + group( + 'DevTools flow — local signal IDs ' + '→ BaseHierarchyAdapter.fromTree → search', () { + // Basic search, address, glob, and controller behavior is covered by + // hierarchy_search_controller_test, regex_search_test, + // address_conversion_test, and module_search_test. + // + // This group focuses on what is unique to the DevTools local-ID flow: + // search correctness when SignalOccurrence.name is a local name (not a full + // path). + + test('search works with local signal IDs', () { + // Plain prefix search still finds signals by name + final results = service.searchSignals('clk'); + expect(results, isNotEmpty); + expect(results.map((r) => r.name), everyElement('clk')); + // Glob still works + final globResults = service.searchSignals('**/entry'); + expect(globResults, isNotEmpty); + expect(globResults.first.name, 'entry'); + }); + + test('signalByAddress resolves despite local IDs', () { + final addr = + OccurrenceAddress.tryFromPathname('Abcd/lab/cam/hit', service.root); + expect(addr, isNotNull); + final hit = service.signalByAddress(addr!); + expect(hit, isNotNull); + expect(hit!.name, 'hit'); + expect(hit.name, 'hit'); // local, not full path + expect(hit.path(), 'Abcd/lab/cam/hit'); + }); + + test('searchModules works with local-ID tree', () { + final results = service.searchOccurrences('cam'); + expect(results, isNotEmpty); + expect(results.first.occurrence.name, 'cam'); + }); + }); + + // ── SignalOccurrence ID format verification ── + + group('local signal ID format', () { + test('signals have local IDs (not full paths)', () { + final sigs = service.root.signals; + final clk = sigs.firstWhere((s) => s.name == 'clk'); + // The signal id is the local name, not the full path + expect(clk.name, 'clk'); + // But fullPath is the full qualified path + expect(clk.path(), 'Abcd/clk'); + }); + + test('local signal IDs do not break address resolution', () { + final addr = OccurrenceAddress.tryFromPathname( + 'Abcd/lab/cam/match_out', service.root); + expect(addr, isNotNull); + final result = service.signalByAddress(addr!); + expect(result, isNotNull); + expect(result!.name, 'match_out'); + expect(result.name, 'match_out'); // local name + }); + + test('search results carry the correct signal object', () { + final results = service.searchSignals('Abcd/rdata_s'); + expect(results, isNotEmpty); + final r = results.first; + expect(r.signal, isNotNull); + expect(r.signal!.name, 'rdata_s'); // local name + expect(r.signal!.path(), 'Abcd/rdata_s'); // full path + expect(r.signal!.width, 32); + }); + }); +} diff --git a/packages/rohd_hierarchy/test/filter_bank_integration_test.dart b/packages/rohd_hierarchy/test/filter_bank_integration_test.dart new file mode 100644 index 000000000..4ed1ee309 --- /dev/null +++ b/packages/rohd_hierarchy/test/filter_bank_integration_test.dart @@ -0,0 +1,710 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// filter_bank_integration_test.dart +// Integration tests using a real ROHD FilterBank netlist JSON fixture. +// Covers model getters, service methods, and adapter edge cases. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'dart:io'; + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +/// Load the slim FilterBank fixture and build a NetlistHierarchyAdapter. +NetlistHierarchyAdapter _loadFixture() { + final json = File('test/fixtures/filter_bank.json').readAsStringSync(); + return NetlistHierarchyAdapter.fromJson(json); +} + +void main() { + late NetlistHierarchyAdapter adapter; + late HierarchyService service; + + setUpAll(() { + adapter = _loadFixture(); + service = adapter; + service.root.buildAddresses(); + }); + + // ─────────────── NetlistHierarchyAdapter parsing ─────────────── + + group('NetlistHierarchyAdapter — FilterBank fixture', () { + test('top module is FilterBank', () { + expect(service.root.name, 'FilterBank'); + }); + + test('rootNameOverride replaces root node name', () { + final json = File('test/fixtures/filter_bank.json').readAsStringSync(); + final custom = NetlistHierarchyAdapter.fromJson( + json, + rootNameOverride: 'MyDesign', + ); + expect(custom.root.name, 'MyDesign'); + expect(custom.root.definition, 'FilterBank'); + }); + + test('child modules preserve definition separately from instance name', () { + final controller = service.root.children.firstWhere( + (child) => child.name == 'controller_1', + ); + + expect(controller.name, 'controller_1'); + expect(controller.definition, 'FilterController'); + }); + + test('root has expected ports as signals', () { + final portNames = service.root.signals.map((s) => s.name).toSet(); + expect(portNames, containsAll(['clk', 'reset', 'start', 'done'])); + }); + + test('has hierarchical children (ch0, ch1, controller)', () { + final childNames = service.root.children.map((c) => c.name).toSet(); + // ch0_1 and ch1_1 are FilterChannel instances; controller_1 is + // FilterController + expect(childNames, containsAll(['ch0_1', 'ch1_1', 'controller_1'])); + }); + + test('primitive cells are marked isPrimitive', () { + // array_slice cells in FilterBank are $slice — primitive + final sliceCells = service.root.children.where( + (c) => c.definition != null && c.definition!.startsWith(r'$'), + ); + expect(sliceCells, isNotEmpty); + for (final cell in sliceCells) { + expect( + cell.isPrimitive, + isTrue, + reason: '${cell.name} (${cell.definition}) should be primitive', + ); + } + }); + + test('primitive cells have port signals from port_directions', () { + final primitives = service.root.children.where((c) => c.isPrimitive); + for (final prim in primitives) { + expect( + prim.signals, + isNotEmpty, + reason: '${prim.name} should have port signals', + ); + // All signals on primitive cells should have a direction + for (final s in prim.signals) { + expect( + s.isPort, + isTrue, + reason: '${prim.name}/${s.name} should be a port', + ); + expect(s.direction, isNotEmpty); + } + } + }); + + test('netnames with hide_name=1 are excluded', () { + // FilterBank has controller_1_loadingPhase with hide_name=1 + final allSignalNames = service.root.depthFirstSignals().map( + (s) => s.name, + ); + expect(allSignalNames, isNot(contains('controller_1_loadingPhase'))); + }); + + test('netnames with computed attribute are included with isComputed', () { + // CoeffBank has const_0_2_h0 with computed=1 + // Navigate: FilterBank → ch0_1 → one of its children should have + // a CoeffBank with computed signals + bool foundComputed(HierarchyOccurrence node) { + for (final s in node.signals) { + if (s.isComputed) { + return true; + } + } + return node.children.any(foundComputed); + } + + expect( + foundComputed(service.root), + isTrue, + reason: 'Should have at least one computed signal', + ); + }); + + test(r'$-prefixed netnames are excluded', () { + // Any netname starting with $ should be filtered out + final allNames = service.root.depthFirstSignals().map((s) => s.name); + final dollarNames = allNames.where((n) => n.startsWith(r'$')); + expect( + dollarNames, + isEmpty, + reason: r'No $-prefixed netnames should appear', + ); + }); + }); + + // ─────────────── HierarchyNode model getters ─────────────── + + group('HierarchyNode model getters', () { + test('ports returns only signals with direction', () { + final ports = service.root.ports; + expect(ports, isNotEmpty); + for (final p in ports) { + expect(p.isPort, isTrue); + expect(p.direction, isNotEmpty); + } + }); + + test('inputs returns only input ports', () { + final inputs = service.root.inputs; + expect(inputs, isNotEmpty); + for (final s in inputs) { + expect(s.direction, 'input'); + } + expect(inputs.map((s) => s.name), contains('clk')); + }); + + test('outputs returns only output ports', () { + final outputs = service.root.outputs; + expect(outputs, isNotEmpty); + for (final s in outputs) { + expect(s.direction, 'output'); + } + expect(outputs.map((s) => s.name), contains('done')); + }); + + test(r'isPrimitiveType is true for $-prefixed types', () { + expect(HierarchyOccurrence.isPrimitiveType(r'$mux'), isTrue); + expect(HierarchyOccurrence.isPrimitiveType(r'$and'), isTrue); + }); + + test(r'isPrimitiveType is false for non-$-prefixed types', () { + expect(HierarchyOccurrence.isPrimitiveType('FilterBank'), isFalse); + }); + + test('isPrimitiveType is false for empty string', () { + expect(HierarchyOccurrence.isPrimitiveType(''), isFalse); + }); + + test('isPrimitiveCell reflects isPrimitive field and type', () { + // A node marked isPrimitive=true + final primCell = service.root.children.firstWhere((c) => c.isPrimitive); + expect(primCell.isPrimitiveCell, isTrue); + + // The root module is not primitive + expect(service.root.isPrimitiveCell, isFalse); + }); + + test('depthFirstSignals places root signals first', () { + final all = service.root.depthFirstSignals(); + expect(all, isNotEmpty); + + final rootSigs = service.root.signals; + for (var i = 0; i < rootSigs.length; i++) { + expect(all[i].name, rootSigs[i].name); + } + }); + + test('depthFirstSignals count equals recursive signal total', () { + final all = service.root.depthFirstSignals(); + int countSignals(HierarchyOccurrence n) => + n.signals.length + + n.children.fold(0, (sum, c) => sum + countSignals(c)); + expect(all.length, countSignals(service.root)); + }); + }); + + // ─────────────── SignalOccurrence model getters ─────────────── + + group('SignalOccurrence model getters', () { + test('isPort is true for Port instances', () { + final port = service.root.signals.first; + expect(port.isPort, isTrue); + }); + + test('input port has isInput true and isOutput/isInout false', () { + final clk = service.root.signals.firstWhere((s) => s.name == 'clk'); + expect(clk.isPort, isTrue); + expect(clk.isInput, isTrue); + expect(clk.isOutput, isFalse); + expect(clk.isInout, isFalse); + }); + + test('output port has isOutput true and isInput false', () { + final done = service.root.signals.firstWhere((s) => s.name == 'done'); + expect(done.isOutput, isTrue); + expect(done.isInput, isFalse); + }); + + test('isPort is false for non-Port signals (internal wires)', () { + // Internal signals (from netnames) are SignalOccurrence, not Port. + // The fixture includes visible non-port netnames like tapMatch0. + final allSigs = service.root.depthFirstSignals(); + final nonPorts = allSigs.where((s) => !s.isPort).toList(); + expect( + nonPorts, + isNotEmpty, + reason: 'Should have non-Port internal signals from netnames', + ); + }); + + test('SignalOccurrence.toString includes name and width', () { + final clk = service.root.signals.firstWhere((s) => s.name == 'clk'); + final str = clk.toString(); + expect(str, contains('clk')); + }); + }); + + // ─────────────── HierarchyService methods ─────────────── + + group('HierarchyService — search coverage', () { + test('searchNodes returns HierarchyNode objects', () { + final nodes = service.matchOccurrences('controller'); + expect(nodes, isNotEmpty); + for (final n in nodes) { + expect(n, isA()); + } + }); + + test('autocompletePaths returns children for partial path', () { + final suggestions = service.autocompletePaths('FilterBank/'); + expect(suggestions, isNotEmpty); + for (final s in suggestions) { + expect(s, startsWith('FilterBank/')); + } + }); + + test('autocompletePaths filters by prefix', () { + final suggestions = service.autocompletePaths('FilterBank/ch'); + expect(suggestions, isNotEmpty); + for (final s in suggestions) { + expect(s.toLowerCase(), contains('/ch')); + } + }); + + test('autocompletePaths with empty string returns root', () { + final suggestions = service.autocompletePaths(''); + // Should suggest root-level completions + expect(suggestions, isNotEmpty); + }); + + test('autocompletePaths appends / for nodes with children', () { + final suggestions = service.autocompletePaths('FilterBank/'); + final withSlash = suggestions.where((s) => s.endsWith('/')); + // At least ch0_1 and ch1_1 have children + expect(withSlash, isNotEmpty); + }); + + test('hasRegexChars is false for plain text', () { + expect(HierarchyService.hasRegexChars('clk'), isFalse); + }); + + test('hasRegexChars detects * glob', () { + expect(HierarchyService.hasRegexChars('c*'), isTrue); + }); + + test('hasRegexChars detects ? glob', () { + expect(HierarchyService.hasRegexChars('cl?'), isTrue); + }); + + test('hasRegexChars detects character class', () { + expect(HierarchyService.hasRegexChars('[a-z]'), isTrue); + }); + + test('hasRegexChars detects group alternation', () { + expect(HierarchyService.hasRegexChars('(a|b)'), isTrue); + }); + + test('hasRegexChars detects + quantifier', () { + expect(HierarchyService.hasRegexChars('a+'), isTrue); + }); + + test('longestCommonPrefix finds shared prefix', () { + expect( + HierarchyService.longestCommonPrefix([ + 'FilterBank/ch0', + 'FilterBank/ch1', + ]), + 'FilterBank/ch', + ); + }); + + test('longestCommonPrefix returns null for empty list', () { + expect(HierarchyService.longestCommonPrefix([]), isNull); + }); + + test('longestCommonPrefix returns null for no common prefix', () { + expect(HierarchyService.longestCommonPrefix(['abc', 'xyz']), isNull); + }); + + test('longestCommonPrefix is case-sensitive', () { + final prefix = HierarchyService.longestCommonPrefix([ + 'Filter/abc', + 'Filter/abd', + ]); + expect(prefix, 'Filter/ab'); + }); + }); + + // ─────────────── HierarchySearchController ─────────────── + + group('HierarchySearchController — additional coverage', () { + test('selectAt selects valid index', () { + final ctrl = HierarchySearchController.forSignals( + service, + )..updateQuery('clk'); + expect(ctrl.hasResults, isTrue); + + ctrl.selectAt(0); + expect(ctrl.selectedIndex, 0); + }); + + test('selectAt clamps high index to last result', () { + final ctrl = HierarchySearchController.forSignals( + service, + )..updateQuery('clk'); + expect(ctrl.hasResults, isTrue); + + ctrl.selectAt(999); + expect(ctrl.selectedIndex, ctrl.results.length - 1); + }); + + test('selectAt clamps negative index to zero', () { + final ctrl = HierarchySearchController.forSignals( + service, + )..updateQuery('clk'); + expect(ctrl.hasResults, isTrue); + + ctrl.selectAt(-5); + expect(ctrl.selectedIndex, 0); + }); + + test('selectAt on empty results is no-op', () { + final ctrl = HierarchySearchController.forSignals( + service, + )..selectAt(3); + expect(ctrl.selectedIndex, 0); + expect(ctrl.hasResults, isFalse); + }); + + test('tabComplete expands to longest common prefix', () { + final ctrl = HierarchySearchController.forSignals( + service, + )..updateQuery('clk'); + if (ctrl.results.length > 1) { + final expansion = ctrl.tabComplete('clk'); + // Expansion should be longer than the query if results share a + // common prefix beyond 'clk' + if (expansion != null) { + expect(expansion.length, greaterThan(3)); + } + } + }); + + test('tabComplete returns null when no results', () { + final ctrl = HierarchySearchController.forSignals( + service, + )..updateQuery('zzz_nonexistent'); + expect(ctrl.tabComplete('zzz_nonexistent'), isNull); + }); + + test('tabComplete returns null when prefix is not longer', () { + final ctrl = HierarchySearchController.forSignals( + service, + )..updateQuery('clk'); + // If there's a single result whose displayPath equals normalized + // query, tabComplete should return null or the path itself. + // With multiple results from different modules, the common prefix + // may not be longer. + final result = ctrl.tabComplete(ctrl.results.first.displayPath); + // Either null or the same length — shouldn't crash + expect(result, anyOf(isNull, isA())); + }); + }); + + // ─────────────── ModuleSearchResult getters ─────────────── + + group('ModuleSearchResult — additional getters', () { + test('isModule reflects non-primitive node', () { + final results = service.searchOccurrences('ch0'); + expect(results, isNotEmpty); + final r = results.first; + expect(r.isModule, isNotNull); + }); + + test('childCount reflects node.children.length', () { + final results = service.searchOccurrences('FilterBank'); + final fbResult = results.firstWhere( + (r) => r.path.length == 1, + orElse: () => results.first, + ); + expect(fbResult.childCount, greaterThan(0)); + }); + + test('toString includes module name', () { + final results = service.searchOccurrences('ch0'); + expect(results.first.toString(), contains('ch0')); + }); + }); + + // ─────────────── SignalSearchResult toString ─────────────── + + group('SignalSearchResult.toString', () { + test('toString includes signal name', () { + final results = service.searchSignals('clk'); + expect(results, isNotEmpty); + expect(results.first.toString(), contains('clk')); + }); + }); + + // ─────────────── BaseHierarchyAdapter edge case ─────────────── + // The real uninitialized-root StateError test lives in + // coverage_gaps_test.dart. Here we just verify fromTree works. + + group('BaseHierarchyAdapter — fromTree produces usable root', () { + test('fromTree immediately sets root', () { + final tree = HierarchyOccurrence(name: 'r'); + final svc = BaseHierarchyAdapter.fromTree(tree); + expect(svc.root.name, 'r'); + }); + }); + + // ─────────────── Multiple instantiation (dedup) ─────────────── + + group('Multiple instantiation — FilterChannel dedup', () { + test('ch0 and ch1 are separate node instances', () { + final ch0 = service.root.children.firstWhere((c) => c.name == 'ch0_1'); + final ch1 = service.root.children.firstWhere((c) => c.name == 'ch1_1'); + expect(identical(ch0, ch1), isFalse); + }); + + test('ch0 and ch1 have identical signal structure', () { + final ch0 = service.root.children.firstWhere((c) => c.name == 'ch0_1'); + final ch1 = service.root.children.firstWhere((c) => c.name == 'ch1_1'); + + expect(ch0.signals.length, ch1.signals.length); + + final ch0PortNames = ch0.signals.map((s) => s.name).toSet(); + final ch1PortNames = ch1.signals.map((s) => s.name).toSet(); + expect(ch0PortNames, ch1PortNames); + }); + + test('search finds signals in both channel instances', () { + // Both channels should have a clk port + final results = service.searchSignals('clk'); + final channelClks = results + .where( + (r) => r.signalId.contains('ch0_1') || r.signalId.contains('ch1_1'), + ) + .toList(); + // Should find clk in both ch0_1 and ch1_1 + expect( + channelClks.where((r) => r.signalId.contains('ch0_1')), + isNotEmpty, + ); + expect( + channelClks.where((r) => r.signalId.contains('ch1_1')), + isNotEmpty, + ); + }); + + test('addresses resolve independently for each instance', () { + final ch0Addr = OccurrenceAddress.tryFromPathname( + 'FilterBank/ch0_1/clk', + service.root, + ); + final ch1Addr = OccurrenceAddress.tryFromPathname( + 'FilterBank/ch1_1/clk', + service.root, + ); + + expect(ch0Addr, isNotNull); + expect(ch1Addr, isNotNull); + expect(ch0Addr, isNot(equals(ch1Addr))); + + final ch0Sig = service.signalByAddress(ch0Addr!); + final ch1Sig = service.signalByAddress(ch1Addr!); + expect(ch0Sig, isNotNull); + expect(ch1Sig, isNotNull); + expect(ch0Sig!.name, 'clk'); + expect(ch1Sig!.name, 'clk'); + }); + + test('both instances have internal (non-port) signals', () { + final ch0 = service.root.children.firstWhere((c) => c.name == 'ch0_1'); + final ch1 = service.root.children.firstWhere((c) => c.name == 'ch1_1'); + + final ch0Internal = ch0.signals.where((s) => !s.isPort).toList(); + final ch1Internal = ch1.signals.where((s) => !s.isPort).toList(); + + expect( + ch0Internal, + isNotEmpty, + reason: 'ch0_1 should have internal signals from netnames', + ); + expect( + ch1Internal, + isNotEmpty, + reason: 'ch1_1 should have internal signals from netnames', + ); + }); + + test('both instances share the same internal signal names', () { + final ch0 = service.root.children.firstWhere((c) => c.name == 'ch0_1'); + final ch1 = service.root.children.firstWhere((c) => c.name == 'ch1_1'); + + final ch0Names = + ch0.signals.where((s) => !s.isPort).map((s) => s.name).toSet(); + final ch1Names = + ch1.signals.where((s) => !s.isPort).map((s) => s.name).toSet(); + expect(ch0Names, ch1Names); + }); + + test('internal signals are addressable per-instance', () { + // validPipe exists as a netname in both FilterChannel definitions + final ch0Addr = OccurrenceAddress.tryFromPathname( + 'FilterBank/ch0_1/validPipe', + service.root, + ); + final ch1Addr = OccurrenceAddress.tryFromPathname( + 'FilterBank/ch1_1/validPipe', + service.root, + ); + + expect(ch0Addr, isNotNull, reason: 'ch0_1/validPipe should resolve'); + expect(ch1Addr, isNotNull, reason: 'ch1_1/validPipe should resolve'); + expect(ch0Addr, isNot(equals(ch1Addr))); + + final ch0Sig = service.signalByAddress(ch0Addr!); + final ch1Sig = service.signalByAddress(ch1Addr!); + expect(ch0Sig, isNotNull); + expect(ch1Sig, isNotNull); + expect(ch0Sig!.name, 'validPipe'); + expect(ch1Sig!.name, 'validPipe'); + expect(ch0Sig.isPort, isFalse); + }); + + test('search finds internal signals in both instances', () { + final results = service.searchSignals('validPipe'); + final inCh0 = results.where((r) => r.signalId.contains('ch0_1')); + final inCh1 = results.where((r) => r.signalId.contains('ch1_1')); + expect(inCh0, isNotEmpty, reason: 'validPipe should be found in ch0_1'); + expect(inCh1, isNotEmpty, reason: 'validPipe should be found in ch1_1'); + }); + + test('depthFirstSignals includes internal signals from both instances', () { + final all = service.root.depthFirstSignals(); + final vpSigs = all.where((s) => s.name == 'validPipe').toList(); + expect( + vpSigs.length, + greaterThanOrEqualTo(2), + reason: 'validPipe should appear in at least ch0 and ch1', + ); + }); + }); + + // ─────────────── InOut (bidirectional) port tests ─────────────── + + group('InOut port — dataBus', () { + test('root has dataBus as inout port', () { + final dataBus = service.root.signals + .where((s) => s.isPort) + .where((p) => p.name == 'dataBus') + .firstOrNull; + expect(dataBus, isNotNull, reason: 'FilterBank should have dataBus'); + expect(dataBus!.direction, 'inout'); + expect(dataBus.isInout, isTrue); + expect(dataBus.isInput, isFalse); + expect(dataBus.isOutput, isFalse); + }); + + test('inputs getter excludes inout ports', () { + final inputs = service.root.inputs; + final inoutInInputs = inputs.where((s) => s.direction == 'inout'); + expect( + inoutInInputs, + isEmpty, + reason: 'inputs should not include inout ports', + ); + }); + + test('outputs getter excludes inout ports', () { + final outputs = service.root.outputs; + final inoutInOutputs = outputs.where((s) => s.direction == 'inout'); + expect( + inoutInOutputs, + isEmpty, + reason: 'outputs should not include inout ports', + ); + }); + + test('ports getter includes inout ports', () { + final allPorts = service.root.ports; + final inouts = allPorts.where((p) => p.direction == 'inout').toList(); + expect(inouts, isNotEmpty, reason: 'ports should include inout ports'); + expect(inouts.first.name, 'dataBus'); + }); + + test('dataBus is addressable and resolvable', () { + final addr = OccurrenceAddress.tryFromPathname( + 'FilterBank/dataBus', + service.root, + ); + expect(addr, isNotNull, reason: 'dataBus should be addressable'); + + final sig = service.signalByAddress(addr!); + expect(sig, isNotNull); + expect(sig!.name, 'dataBus'); + expect(sig.isInout, isTrue); + }); + + test('search finds dataBus inout port', () { + final results = service.searchSignals('dataBus'); + expect(results, isNotEmpty); + final dataBusResults = results.where( + (r) => r.signalId.contains('dataBus'), + ); + expect(dataBusResults, isNotEmpty); + }); + + test('SharedDataBus child also has dataBus inout', () { + final sharedBus = service.root.children + .where((c) => c.name == 'sharedBus_1') + .firstOrNull; + expect( + sharedBus, + isNotNull, + reason: 'sharedBus_1 cell should be present', + ); + final childDataBus = sharedBus!.signals + .where((s) => s.isPort) + .where((p) => p.name == 'dataBus') + .firstOrNull; + expect( + childDataBus, + isNotNull, + reason: 'SharedDataBus should have dataBus inout', + ); + expect(childDataBus!.isInout, isTrue); + }); + + test('depthFirstSignals includes inout ports', () { + final all = service.root.depthFirstSignals(); + final inouts = all.where((s) => s.isInout); + expect( + inouts, + isNotEmpty, + reason: 'depthFirstSignals should include inout ports', + ); + }); + + test('addressToPathname round-trips for inout signal', () { + final addr = OccurrenceAddress.tryFromPathname( + 'FilterBank/dataBus', + service.root, + ); + expect(addr, isNotNull); + final pathname = service.addressToPathname(addr!, asSignal: true); + expect(pathname, 'FilterBank/dataBus'); + }); + }); +} diff --git a/packages/rohd_hierarchy/test/fixtures/filter_bank.json b/packages/rohd_hierarchy/test/fixtures/filter_bank.json new file mode 100644 index 000000000..494318612 --- /dev/null +++ b/packages/rohd_hierarchy/test/fixtures/filter_bank.json @@ -0,0 +1,1183 @@ +{ + "modules": { + "CoeffBank_T3_W16": { + "attributes": { + "src": "generated" + }, + "ports": { + "tapIndex": { + "direction": "input", + "bits": [ + 2, + 3 + ] + }, + "coeffArray": { + "direction": "input", + "bits": [ + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51 + ] + }, + "coeffOut": { + "direction": "output", + "bits": [ + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67 + ] + } + }, + "netnames": { + "tapMatch0": { + "bits": [ + 68 + ], + "attributes": {} + }, + "tapMatch1": { + "bits": [ + 69 + ], + "attributes": {} + }, + "const_0_2_h0": { + "bits": [ + 103, + 104 + ], + "attributes": { + "computed": 1 + } + } + }, + "cells": { + "mux_3": { + "type": "$mux", + "port_directions": { + "S": "input", + "A": "input", + "B": "input", + "Y": "output" + } + }, + "equals_3": { + "type": "$eq", + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + } + }, + "mux_0_1": { + "type": "$mux", + "port_directions": { + "S": "input", + "A": "input", + "B": "input", + "Y": "output" + } + }, + "equals_0_1": { + "type": "$eq", + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + } + }, + "mux_1_1": { + "type": "$mux", + "port_directions": { + "S": "input", + "A": "input", + "B": "input", + "Y": "output" + } + } + } + }, + "MacUnit_W16": { + "attributes": { + "src": "generated" + }, + "ports": { + "sampleIn": { + "direction": "input", + "bits": [ + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17 + ] + }, + "coeffIn": { + "direction": "input", + "bits": [ + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33 + ] + }, + "accumIn": { + "direction": "input", + "bits": [ + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49 + ] + }, + "clk": { + "direction": "input", + "bits": [ + 50 + ] + }, + "reset": { + "direction": "input", + "bits": [ + 51 + ] + }, + "enable": { + "direction": "input", + "bits": [ + 52 + ] + }, + "result": { + "direction": "output", + "bits": [ + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68 + ] + } + }, + "netnames": { + "sampleIn_stage2_i": { + "bits": [ + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84 + ], + "attributes": {} + }, + "sampleIn_stage0_o": { + "bits": [ + 85, + 86, + 87, + 88, + 89, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100 + ], + "attributes": {} + } + }, + "cells": { + "comb_stage2_1": { + "type": "Combinational", + "port_directions": { + "_in0_sampleIn_stage2_i": "input", + "_in2_coeffIn_stage2_i": "input", + "_in4_accumIn_stage2_i": "input", + "_out1_sampleIn_stage2": "output", + "_out3_coeffIn_stage2": "output", + "_out5_accumIn_stage2": "output" + } + }, + "ff_sampleIn_1": { + "type": "Sequential", + "port_directions": { + "_in0_reset": "input", + "_in4_sampleIn_stage0_o": "input", + "_in5_sampleIn_stage1_o": "input", + "_trigger0_clk": "input", + "_out6_sampleIn_stage1_i": "output", + "_out7_sampleIn_stage2_i": "output" + } + }, + "comb_stage0_1": { + "type": "Combinational", + "port_directions": { + "_in0_sampleIn_stage0_i": "input", + "_in2_coeffIn_stage0_i": "input", + "_in4_accumIn_stage0_i": "input", + "_in6_product": "input", + "_out1_sampleIn_stage0": "output", + "_out3_coeffIn_stage0": "output", + "_out5_accumIn_stage0": "output", + "_out7_sampleIn_stage0": "output" + } + }, + "multiply_1": { + "type": "$mul", + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + } + }, + "ff_coeffIn_1": { + "type": "Sequential", + "port_directions": { + "_in0_reset": "input", + "_in4_coeffIn_stage0_o": "input", + "_in5_coeffIn_stage1_o": "input", + "_trigger0_clk": "input", + "_out6_coeffIn_stage1_i": "output", + "_out7_coeffIn_stage2_i": "output" + } + } + } + }, + "FilterChannel_T3_W16": { + "attributes": { + "src": "generated" + }, + "ports": { + "sampleIn": { + "direction": "input", + "bits": [ + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17 + ] + }, + "validIn": { + "direction": "input", + "bits": [ + 18 + ] + }, + "clk": { + "direction": "input", + "bits": [ + 19 + ] + }, + "reset": { + "direction": "input", + "bits": [ + 20 + ] + }, + "enable": { + "direction": "input", + "bits": [ + 21 + ] + }, + "dataOut": { + "direction": "output", + "bits": [ + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37 + ] + }, + "validOut": { + "direction": "output", + "bits": [ + 38 + ] + } + }, + "netnames": { + "validPipe": { + "bits": [ + 39 + ], + "attributes": {} + }, + "outputReady": { + "bits": [ + 40 + ], + "attributes": {} + }, + "const_0_2_h0": { + "bits": [ + 420, + 421 + ], + "attributes": { + "computed": 1 + } + } + }, + "cells": { + "combinational_2": { + "type": "Combinational", + "port_directions": { + "_in0_validPipe": "input", + "_in1_outputReg": "input", + "_out4_dataOut": "output", + "_out5_validOut": "output" + } + }, + "sequential_3": { + "type": "Sequential", + "port_directions": { + "_in0_reset": "input", + "_in3_enable": "input", + "_in4_outputReady": "input", + "_trigger0_clk": "input", + "_out5_validPipe": "output" + } + }, + "and__3": { + "type": "$and", + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + } + }, + "sequential_0_1": { + "type": "Sequential", + "port_directions": { + "_in0_reset": "input", + "_in5_lastTap": "input", + "_in6_lastTapD1": "input", + "_in7_lastTapD2": "input", + "_in8_accumReg": "input", + "_trigger0_clk": "input", + "_out9_lastTapD1": "output", + "_out10_lastTapD2": "output", + "_out11_outputReg": "output" + } + }, + "sequential_1_1": { + "type": "Sequential", + "port_directions": { + "_in0_reset": "input", + "_in3_enable": "input", + "_in4_lastTap": "input", + "_in7__tapCounter_add_const_1": "input", + "_trigger0_clk": "input", + "_out10_tapCounter": "output" + } + } + } + }, + "FilterController": { + "attributes": { + "src": "generated" + }, + "ports": { + "clk": { + "direction": "input", + "bits": [ + 2 + ] + }, + "reset": { + "direction": "input", + "bits": [ + 3 + ] + }, + "start": { + "direction": "input", + "bits": [ + 4 + ] + }, + "inputValid": { + "direction": "input", + "bits": [ + 5 + ] + }, + "inputDone": { + "direction": "input", + "bits": [ + 6 + ] + }, + "filterEnable": { + "direction": "output", + "bits": [ + 7 + ] + }, + "loadingPhase": { + "direction": "output", + "bits": [ + 8 + ] + }, + "doneFlag": { + "direction": "output", + "bits": [ + 9 + ] + }, + "state": { + "direction": "output", + "bits": [ + 10, + 11, + 12 + ] + } + }, + "netnames": { + "currentState": { + "bits": [ + 13, + 14, + 15 + ], + "attributes": {} + }, + "isDraining": { + "bits": [ + 16 + ], + "attributes": {} + }, + "const_0_3_h3": { + "bits": [ + 94, + 95, + 96 + ], + "attributes": { + "computed": 1 + } + } + }, + "cells": { + "combinational_1": { + "type": "Combinational", + "port_directions": { + "_in0_currentState": "input", + "_in1_FilterState_idle": "input", + "_in6_start": "input", + "_in9_FilterState_loading": "input", + "_in14_inputValid": "input", + "_in17_FilterState_running": "input", + "_in22_inputDone": "input", + "_in25_FilterState_draining": "input", + "_in30_drainDone": "input", + "_in33_FilterState_done": "input", + "_out42_filterEnable": "output", + "_out43_loadingPhase": "output", + "_out44_doneFlag": "output", + "_out45_nextState": "output" + } + }, + "swizzle_1": { + "type": "$buf", + "port_directions": { + "A": "input", + "Y": "output" + } + }, + "equals_2": { + "type": "$eq", + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + } + }, + "sequential_2": { + "type": "Sequential", + "port_directions": { + "_in0_reset": "input", + "_in3_isDraining": "input", + "_in4__drainCount_add_const_1": "input", + "_trigger0_clk": "input", + "_out7_drainCount": "output" + } + }, + "equals_0_1": { + "type": "$eq", + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + } + } + } + }, + "FilterChannel_T3_W16_0": { + "attributes": { + "src": "generated" + }, + "ports": { + "sampleIn": { + "direction": "input", + "bits": [ + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17 + ] + }, + "validIn": { + "direction": "input", + "bits": [ + 18 + ] + }, + "clk": { + "direction": "input", + "bits": [ + 19 + ] + }, + "reset": { + "direction": "input", + "bits": [ + 20 + ] + }, + "enable": { + "direction": "input", + "bits": [ + 21 + ] + }, + "dataOut": { + "direction": "output", + "bits": [ + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37 + ] + }, + "validOut": { + "direction": "output", + "bits": [ + 38 + ] + } + }, + "netnames": { + "validPipe": { + "bits": [ + 39 + ], + "attributes": {} + }, + "outputReady": { + "bits": [ + 40 + ], + "attributes": {} + }, + "const_0_2_h0": { + "bits": [ + 420, + 421 + ], + "attributes": { + "computed": 1 + } + } + }, + "cells": { + "combinational_2": { + "type": "Combinational", + "port_directions": { + "_in0_validPipe": "input", + "_in1_outputReg": "input", + "_out4_dataOut": "output", + "_out5_validOut": "output" + } + }, + "sequential_3": { + "type": "Sequential", + "port_directions": { + "_in0_reset": "input", + "_in3_enable": "input", + "_in4_outputReady": "input", + "_trigger0_clk": "input", + "_out5_validPipe": "output" + } + }, + "and__3": { + "type": "$and", + "port_directions": { + "A": "input", + "B": "input", + "Y": "output" + } + }, + "sequential_0_1": { + "type": "Sequential", + "port_directions": { + "_in0_reset": "input", + "_in5_lastTap": "input", + "_in6_lastTapD1": "input", + "_in7_lastTapD2": "input", + "_in8_accumReg": "input", + "_trigger0_clk": "input", + "_out9_lastTapD1": "output", + "_out10_lastTapD2": "output", + "_out11_outputReg": "output" + } + }, + "sequential_1_1": { + "type": "Sequential", + "port_directions": { + "_in0_reset": "input", + "_in3_enable": "input", + "_in4_lastTap": "input", + "_in7__tapCounter_add_const_1": "input", + "_trigger0_clk": "input", + "_out10_tapCounter": "output" + } + } + } + }, + "SharedDataBus": { + "attributes": { + "src": "generated" + }, + "ports": { + "writeEnable": { + "direction": "input", + "bits": [ + 502 + ] + }, + "clk": { + "direction": "input", + "bits": [ + 503 + ] + }, + "reset": { + "direction": "input", + "bits": [ + 504 + ] + }, + "storedValue": { + "direction": "output", + "bits": [ + 505, + 506, + 507, + 508, + 509, + 510, + 511, + 512, + 513, + 514, + 515, + 516, + 517, + 518, + 519, + 520 + ] + }, + "dataBus": { + "direction": "inout", + "bits": [ + 521, + 522, + 523, + 524, + 525, + 526, + 527, + 528, + 529, + 530, + 531, + 532, + 533, + 534, + 535, + 536 + ] + } + }, + "netnames": { + "latch": { + "bits": [ + 537, + 538, + 539, + 540, + 541, + 542, + 543, + 544, + 545, + 546, + 547, + 548, + 549, + 550, + 551, + 552 + ], + "attributes": {} + } + }, + "cells": {} + }, + "FilterBank": { + "attributes": { + "src": "generated", + "top": 1 + }, + "ports": { + "clk": { + "direction": "input", + "bits": [ + 2 + ] + }, + "reset": { + "direction": "input", + "bits": [ + 3 + ] + }, + "start": { + "direction": "input", + "bits": [ + 4 + ] + }, + "samplesIn": { + "direction": "input", + "bits": [ + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36 + ] + }, + "validIn": { + "direction": "input", + "bits": [ + 37 + ] + }, + "inputDone": { + "direction": "input", + "bits": [ + 38 + ] + }, + "channelOut": { + "direction": "output", + "bits": [ + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70 + ] + }, + "validOut": { + "direction": "output", + "bits": [ + 71 + ] + }, + "done": { + "direction": "output", + "bits": [ + 72 + ] + }, + "state": { + "direction": "output", + "bits": [ + 73, + 74, + 75 + ] + }, + "dataBus": { + "direction": "inout", + "bits": [ + 600, + 601, + 602, + 603, + 604, + 605, + 606, + 607, + 608, + 609, + 610, + 611, + 612, + 613, + 614, + 615 + ] + } + }, + "netnames": { + "channelOut_0_": { + "bits": [ + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54 + ], + "attributes": {} + }, + "sample0_data": { + "bits": [ + 147, + 148, + 149, + 150, + 151, + 152, + 153, + 154, + 155, + 156, + 157, + 158, + 159, + 160, + 161, + 162 + ], + "attributes": {} + }, + "controller_1_loadingPhase": { + "bits": [ + 146 + ], + "hide_name": 1, + "attributes": {} + } + }, + "cells": { + "ch0_1": { + "type": "FilterChannel_T3_W16_0", + "port_directions": { + "sampleIn": "input", + "validIn": "input", + "clk": "input", + "reset": "input", + "enable": "input", + "dataOut": "output", + "validOut": "output" + } + }, + "controller_1": { + "type": "FilterController", + "port_directions": { + "clk": "input", + "reset": "input", + "start": "input", + "inputValid": "input", + "inputDone": "input", + "filterEnable": "output", + "loadingPhase": "output", + "doneFlag": "output", + "state": "output" + } + }, + "ch1_1": { + "type": "FilterChannel_T3_W16", + "port_directions": { + "sampleIn": "input", + "validIn": "input", + "clk": "input", + "reset": "input", + "enable": "input", + "dataOut": "output", + "validOut": "output" + } + }, + "array_slice_3": { + "type": "$slice", + "port_directions": { + "A": "input", + "Y": "output" + } + }, + "array_slice_4": { + "type": "$slice", + "port_directions": { + "A": "input", + "Y": "output" + } + }, + "sharedBus_1": { + "type": "SharedDataBus", + "port_directions": { + "writeEnable": "input", + "clk": "input", + "reset": "input", + "storedValue": "output", + "dataBus": "inout" + } + } + } + } + } +} \ No newline at end of file diff --git a/packages/rohd_hierarchy/test/hierarchy_path_vs_signal_id_test.dart b/packages/rohd_hierarchy/test/hierarchy_path_vs_signal_id_test.dart new file mode 100644 index 000000000..9c574df79 --- /dev/null +++ b/packages/rohd_hierarchy/test/hierarchy_path_vs_signal_id_test.dart @@ -0,0 +1,194 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// hierarchy_path_vs_signal_id_test.dart +// Verifies that signal.path(separator:) and search result signalId +// work correctly with different separators. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +void main() { + group('signal.path() with separator', () { + late BaseHierarchyAdapter adapter; + + setUp(() { + final root = HierarchyOccurrence( + name: 'abcd', + signals: [ + SignalOccurrence(name: 'clk', width: 1, direction: 'input'), + SignalOccurrence(name: 'arvalid_s', width: 1, direction: 'input'), + ], + children: [ + HierarchyOccurrence( + name: 'lab', + signals: [ + SignalOccurrence(name: 'clk', width: 1, direction: 'input'), + SignalOccurrence(name: 'data', width: 8, direction: 'output'), + ], + ), + ], + )..buildAddresses(); + adapter = BaseHierarchyAdapter.fromTree(root); + }); + + SignalOccurrence? resolve(String path) { + final addr = OccurrenceAddress.tryFromPathname(path, adapter.root); + return addr != null ? adapter.signalByAddress(addr) : null; + } + + test('resolves dot-separated IDs', () { + final s = resolve('abcd.clk'); + expect(s, isNotNull); + expect(s!.path(separator: '.'), 'abcd.clk'); + }); + + test('resolves slash-separated IDs', () { + final s = resolve('abcd/clk'); + expect(s, isNotNull); + expect(s!.path(), 'abcd/clk'); + }); + + test('resolves with exact case', () { + final s = resolve('abcd.clk'); + expect(s, isNotNull); + expect(s!.path(), 'abcd/clk'); + }); + + test('resolves nested dot-separated IDs', () { + final s = resolve('abcd.lab.data'); + expect(s, isNotNull); + expect(s!.path(separator: '.'), 'abcd.lab.data'); + }); + + test('resolves nested slash-separated IDs', () { + final s = resolve('abcd/lab/data'); + expect(s, isNotNull); + expect(s!.path(), 'abcd/lab/data'); + }); + + test('searchSignals returns result with resolved signal', () { + final results = adapter.searchSignals('clk'); + expect(results, isNotEmpty); + for (final r in results) { + expect(r.signal, isNotNull, + reason: 'SignalOccurrence should be resolved for "${r.signalId}"'); + } + final clkResult = + results.firstWhere((r) => r.path.last == 'clk' && r.path.length == 2); + expect(clkResult.signal!.path(), 'abcd/clk'); + expect(clkResult.signal!.path(separator: '.'), 'abcd.clk'); + }); + + test('searchSignals signalId is walker-built (slash) path', () { + final results = adapter.searchSignals('clk'); + expect(results, isNotEmpty); + final clkResult = + results.firstWhere((r) => r.path.last == 'clk' && r.path.length == 2); + expect(clkResult.signalId, 'abcd/clk'); + }); + + test('signal.path() matches signalId with default separator', () { + final results = adapter.searchSignals('clk'); + final result = results.first; + expect(result.signal, isNotNull); + expect(result.signal!.path(), result.signalId); + }); + + test('searchSignalsRegex returns result with signal resolved', () { + final results = adapter.searchSignalsRegex('**/clk'); + expect(results.length, greaterThanOrEqualTo(2)); + for (final r in results) { + expect(r.signal, isNotNull, + reason: 'SignalOccurrence should be resolved for "${r.signalId}"'); + expect(r.signal!.path(), contains('/')); + } + }); + }); + + group('ROHD slash-separated hierarchy', () { + late BaseHierarchyAdapter adapter; + + setUp(() { + final root = HierarchyOccurrence( + name: 'Top', + signals: [SignalOccurrence(name: 'clk', width: 1)], + children: [ + HierarchyOccurrence( + name: 'cpu', + signals: [SignalOccurrence(name: 'data_out', width: 8)], + ), + ], + )..buildAddresses(); + adapter = BaseHierarchyAdapter.fromTree(root); + }); + + test('ROHD signals: path() matches signalId (both slash)', () { + final results = adapter.searchSignals('clk'); + expect(results, isNotEmpty); + final r = results.first; + expect(r.signal!.path(), r.signalId); + }); + }); + + group('SignalOccurrence as port', () { + test('creates a port signal with defaults', () { + final p = SignalOccurrence(name: 'clk', width: 1, direction: 'input'); + expect(p.name, 'clk'); + expect(p.direction, 'input'); + expect(p.width, 1); + expect(p.isPort, isTrue); + expect(p.isInput, isTrue); + }); + + test('creates a port signal with explicit overrides', () { + final p = SignalOccurrence( + name: 'data', + direction: 'output', + width: 32, + isComputed: true, + ); + HierarchyOccurrence(name: 'Top', signals: [p]).buildAddresses(); + expect(p.name, 'data'); + expect(p.width, 32); + expect(p.direction, 'output'); + expect(p.path(), 'Top/data'); + expect(p.parent!.path(), 'Top'); + expect(p.isComputed, isTrue); + expect(p.isOutput, isTrue); + }); + }); + + group('SignalOccurrence.value', () { + test('value is null by default', () { + final s = SignalOccurrence(name: 'a', width: 1); + expect(s.value, isNull); + }); + + test('value stores the provided runtime value', () { + final s = SignalOccurrence(name: 'a', width: 8, value: 'ff'); + expect(s.value, 'ff'); + }); + }); + + group('SignalOccurrence.parent', () { + test('parent is null before buildAddresses', () { + final s = SignalOccurrence(name: 'a', width: 1); + expect(s.parent, isNull); + }); + + test('parent is set after buildAddresses', () { + final s = SignalOccurrence(name: 'a', width: 1); + HierarchyOccurrence( + name: 'Top', + children: [ + HierarchyOccurrence(name: 'sub', signals: [s]) + ], + ).buildAddresses(); + expect(s.parent!.path(), 'Top/sub'); + }); + }); +} diff --git a/packages/rohd_hierarchy/test/hierarchy_query_test.dart b/packages/rohd_hierarchy/test/hierarchy_query_test.dart new file mode 100644 index 000000000..7425aaa43 --- /dev/null +++ b/packages/rohd_hierarchy/test/hierarchy_query_test.dart @@ -0,0 +1,655 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// hierarchy_query_test.dart +// Tests for PrefixQuery and RegexQuery matching logic. +// +// 2026 May +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +/// Build a test hierarchy: +/// +/// ```text +/// SoC +/// ├─ signals: [clk, reset, irq0, irq1] +/// ├─ cpu0 +/// │ ├─ signals: [clk, reset, pc] +/// │ ├─ alu +/// │ │ └─ signals: [a, b, result, carry_out, overflow] +/// │ ├─ regfile +/// │ │ └─ signals: [clk, reset, d0, d1, d2, d15, wr_en] +/// │ └─ decoder +/// │ └─ signals: [opcode, enable, mode] +/// ├─ cpu1 +/// │ ├─ signals: [clk, reset, pc] +/// │ ├─ alu +/// │ │ └─ signals: [a, b, result, carry_out, overflow] +/// │ └─ regfile +/// │ └─ signals: [clk, reset, d0, d1, d2, d15, wr_en] +/// ├─ mem_ctrl +/// │ ├─ signals: [clk, reset, addr, data_in, data_out, valid] +/// │ ├─ ch0 +/// │ │ └─ signals: [clk, addr, data, hit, miss] +/// │ ├─ ch1 +/// │ │ └─ signals: [clk, addr, data, hit, miss] +/// │ └─ ch2 +/// │ └─ signals: [clk, addr, data, hit, miss] +/// └─ io_mux +/// ├─ signals: [clk, sel, data_muxed, valid_muxed] +/// ├─ uart0 +/// │ └─ signals: [clk, tx, rx, baud_sel] +/// └─ uart1 +/// └─ signals: [clk, tx, rx, baud_sel] +/// ``` +HierarchyService buildTestHierarchy() { + HierarchyOccurrence mkAlu() => HierarchyOccurrence( + name: 'alu', + definition: 'ALU', + signals: [ + SignalOccurrence(name: 'a', width: 8), + SignalOccurrence(name: 'b', width: 8), + SignalOccurrence(name: 'result', width: 8), + SignalOccurrence(name: 'carry_out', width: 1), + SignalOccurrence(name: 'overflow', width: 1), + ], + ); + + HierarchyOccurrence mkRegfile() => HierarchyOccurrence( + name: 'regfile', + definition: 'RegFile', + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'reset', width: 1), + SignalOccurrence(name: 'd0', width: 8), + SignalOccurrence(name: 'd1', width: 8), + SignalOccurrence(name: 'd2', width: 8), + SignalOccurrence(name: 'd15', width: 8), + SignalOccurrence(name: 'wr_en', width: 1), + ], + ); + + final decoder = HierarchyOccurrence( + name: 'decoder', + definition: 'Decoder', + signals: [ + SignalOccurrence(name: 'opcode', width: 4), + SignalOccurrence(name: 'enable', width: 1), + SignalOccurrence(name: 'mode', width: 2), + ], + ); + + final cpu0 = HierarchyOccurrence( + name: 'cpu0', + definition: 'CPU', + children: [mkAlu(), mkRegfile(), decoder], + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'reset', width: 1), + SignalOccurrence(name: 'pc', width: 32), + ], + ); + + final cpu1 = HierarchyOccurrence( + name: 'cpu1', + definition: 'CPU', + children: [mkAlu(), mkRegfile()], + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'reset', width: 1), + SignalOccurrence(name: 'pc', width: 32), + ], + ); + + HierarchyOccurrence mkCacheChannel(String name) => HierarchyOccurrence( + name: name, + definition: 'CacheChannel', + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'addr', width: 16), + SignalOccurrence(name: 'data', width: 32), + SignalOccurrence(name: 'hit', width: 1), + SignalOccurrence(name: 'miss', width: 1), + ], + ); + + final memCtrl = HierarchyOccurrence( + name: 'mem_ctrl', + definition: 'MemController', + children: [ + mkCacheChannel('ch0'), + mkCacheChannel('ch1'), + mkCacheChannel('ch2') + ], + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'reset', width: 1), + SignalOccurrence(name: 'addr', width: 16), + SignalOccurrence(name: 'data_in', width: 32), + SignalOccurrence(name: 'data_out', width: 32), + SignalOccurrence(name: 'valid', width: 1), + ], + ); + + HierarchyOccurrence mkUart(String name) => HierarchyOccurrence( + name: name, + definition: 'UART', + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'tx', width: 1), + SignalOccurrence(name: 'rx', width: 1), + SignalOccurrence(name: 'baud_sel', width: 3), + ], + ); + + final ioMux = HierarchyOccurrence( + name: 'io_mux', + definition: 'IOMux', + children: [mkUart('uart0'), mkUart('uart1')], + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'sel', width: 2), + SignalOccurrence(name: 'data_muxed', width: 8), + SignalOccurrence(name: 'valid_muxed', width: 1), + ], + ); + + final root = HierarchyOccurrence( + name: 'SoC', + definition: 'SoC', + children: [cpu0, cpu1, memCtrl, ioMux], + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'reset', width: 1), + SignalOccurrence(name: 'irq0', width: 1), + SignalOccurrence(name: 'irq1', width: 1), + ], + ); + + return BaseHierarchyAdapter.fromTree(root); +} + +void main() { + late HierarchyService svc; + + setUpAll(() { + svc = buildTestHierarchy(); + }); + + // ═══════════════════════════════════════════════════════════════ + // PrefixQuery + // ═══════════════════════════════════════════════════════════════ + + group('PrefixQuery', () { + group('matchOccurrence', () { + test('matches occurrence name containing segment', () { + final q = PrefixQuery('cpu'); + // 'cpu0' contains 'cpu' + expect(q.matchOccurrence('cpu0', 0), equals({1})); + }); + + test('returns empty set when no match', () { + final q = PrefixQuery('mem'); + expect(q.matchOccurrence('cpu0', 0), isEmpty); + }); + + test('past end of segments returns current state', () { + final q = PrefixQuery('cpu'); + // stateIndex == segmentCount → already consumed + expect(q.matchOccurrence('anything', 1), equals({1})); + }); + + test('multi-segment: advances one segment at a time', () { + final q = PrefixQuery('cpu/alu'); + expect(q.matchOccurrence('cpu0', 0), equals({1})); + expect(q.matchOccurrence('alu', 1), equals({2})); + // 'regfile' doesn't match 'alu' + expect(q.matchOccurrence('regfile', 1), isEmpty); + }); + + test('dot separator treated as slash', () { + final q = PrefixQuery('cpu.alu'); + expect(q.segmentCount, equals(2)); + expect(q.matchOccurrence('cpu0', 0), equals({1})); + }); + }); + + group('matchSignal', () { + test('matches signal name with startsWith', () { + final q = PrefixQuery('cpu/clk'); + // At state 1 (after matching 'cpu'), 'clk' starts with 'clk' + expect(q.matchSignal('clk', 1), isTrue); + expect(q.matchSignal('clk_gated', 1), isTrue); + }); + + test('does not match signal for non-last segment', () { + final q = PrefixQuery('cpu/alu/res'); + // At state 1, there are still 2 segments left → only last matches + expect(q.matchSignal('result', 1), isFalse); + // At state 2, this is the last segment + expect(q.matchSignal('result', 2), isTrue); + }); + + test('past end matches any signal', () { + final q = PrefixQuery('cpu'); + expect(q.matchSignal('anything', 1), isTrue); + }); + }); + + group('isComplete', () { + test('complete when stateIndex >= segmentCount', () { + final q = PrefixQuery('cpu/alu'); + expect(q.isComplete(0), isFalse); + expect(q.isComplete(1), isFalse); + expect(q.isComplete(2), isTrue); + expect(q.isComplete(3), isTrue); + }); + }); + + group('isEmpty', () { + test('empty for blank query', () { + expect(PrefixQuery('').isEmpty, isTrue); + expect(PrefixQuery(' ').isEmpty, isTrue); + }); + + test('not empty for real query', () { + expect(PrefixQuery('clk').isEmpty, isFalse); + }); + }); + + group('target property', () { + test('defaults to signals', () { + expect(PrefixQuery('x').target, equals(SearchTarget.signals)); + }); + + test('can be set to occurrences', () { + final q = PrefixQuery('x', target: SearchTarget.occurrences); + expect(q.target, equals(SearchTarget.occurrences)); + }); + }); + }); + + // ═══════════════════════════════════════════════════════════════ + // RegexQuery + // ═══════════════════════════════════════════════════════════════ + + group('RegexQuery', () { + group('exact name matching', () { + test('matches exact occurrence name', () { + final q = RegexQuery('SoC/cpu0/alu'); + expect(q.matchOccurrence('SoC', 0), equals({1})); + expect(q.matchOccurrence('cpu0', 1), equals({2})); + expect(q.matchOccurrence('alu', 2), equals({3})); + }); + + test('does not match wrong name', () { + final q = RegexQuery('SoC/cpu0'); + expect(q.matchOccurrence('cpu1', 1), isEmpty); + }); + + test('case sensitive', () { + final q = RegexQuery('SoC/cpu0'); + expect(q.matchOccurrence('SoC', 0), equals({1})); + expect(q.matchOccurrence('cpu0', 1), equals({2})); + }); + }); + + group('glob wildcard *', () { + test('star matches any characters', () { + final q = RegexQuery('SoC/cpu*'); + expect(q.matchOccurrence('cpu0', 1), equals({2})); + expect(q.matchOccurrence('cpu1', 1), equals({2})); + expect(q.matchOccurrence('mem_ctrl', 1), isEmpty); + }); + + test('star at start', () { + final q = RegexQuery('SoC/*_ctrl'); + expect(q.matchOccurrence('mem_ctrl', 1), equals({2})); + expect(q.matchOccurrence('cpu0', 1), isEmpty); + }); + + test('star in middle', () { + final q = RegexQuery('SoC/io_*'); + expect(q.matchOccurrence('io_mux', 1), equals({2})); + expect(q.matchOccurrence('io_ctrl', 1), equals({2})); + expect(q.matchOccurrence('cpu0', 1), isEmpty); + }); + + test('standalone star matches anything', () { + final q = RegexQuery('SoC/*'); + expect(q.matchOccurrence('cpu0', 1), equals({2})); + expect(q.matchOccurrence('mem_ctrl', 1), equals({2})); + expect(q.matchOccurrence('io_mux', 1), equals({2})); + }); + }); + + group('glob wildcard ?', () { + test('question mark matches one character', () { + final q = RegexQuery('SoC/cpu?'); + expect(q.matchOccurrence('cpu0', 1), equals({2})); + expect(q.matchOccurrence('cpu1', 1), equals({2})); + // 'cpuXY' is two chars after 'cpu' → no match + expect(q.matchOccurrence('cpuXY', 1), isEmpty); + }); + }); + + group('glob-star ** (cross hierarchy boundaries)', () { + test('** matches zero levels', () { + final q = RegexQuery('SoC/**/alu'); + // ** at index 1 can match zero levels → try index 2 ('alu') + // directly against children of SoC + final states = q.matchOccurrence('alu', 1); + // Should include state 1 (stay at **) and possibly skip to 2 + expect(states, contains(1)); + }); + + test('** matches one or more levels', () { + final q = RegexQuery('SoC/**/clk'); + // ** stays at ** when consuming a node + expect(q.matchOccurrence('cpu0', 1), contains(1)); + expect(q.matchOccurrence('alu', 1), contains(1)); + }); + + test('** followed by exact segment', () { + final q = RegexQuery('SoC/**/alu'); + // At state 1 (**), 'alu' should match both staying and advancing + final states = q.matchOccurrence('alu', 1); + expect(states, contains(1)); // stay at ** + expect(states, contains(3)); // skip ** + match 'alu' → index 3 + }); + + test('isComplete with trailing **', () { + final q = RegexQuery('SoC/**'); + expect(q.isComplete(1), isTrue); // ** can match zero + expect(q.isComplete(2), isTrue); // past end + }); + }); + + group('character classes [...]', () { + test('matches character range', () { + final q = RegexQuery('SoC/**/d[0-9]+'); + // Signal matching + expect(q.matchSignal('d0', 2), isTrue); + expect(q.matchSignal('d1', 2), isTrue); + expect(q.matchSignal('d15', 2), isTrue); + expect(q.matchSignal('clk', 2), isFalse); + }); + + test('fixed character set', () { + final q = RegexQuery('SoC/mem_ctrl/ch[012]'); + expect(q.matchOccurrence('ch0', 2), equals({3})); + expect(q.matchOccurrence('ch1', 2), equals({3})); + expect(q.matchOccurrence('ch2', 2), equals({3})); + expect(q.matchOccurrence('ch3', 2), isEmpty); + }); + }); + + group('alternation (...|...)', () { + test('matches either alternative', () { + final q = RegexQuery('SoC/**/(clk|reset)'); + expect(q.matchSignal('clk', 2), isTrue); + expect(q.matchSignal('reset', 2), isTrue); + expect(q.matchSignal('data', 2), isFalse); + }); + + test('alternation on occurrences', () { + final q = RegexQuery('SoC/(cpu0|cpu1)'); + expect(q.matchOccurrence('cpu0', 1), equals({2})); + expect(q.matchOccurrence('cpu1', 1), equals({2})); + expect(q.matchOccurrence('mem_ctrl', 1), isEmpty); + }); + }); + + group('regex quantifiers', () { + test('{n,m} repetition', () { + final q = RegexQuery('SoC/**/d[0-9]{1,2}'); + expect(q.matchSignal('d0', 2), isTrue); + expect(q.matchSignal('d15', 2), isTrue); + // 'd123' has 3 digits → no match (anchored) + expect(q.matchSignal('d123', 2), isFalse); + }); + + test('+ one or more', () { + final q = RegexQuery('SoC/**/irq[0-9]+'); + expect(q.matchSignal('irq0', 2), isTrue); + expect(q.matchSignal('irq1', 2), isTrue); + expect(q.matchSignal('irq', 2), isFalse); + }); + }); + + group('matchSignal', () { + test('exact signal name', () { + final q = RegexQuery('SoC/cpu0/clk'); + expect(q.matchSignal('clk', 2), isTrue); + expect(q.matchSignal('reset', 2), isFalse); + }); + + test('glob * on signal', () { + final q = RegexQuery('SoC/cpu0/alu/*'); + expect(q.matchSignal('a', 3), isTrue); + expect(q.matchSignal('result', 3), isTrue); + }); + + test('glob * prefix on signal', () { + final q = RegexQuery('SoC/**/carry_*'); + expect(q.matchSignal('carry_out', 2), isTrue); + expect(q.matchSignal('overflow', 2), isFalse); + }); + + test('** then signal matches all signals when past segments', () { + final q = RegexQuery('SoC/**'); + // At state 1 (**), isComplete is true → match all signals + expect(q.matchSignal('clk', 1), isTrue); + expect(q.matchSignal('anything', 1), isTrue); + }); + + test('signal does not match non-terminal segment', () { + // SoC/cpu0/alu/result — 'result' is segment index 3, last segment + final q = RegexQuery('SoC/cpu0/alu/result'); + // At state 2, there's still 'result' to match → not last-terminal + expect(q.matchSignal('result', 2), isFalse); + // At state 3 it is the last segment + expect(q.matchSignal('result', 3), isTrue); + }); + }); + + group('isComplete', () { + test('complete when past all segments', () { + final q = RegexQuery('SoC/cpu0'); + expect(q.isComplete(0), isFalse); + expect(q.isComplete(1), isFalse); + expect(q.isComplete(2), isTrue); + }); + + test('complete with trailing glob-stars', () { + final q = RegexQuery('SoC/**'); + expect(q.isComplete(0), isFalse); + expect(q.isComplete(1), isTrue); // ** matches zero + }); + + test('not complete with remaining regex segments', () { + final q = RegexQuery('SoC/**/alu'); + expect(q.isComplete(1), isFalse); // ** then 'alu' remains + }); + }); + + group('target property', () { + test('defaults to signals', () { + expect(RegexQuery('x').target, equals(SearchTarget.signals)); + }); + + test('can be set to both', () { + final q = RegexQuery('x', target: SearchTarget.both); + expect(q.target, equals(SearchTarget.both)); + }); + }); + }); + + // ═══════════════════════════════════════════════════════════════ + // Factory constructors on HierarchyQuery + // ═══════════════════════════════════════════════════════════════ + + group('HierarchyQuery factories', () { + test('.prefix creates PrefixQuery', () { + final q = HierarchyQuery.prefix('cpu/clk'); + expect(q, isA()); + expect(q.segmentCount, equals(2)); + }); + + test('.regex creates RegexQuery', () { + final q = HierarchyQuery.regex('SoC/**/clk'); + expect(q, isA()); + expect(q.segmentCount, equals(3)); + }); + + test('.prefix with target', () { + final q = HierarchyQuery.prefix('x', target: SearchTarget.occurrences); + expect(q.target, equals(SearchTarget.occurrences)); + }); + + test('.regex with target', () { + final q = HierarchyQuery.regex('x', target: SearchTarget.both); + expect(q.target, equals(SearchTarget.both)); + }); + }); + + // ═══════════════════════════════════════════════════════════════ + // Edge cases + // ═══════════════════════════════════════════════════════════════ + + group('Edge cases', () { + test('empty query is isEmpty', () { + expect(HierarchyQuery.prefix('').isEmpty, isTrue); + expect(HierarchyQuery.regex('').isEmpty, isTrue); + expect(HierarchyQuery.prefix(' ').isEmpty, isTrue); + expect(HierarchyQuery.regex(' ').isEmpty, isTrue); + }); + + test('PrefixQuery with only separators', () { + final q = PrefixQuery('///'); + expect(q.segmentCount, equals(0)); + expect(q.isEmpty, isFalse); // raw string isn't blank + expect(q.isComplete(0), isTrue); // no segments to match + }); + + test('RegexQuery single segment', () { + final q = RegexQuery('clk'); + expect(q.segmentCount, equals(1)); + expect(q.matchSignal('clk', 0), isTrue); + expect(q.matchSignal('reset', 0), isFalse); + }); + + test('RegexQuery multiple consecutive glob-stars', () { + final q = RegexQuery('SoC/**/**/clk'); + // Should still work — multiple **'s just redundantly match zero+ + expect(q.isComplete(1), isFalse); // **/** then clk + final states = q.matchOccurrence('cpu0', 1); + expect(states, contains(1)); // stay at first ** + }); + + test('RegexQuery with .* explicit regex', () { + final q = RegexQuery('SoC/**/.*mux.*'); + expect(q.matchOccurrence('io_mux', 2), equals({3})); + expect(q.matchSignal('data_muxed', 2), isTrue); + expect(q.matchSignal('valid_muxed', 2), isTrue); + expect(q.matchSignal('clk', 2), isFalse); + }); + + test('PrefixQuery crossesBoundaries is false', () { + expect(PrefixQuery('x').crossesBoundaries, isFalse); + }); + + test('RegexQuery crossesBoundaries is false (uses ** explicitly)', () { + expect(RegexQuery('SoC/**/clk').crossesBoundaries, isFalse); + }); + }); + + // ═══════════════════════════════════════════════════════════════ + // Integration: queries against the real hierarchy via + // HierarchyService (to verify the contract makes sense) + // ═══════════════════════════════════════════════════════════════ + + group('Integration with hierarchy (signal path search)', () { + test('PrefixQuery segments match existing search', () { + // Verify PrefixQuery produces the same segments as + // the existing searchSignalPaths logic. + final q = PrefixQuery('cpu/alu/res'); + final paths = svc.searchSignalPaths('cpu/alu/res'); + // Both cpu0 and cpu1 have ALU with 'result' + expect(paths.length, equals(2)); + for (final p in paths) { + expect(p, contains('result')); + } + // Verify the query matches the same way + expect(q.matchOccurrence('cpu0', 0), isNotEmpty); + expect(q.matchOccurrence('alu', 1), isNotEmpty); + expect(q.matchSignal('result', 2), isTrue); + }); + + test('RegexQuery glob matches existing regex search', () { + // SoC/**/clk should find clk at many levels + final paths = svc.searchSignalPathsRegex('SoC/**/clk'); + // clk exists at: SoC, cpu0, cpu1, cpu0/regfile, cpu1/regfile, + // mem_ctrl, ch0, ch1, ch2, io_mux, uart0, uart1 = 12 total + expect(paths.length, equals(12)); + for (final p in paths) { + expect(p, endsWith('/clk')); + } + }); + + test('RegexQuery character class matches indexed signals', () { + final paths = svc.searchSignalPathsRegex('SoC/**/d[0-9]+'); + // d0, d1, d2, d15 in cpu0/regfile and cpu1/regfile = 8 total + expect(paths.length, equals(8)); + for (final p in paths) { + expect(p, matches(RegExp(r'/d\d+$'))); + } + }); + + test('RegexQuery alternation matches specific signals', () { + final paths = svc.searchSignalPathsRegex('SoC/**/(tx|rx)'); + // tx and rx in uart0 and uart1 = 4 total + expect(paths.length, equals(4)); + }); + + test('RegexQuery ch[0-2] matches channel occurrences', () { + final paths = svc.searchOccurrencePathsRegex('SoC/mem_ctrl/ch[0-2]'); + expect(paths.length, equals(3)); + expect( + paths, + containsAll([ + 'SoC/mem_ctrl/ch0', + 'SoC/mem_ctrl/ch1', + 'SoC/mem_ctrl/ch2', + ])); + }); + + test('RegexQuery *_mux matches occurrence by suffix', () { + final paths = svc.searchOccurrencePathsRegex('SoC/*_mux'); + expect(paths.length, equals(1)); + expect(paths.first, equals('SoC/io_mux')); + }); + + test('RegexQuery .*mux.* matches signals containing mux', () { + final paths = svc.searchSignalPathsRegex('SoC/**/.*mux.*'); + expect(paths, contains('SoC/io_mux/data_muxed')); + expect(paths, contains('SoC/io_mux/valid_muxed')); + }); + + test('PrefixQuery finds irq signals at root', () { + final paths = svc.searchSignalPaths('SoC/irq'); + expect(paths.length, equals(2)); + expect(paths, contains('SoC/irq0')); + expect(paths, contains('SoC/irq1')); + }); + + test('RegexQuery baud_sel across both UARTs', () { + final paths = svc.searchSignalPathsRegex('SoC/**/baud_sel'); + expect(paths.length, equals(2)); + expect(paths, contains('SoC/io_mux/uart0/baud_sel')); + expect(paths, contains('SoC/io_mux/uart1/baud_sel')); + }); + }); +} diff --git a/packages/rohd_hierarchy/test/hierarchy_search_controller_test.dart b/packages/rohd_hierarchy/test/hierarchy_search_controller_test.dart new file mode 100644 index 000000000..bd7801f2a --- /dev/null +++ b/packages/rohd_hierarchy/test/hierarchy_search_controller_test.dart @@ -0,0 +1,505 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// hierarchy_search_controller_test.dart +// Tests for HierarchySearchController. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +/// Minimal hierarchy for testing the controller with a real +/// HierarchyService. +HierarchyOccurrence _buildTestTree() => HierarchyOccurrence( + name: 'Top', + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'rst', width: 1), + ], + children: [ + HierarchyOccurrence( + name: 'cpu', + signals: [ + SignalOccurrence(name: 'data_in', width: 8), + SignalOccurrence(name: 'data_out', width: 8), + ], + children: [ + HierarchyOccurrence( + name: 'alu', + signals: [ + SignalOccurrence(name: 'a', width: 16), + SignalOccurrence(name: 'b', width: 16), + SignalOccurrence(name: 'result', width: 16), + ], + ), + ], + ), + HierarchyOccurrence( + name: 'mem', + signals: [ + SignalOccurrence(name: 'addr', width: 32), + ], + ), + ], + ); + +void main() { + late BaseHierarchyAdapter hierarchy; + late HierarchySearchController signalCtrl; + late HierarchySearchController moduleCtrl; + + setUp(() { + hierarchy = BaseHierarchyAdapter.fromTree(_buildTestTree()); + signalCtrl = HierarchySearchController.forSignals(hierarchy); + moduleCtrl = HierarchySearchController.forOccurrences(hierarchy); + }); + + group('HierarchySearchController — signal search', () { + test('starts with empty state', () { + expect(signalCtrl.results, isEmpty); + expect(signalCtrl.selectedIndex, 0); + expect(signalCtrl.hasResults, isFalse); + expect(signalCtrl.counterText, isEmpty); + expect(signalCtrl.currentSelection, isNull); + }); + + test('updateQuery populates results', () { + signalCtrl.updateQuery('clk'); + expect(signalCtrl.hasResults, isTrue); + expect(signalCtrl.results.first.name, 'clk'); + expect(signalCtrl.selectedIndex, 0); + }); + + test('updateQuery with empty string clears results', () { + signalCtrl.updateQuery('clk'); + expect(signalCtrl.hasResults, isTrue); + + signalCtrl.updateQuery(''); + expect(signalCtrl.hasResults, isFalse); + expect(signalCtrl.selectedIndex, 0); + }); + + test('updateQuery resets selectedIndex', () { + signalCtrl + ..updateQuery('data') + ..selectNext(); // index 1 + expect(signalCtrl.selectedIndex, 1); + + signalCtrl.updateQuery('data'); // re-search + expect(signalCtrl.selectedIndex, 0); // reset + }); + + test('normalise converts dots to slashes', () { + signalCtrl.updateQuery('cpu.alu.a'); + expect(signalCtrl.hasResults, isTrue); + expect(signalCtrl.results.first.name, 'a'); + }); + + test('counterText is correct', () { + signalCtrl.updateQuery('data'); + expect(signalCtrl.counterText, '1/${signalCtrl.results.length}'); + + signalCtrl.selectNext(); + expect(signalCtrl.counterText, '2/${signalCtrl.results.length}'); + }); + + test('currentSelection returns the highlighted result', () { + signalCtrl.updateQuery('data'); + final first = signalCtrl.currentSelection; + expect(first, isNotNull); + expect(first!.name, 'data_in'); + + signalCtrl.selectNext(); + expect(signalCtrl.currentSelection!.name, 'data_out'); + }); + + test('selectNext wraps around', () { + signalCtrl.updateQuery('data'); + final count = signalCtrl.results.length; + expect(count, greaterThan(1)); + + for (var i = 0; i < count; i++) { + signalCtrl.selectNext(); + } + expect(signalCtrl.selectedIndex, 0); // wrapped + }); + + test('selectPrevious wraps around', () { + signalCtrl + ..updateQuery('data') + ..selectPrevious(); // wraps from 0 → last + expect(signalCtrl.selectedIndex, signalCtrl.results.length - 1); + }); + + test('selectNext/selectPrevious no-op when empty', () { + signalCtrl.selectNext(); + expect(signalCtrl.selectedIndex, 0); + signalCtrl.selectPrevious(); + expect(signalCtrl.selectedIndex, 0); + }); + + test('clear resets everything', () { + signalCtrl + ..updateQuery('data') + ..selectNext(); + expect(signalCtrl.hasResults, isTrue); + expect(signalCtrl.selectedIndex, greaterThan(0)); + + signalCtrl.clear(); + expect(signalCtrl.results, isEmpty); + expect(signalCtrl.selectedIndex, 0); + expect(signalCtrl.currentSelection, isNull); + }); + + test('no results for non-matching query', () { + signalCtrl.updateQuery('xyz_no_match'); + expect(signalCtrl.hasResults, isFalse); + expect(signalCtrl.counterText, isEmpty); + }); + + test('plain query uses prefix match, not substring', () { + // 'a' should match signals starting with 'a' (addr, a), + // but NOT signals that merely contain 'a' (data_in, data_out). + signalCtrl.updateQuery('a'); + final names = signalCtrl.results.map((r) => r.name).toList(); + expect(names, contains('a')); // Top/cpu/alu/a + expect(names, contains('addr')); // Top/mem/addr + expect(names, isNot(contains('data_in'))); // 'a' is not a prefix + expect(names, isNot(contains('data_out'))); + }); + + test('glob * pattern routes to regex search', () { + // 'cpu/*_out' should match signals ending in '_out' under cpu + // (single-segment globs like '*_out' only search root-level; + // use a path segment to target a child module). + signalCtrl.updateQuery('cpu/*_out'); + expect(signalCtrl.hasResults, isTrue); + final names = signalCtrl.results.map((r) => r.name).toList(); + expect(names, contains('data_out')); + expect(names, isNot(contains('data_in'))); + }); + + test('glob * at end matches prefix', () { + signalCtrl.updateQuery('cpu/data*'); + final names = signalCtrl.results.map((r) => r.name).toList(); + expect(names, containsAll(['data_in', 'data_out'])); + }); + + test('glob * at root matches top-level signals', () { + // Single-segment glob only searches root module signals. + signalCtrl.updateQuery('*st'); + expect(signalCtrl.hasResults, isTrue); + final names = signalCtrl.results.map((r) => r.name).toList(); + expect(names, contains('rst')); + }); + }); + + group('HierarchySearchController — module search', () { + test('finds modules by name', () { + moduleCtrl.updateQuery('cpu'); + expect(moduleCtrl.hasResults, isTrue); + expect(moduleCtrl.results.first.occurrence.name, 'cpu'); + }); + + test('finds nested modules', () { + moduleCtrl.updateQuery('alu'); + expect(moduleCtrl.hasResults, isTrue); + expect(moduleCtrl.results.first.occurrence.name, 'alu'); + }); + + test('counterText and selection work for modules', () { + moduleCtrl.updateQuery('m'); // matches 'mem', possibly others + expect(moduleCtrl.hasResults, isTrue); + expect(moduleCtrl.counterText, isNotEmpty); + expect(moduleCtrl.currentSelection, isNotNull); + }); + }); + + group('scrollOffsetToReveal', () { + test('returns null when item is visible', () { + final offset = HierarchySearchController.scrollOffsetToReveal( + selectedIndex: 2, + itemHeight: 48, + viewportHeight: 300, + currentOffset: 0, + ); + // item at 96..144, viewport 0..300 → visible + expect(offset, isNull); + }); + + test('scrolls up when item is above viewport', () { + final offset = HierarchySearchController.scrollOffsetToReveal( + selectedIndex: 0, + itemHeight: 48, + viewportHeight: 300, + currentOffset: 100, + ); + // item at 0..48, viewport starts at 100 → need to scroll to 0 + expect(offset, 0.0); + }); + + test('scrolls down when item is below viewport', () { + final offset = HierarchySearchController.scrollOffsetToReveal( + selectedIndex: 10, + itemHeight: 48, + viewportHeight: 200, + currentOffset: 0, + ); + // item at 480..528, viewport 0..200 → scroll to 528-200 = 328 + expect(offset, 328.0); + }); + + test('returns null when item is at bottom edge', () { + final offset = HierarchySearchController.scrollOffsetToReveal( + selectedIndex: 4, + itemHeight: 50, + viewportHeight: 250, + currentOffset: 0, + ); + // item at 200..250, viewport 0..250 → exactly visible + expect(offset, isNull); + }); + }); + + // ------------------------------------------------------------------ + // VCD-style dot-separated paths + // ------------------------------------------------------------------ + group('VCD dot-separated paths', () { + late BaseHierarchyAdapter vcdHierarchy; + + setUp(() { + // VCD/FST files produce dot-separated IDs like "testbench.childA.clk" + vcdHierarchy = BaseHierarchyAdapter.fromTree( + HierarchyOccurrence( + name: 'testbench', + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'rst', width: 1), + ], + children: [ + HierarchyOccurrence( + name: 'childA', + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'data', width: 8), + ], + children: [ + HierarchyOccurrence( + name: 'sub', + signals: [ + SignalOccurrence(name: 'out', width: 4), + ], + ), + ], + ), + HierarchyOccurrence( + name: 'childB', + signals: [ + SignalOccurrence(name: 'enable', width: 1), + ], + ), + ], + ), + ); + }); + + test('searchSignalPaths with slash query finds dot-separated signal', () { + // User types "childA/clk" — walker normalises to hierarchySeparator + final results = vcdHierarchy.searchSignalPaths('childA/clk'); + expect(results, isNotEmpty); + expect(results, contains('testbench/childA/clk')); + }); + + test('searchSignalPaths with slash query finds deep signal', () { + final results = vcdHierarchy.searchSignalPaths('childA/sub/out'); + expect(results, isNotEmpty); + expect(results, contains('testbench/childA/sub/out')); + }); + + test('searchSignals with slash query finds dot-separated signal', () { + final results = vcdHierarchy.searchSignals('childA/clk'); + expect(results, isNotEmpty); + expect(results.first.signal!.path(), 'testbench/childA/clk'); + }); + + test('searchModules with slash query finds dot-separated module', () { + final results = vcdHierarchy.searchOccurrences('childA'); + expect(results, isNotEmpty); + expect(results.first.occurrence.path(), 'testbench/childA'); + }); + + test('searchSignalPaths with dot query still works', () { + // Dots in query are treated as separators too + final results = vcdHierarchy.searchSignalPaths('childA.clk'); + expect(results, isNotEmpty); + expect(results, contains('testbench/childA/clk')); + }); + + test('searchSignals with glob on dot-separated paths', () { + // Glob wildcard should work across dot-separated IDs + final results = vcdHierarchy.searchSignals('**/clk'); + expect(results.length, greaterThanOrEqualTo(2)); + final ids = results.map((r) => r.signal!.path()).toSet(); + expect(ids, contains('testbench/clk')); + expect(ids, contains('testbench/childA/clk')); + }); + + test('searchSignals with single segment on dot-separated paths', () { + // Single segment search should use startsWith + final results = vcdHierarchy.searchSignals('ena'); + expect(results, isNotEmpty); + expect(results.first.signal!.path(), 'testbench/childB/enable'); + }); + + test('controller forSignals works with dot-separated hierarchy', () { + final ctrl = + HierarchySearchController.forSignals(vcdHierarchy) + ..updateQuery('childA/clk'); + expect(ctrl.results, isNotEmpty); + expect(ctrl.results.first.signal!.path(), 'testbench/childA/clk'); + }); + }); + + // ------------------------------------------------------------------ + // DevTools flow — hierarchy with local signal IDs + // ------------------------------------------------------------------ + group('DevTools flow — local signal IDs → BaseHierarchyAdapter.fromTree', () { + late BaseHierarchyAdapter rohdHierarchy; + late HierarchySearchController rohdSignalCtrl; + late HierarchySearchController rohdModuleCtrl; + + setUp(() { + // Build a tree with local signal IDs (not full paths) — this is the key + // difference from the VCD path where IDs are full paths. + final alu = HierarchyOccurrence( + name: 'alu', + signals: [ + SignalOccurrence( + name: 'a', + width: 16, + direction: 'input', + ), + SignalOccurrence( + name: 'b', + width: 16, + direction: 'input', + ), + SignalOccurrence( + name: 'result', + width: 16, + direction: 'output', + ), + ], + ); + final cpu = HierarchyOccurrence( + name: 'cpu', + children: [alu], + signals: [ + SignalOccurrence( + name: 'data_in', + width: 8, + direction: 'input', + ), + SignalOccurrence( + name: 'data_out', + width: 8, + direction: 'output', + ), + ], + ); + final mem = HierarchyOccurrence( + name: 'mem', + signals: [ + SignalOccurrence( + name: 'addr', + width: 32, + direction: 'input', + ), + ], + ); + final root = HierarchyOccurrence( + name: 'Top', + children: [cpu, mem], + signals: [ + SignalOccurrence( + name: 'clk', + width: 1, + direction: 'input', + ), + SignalOccurrence( + name: 'rst', + width: 1, + direction: 'input', + ), + ], + )..buildAddresses(); + rohdHierarchy = BaseHierarchyAdapter.fromTree(root); + rohdSignalCtrl = HierarchySearchController.forSignals(rohdHierarchy); + rohdModuleCtrl = HierarchySearchController.forOccurrences(rohdHierarchy); + }); + + test('signal IDs are local (not full paths)', () { + final rootSigs = rohdHierarchy.root.signals; + final clk = rootSigs.firstWhere((s) => s.name == 'clk'); + expect(clk.name, 'clk'); + expect(clk.path(), 'Top/clk'); + }); + + test('updateQuery finds signals despite local IDs', () { + rohdSignalCtrl.updateQuery('clk'); + expect(rohdSignalCtrl.hasResults, isTrue); + expect(rohdSignalCtrl.results.first.name, 'clk'); + }); + + test('signalByAddress works with full path', () { + final addr = + OccurrenceAddress.tryFromPathname('Top/clk', rohdHierarchy.root); + final result = rohdHierarchy.signalByAddress(addr!); + expect(result, isNotNull); + expect(result!.name, 'clk'); + }); + + test('signalByAddress works with nested path', () { + final addr = OccurrenceAddress.tryFromPathname( + 'Top/cpu/alu/a', rohdHierarchy.root); + final result = rohdHierarchy.signalByAddress(addr!); + expect(result, isNotNull); + expect(result!.name, 'a'); + }); + + test('path-based search narrows to module', () { + rohdSignalCtrl.updateQuery('cpu/data'); + expect(rohdSignalCtrl.hasResults, isTrue); + final names = rohdSignalCtrl.results.map((r) => r.name).toSet(); + expect(names, containsAll(['data_in', 'data_out'])); + }); + + test('glob search works', () { + rohdSignalCtrl.updateQuery('**/a'); + expect(rohdSignalCtrl.hasResults, isTrue); + final names = rohdSignalCtrl.results.map((r) => r.name).toSet(); + expect(names, contains('a')); + }); + + test('module search works', () { + rohdModuleCtrl.updateQuery('alu'); + expect(rohdModuleCtrl.hasResults, isTrue); + expect(rohdModuleCtrl.results.first.occurrence.name, 'alu'); + }); + + test('search results match VCD-style tree results', () { + // The SAME queries should produce the same signal NAMES as + // the manually-built tree (VCD path), even though signal IDs differ. + rohdSignalCtrl.updateQuery('data'); + final rohdNames = rohdSignalCtrl.results.map((r) => r.name).toSet(); + + signalCtrl.updateQuery('data'); + final vcdNames = signalCtrl.results.map((r) => r.name).toSet(); + + expect(rohdNames, vcdNames, + reason: 'Same query should find same signals regardless of source'); + }); + }); +} diff --git a/packages/rohd_hierarchy/test/module_search_test.dart b/packages/rohd_hierarchy/test/module_search_test.dart new file mode 100644 index 000000000..9339492cf --- /dev/null +++ b/packages/rohd_hierarchy/test/module_search_test.dart @@ -0,0 +1,325 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// module_search_test.dart +// Tests for module tree search functionality using hierarchy API. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +void main() { + group('Module Tree Search - HierarchyService', () { + late HierarchyOccurrence root; + late HierarchyService hierarchy; + + setUpAll(() { + // Create a test hierarchy + // Top + // CPU (2 children) + // ALU + // Decoder + // Memory + // ControlUnit + + final alu = HierarchyOccurrence( + name: 'ALU', + ); + + final decoder = HierarchyOccurrence( + name: 'Decoder', + ); + + final cpu = HierarchyOccurrence( + name: 'CPU', + children: [alu, decoder], + ); + + final memory = HierarchyOccurrence( + name: 'Memory', + ); + + final controlUnit = HierarchyOccurrence( + name: 'ControlUnit', + ); + + root = HierarchyOccurrence( + name: 'Top', + children: [cpu, memory, controlUnit], + ); + + // Use BaseHierarchyAdapter.fromTree to convert to HierarchyService + hierarchy = BaseHierarchyAdapter.fromTree(root); + }); + + test('root node is accessible', () { + expect(hierarchy.root.name, equals('Top')); + expect(hierarchy.root.isPrimitive, isFalse); + }); + + test('children of root are accessible', () { + final children = hierarchy.root.children; + expect(children, isNotEmpty); + expect(children.length, equals(3)); + expect(children.any((c) => c.name == 'CPU'), isTrue); + }); + + test('searchNodePaths finds CPU module', () { + final results = hierarchy.searchOccurrencePaths('CPU'); + expect(results, isNotEmpty, + reason: 'Should find CPU module by simple name'); + expect(results.any((path) => path.contains('CPU')), isTrue); + }); + + test('searchNodePaths finds ALU with hierarchical query', () { + final results = hierarchy.searchOccurrencePaths('CPU/ALU'); + expect(results, isNotEmpty, + reason: 'Should find ALU with hierarchical path'); + expect(results.any((path) => path.contains('ALU')), isTrue); + }); + + test('searchNodePaths works with dot notation', () { + final results = hierarchy.searchOccurrencePaths('Top.CPU.ALU'); + expect(results, isNotEmpty, reason: 'Should find ALU with dot notation'); + expect(results.any((path) => path.contains('Top/CPU/ALU')), isTrue); + }); + + test('searchNodePaths limits results', () { + final results = hierarchy.searchOccurrencePaths('', limit: 2); + expect(results.length, lessThanOrEqualTo(2), + reason: 'Should respect limit parameter'); + }); + + test('searchModules returns ModuleSearchResult objects', () { + final results = hierarchy.searchOccurrences('Memory'); + expect(results, isNotEmpty); + expect(results.first, isA()); + expect(results.first.name, equals('Memory')); + expect(results.first.isModule, isTrue); + }); + + test('searchModules result contains full metadata', () { + final results = hierarchy.searchOccurrences('Decoder'); + expect(results, isNotEmpty); + final result = results.first; + expect(result.occurrenceId, contains('Decoder')); + expect(result.path, isNotEmpty); + expect(result.path.last, equals('Decoder')); + expect(result.occurrence, isNotNull); + }); + + test('searchNodePaths returns empty for non-matching query', () { + final results = hierarchy.searchOccurrencePaths('nonexistent'); + expect(results, isEmpty, + reason: 'Should return empty list for non-matching query'); + }); + + test('searchNodePaths returns empty for empty query', () { + final results = hierarchy.searchOccurrencePaths(''); + expect(results, isEmpty, + reason: 'Should return empty list for empty query'); + }); + + test('searchModules finds modules at different depths', () { + // Should find both Top and Top/CPU + final results = hierarchy.searchOccurrences('Top'); + expect(results.length, greaterThanOrEqualTo(1)); + expect(results.any((r) => r.name == 'Top'), isTrue); + }); + }); + + group('Module Search - Hierarchical Matching', () { + late HierarchyService hierarchy; + + setUpAll(() { + // Create a deeper hierarchy to test matching + // Design + // ProcessingUnit + // DataPath + // Multiplier + // Adder + // Controller + // Memory + // RAM + // Cache + + final multiplier = HierarchyOccurrence( + name: 'Multiplier', + ); + + final adder = HierarchyOccurrence( + name: 'Adder', + ); + + final dataPath = HierarchyOccurrence( + name: 'DataPath', + children: [multiplier, adder], + ); + + final controller = HierarchyOccurrence( + name: 'Controller', + ); + + final processingUnit = HierarchyOccurrence( + name: 'ProcessingUnit', + children: [dataPath, controller], + ); + + final ram = HierarchyOccurrence( + name: 'RAM', + ); + + final cache = HierarchyOccurrence( + name: 'Cache', + ); + + final memory = HierarchyOccurrence( + name: 'Memory', + children: [ram, cache], + ); + + final root = HierarchyOccurrence( + name: 'Design', + children: [processingUnit, memory], + ); + + hierarchy = BaseHierarchyAdapter.fromTree(root); + }); + + test('single segment matches at any level', () { + final results = hierarchy.searchOccurrencePaths('Multiplier'); + expect(results, isNotEmpty, + reason: 'Should find Multiplier even without full path'); + expect(results.any((r) => r.endsWith('Multiplier')), isTrue); + }); + + test('two segment path matches correctly', () { + final results = hierarchy.searchOccurrencePaths('DataPath/Multiplier'); + expect(results.any((r) => r.contains('DataPath/Multiplier')), isTrue, + reason: 'Should find Multiplier under DataPath'); + }); + + test('full hierarchical path matches precisely', () { + final results = + hierarchy.searchOccurrencePaths('ProcessingUnit/DataPath/Adder'); + expect(results.any((r) => r.contains('ProcessingUnit/DataPath/Adder')), + isTrue, + reason: 'Should find Adder with full hierarchical path'); + }); + + test('partial name matching works', () { + final results1 = hierarchy.searchOccurrencePaths('Path'); + expect(results1.any((r) => r.contains('DataPath')), isTrue, + reason: 'Should match partial "path" in DataPath'); + + final results2 = hierarchy.searchOccurrencePaths('Unit'); + expect(results2.any((r) => r.contains('ProcessingUnit')), isTrue, + reason: 'Should match partial "unit" in ProcessingUnit'); + }); + }); + + group('Module Search - Integration with Tree Filtering', () { + late HierarchyOccurrence root; + + setUpAll(() { + final alu = HierarchyOccurrence( + name: 'ALU', + ); + + final cpu = HierarchyOccurrence( + name: 'CPU', + children: [alu], + ); + + final memory = HierarchyOccurrence( + name: 'Memory', + ); + + root = HierarchyOccurrence( + name: 'Top', + children: [cpu, memory], + ); + }); + + test('hierarchical filtering shows root when descendant matches', () { + final matchesSearch = _filterNodeRecursive(root, 'alu'); + expect(matchesSearch, isTrue, + reason: 'Root should be shown because descendant matches'); + }); + + test('hierarchical filtering shows parent of matching child', () { + final cpuNode = root.children.first; + final cpuMatches = _filterNodeRecursive(cpuNode, 'alu'); + expect(cpuMatches, isTrue, + reason: 'CPU should be shown because child ALU matches'); + }); + + test('hierarchical filtering hides node without matching descendants', () { + final memoryNode = root.children.last; + final memoryMatches = _filterNodeRecursive(memoryNode, 'alu'); + expect(memoryMatches, isFalse, + reason: 'Memory should be hidden because no ALU descendant'); + }); + + test('path separator search shows root for hierarchical match', () { + final matchesSearch = _filterNodeRecursive(root, 'cpu/alu'); + expect(matchesSearch, isTrue, + reason: 'Root should be shown for hierarchical search'); + }); + + test('path separator search shows matching parent', () { + final cpuNode = root.children.first; + final cpuMatches = _filterNodeRecursive(cpuNode, 'cpu/alu'); + expect(cpuMatches, isTrue, + reason: 'CPU should be shown for hierarchical search'); + }); + + test('path separator search hides non-matching subtree', () { + final memoryNode = root.children.last; + final memoryMatches = _filterNodeRecursive(memoryNode, 'cpu/alu'); + expect(memoryMatches, isFalse, + reason: 'Memory should be hidden for non-matching path'); + }); + }); +} + +/// Helper function to simulate tree filtering with hierarchical search. +/// Matches query against node name using hierarchical logic. +bool _filterNodeRecursive(HierarchyOccurrence node, String query) { + final queryParts = query + .replaceAll('.', '/') + .toLowerCase() + .split('/') + .map((s) => s.trim()) + .where((s) => s.isNotEmpty) + .toList(); + + return _matchesHierarchicalQuery(node, queryParts, 0); +} + +bool _matchesHierarchicalQuery( + HierarchyOccurrence node, List queryParts, int queryIdx) { + if (queryIdx >= queryParts.length) { + return true; + } + + final currentQueryPart = queryParts[queryIdx].toLowerCase(); + final nodeName = node.name.toLowerCase(); + + final matched = nodeName.contains(currentQueryPart); + final nextQueryIdx = matched ? queryIdx + 1 : queryIdx; + + if (nextQueryIdx >= queryParts.length) { + return true; + } + + for (final child in node.children) { + if (_matchesHierarchicalQuery(child, queryParts, nextQueryIdx)) { + return true; + } + } + + return false; +} diff --git a/packages/rohd_hierarchy/test/occurrence_address_test.dart b/packages/rohd_hierarchy/test/occurrence_address_test.dart new file mode 100644 index 000000000..aafde9050 --- /dev/null +++ b/packages/rohd_hierarchy/test/occurrence_address_test.dart @@ -0,0 +1,335 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// occurrence_address_test.dart +// Unit tests for OccurrenceAddress class. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +void main() { + group('OccurrenceAddress', () { + test('child() appends module index', () { + final addr = OccurrenceAddress.root.child(0).child(2).child(4); + expect(addr.path, equals([0, 2, 4])); + }); + + test('signal() appends signal index', () { + final addr = const OccurrenceAddress([0, 1]).signal(5); + expect(addr.path, equals([0, 1, 5])); + }); + + test('equality and hashcode work correctly', () { + const addr1 = OccurrenceAddress([0, 2, 4]); + const addr2 = OccurrenceAddress([0, 2, 4]); + const addr3 = OccurrenceAddress([0, 2, 5]); + + expect(addr1, equals(addr2)); + expect(addr1.hashCode, equals(addr2.hashCode)); + expect(addr1, isNot(equals(addr3))); + expect(addr1.hashCode, isNot(equals(addr3.hashCode))); + }); + + test('toString() returns debug string', () { + expect(OccurrenceAddress.root.toString(), equals('[ROOT]')); + expect(const OccurrenceAddress([0, 2, 4]).toString(), equals('[0.2.4]')); + }); + + test('toDotString() returns dot-separated path', () { + expect(OccurrenceAddress.root.toDotString(), equals('')); + expect(const OccurrenceAddress([0]).toDotString(), equals('0')); + expect(const OccurrenceAddress([0, 2, 4]).toDotString(), equals('0.2.4')); + expect( + const OccurrenceAddress([10, 200]).toDotString(), equals('10.200')); + }); + + test('fromDotString() parses dot-separated path', () { + expect( + OccurrenceAddress.fromDotString(''), equals(OccurrenceAddress.root)); + expect(OccurrenceAddress.fromDotString('0'), + equals(const OccurrenceAddress([0]))); + expect(OccurrenceAddress.fromDotString('0.2.4'), + equals(const OccurrenceAddress([0, 2, 4]))); + expect(OccurrenceAddress.fromDotString('10.200'), + equals(const OccurrenceAddress([10, 200]))); + }); + + test('toDotString/fromDotString round-trip', () { + final testCases = [ + OccurrenceAddress.root, + const OccurrenceAddress([0]), + const OccurrenceAddress([5, 10, 15]), + const OccurrenceAddress([0, 0, 0]), + const OccurrenceAddress([255]), + const OccurrenceAddress([0, 1, 2, 3, 4, 5]), + ]; + for (final original in testCases) { + final dot = original.toDotString(); + final restored = OccurrenceAddress.fromDotString(dot); + expect(restored, equals(original), reason: 'Failed for $original'); + } + }); + }); + + group('OccurrenceAddress with HierarchyNode integration', () { + late HierarchyOccurrence root; + + setUp(() { + // Build a simple tree structure + final child0 = HierarchyOccurrence( + name: 'child_0', + signals: [ + SignalOccurrence( + name: 'sig0', + width: 1, + ), + SignalOccurrence( + name: 'sig1', + width: 8, + ), + ], + ); + + final grandchild = HierarchyOccurrence( + name: 'grandchild_0', + signals: [ + SignalOccurrence( + name: 'sig0', + width: 1, + ), + ], + ); + + final child1 = HierarchyOccurrence( + name: 'child_1', + signals: [ + SignalOccurrence( + name: 'sig0', + width: 4, + ), + ], + ); + + child0.children.add(grandchild); + + root = HierarchyOccurrence( + name: 'root', + signals: [ + SignalOccurrence( + name: 'clk', + width: 1, + ), + ], + children: [child0, child1], + ) + // Build addresses for all nodes + ..buildAddresses(); + }); + + test('buildAddresses assigns address to root', () { + expect(root.address, equals(OccurrenceAddress.root)); + }); + + test('buildAddresses assigns addresses to all nodes', () { + expect(root.children[0].address, equals(const OccurrenceAddress([0]))); + expect(root.children[1].address, equals(const OccurrenceAddress([1]))); + expect(root.children[0].children[0].address, + equals(const OccurrenceAddress([0, 0]))); + }); + + test('buildAddresses assigns addresses to all signals', () { + // Root signals + expect(root.signals[0].address, equals(const OccurrenceAddress([0]))); + + // Child signals + expect(root.children[0].signals[0].address, + equals(const OccurrenceAddress([0, 0]))); + expect(root.children[0].signals[1].address, + equals(const OccurrenceAddress([0, 1]))); + + // Grandchild signals + expect(root.children[0].children[0].signals[0].address, + equals(const OccurrenceAddress([0, 0, 0]))); + }); + }); + + group('HierarchyOccurrence.parent', () { + test('parent is null for root', () { + final root = HierarchyOccurrence(name: 'Top')..buildAddresses(); + expect(root.parent, isNull); + }); + + test('parent is set for child nodes after buildAddresses', () { + final child = HierarchyOccurrence(name: 'sub'); + final root = HierarchyOccurrence(name: 'Top', children: [child]) + ..buildAddresses(); + expect(child.parent, same(root)); + expect(child.path(), 'Top/sub'); + }); + }); + + group('HierarchyOccurrence.definition', () { + test('type is null when not provided', () { + final n = HierarchyOccurrence(name: 'a'); + expect(n.definition, isNull); + }); + + test('type is stored when provided', () { + final n = HierarchyOccurrence(name: 'a', definition: 'Counter'); + expect(n.definition, 'Counter'); + }); + }); + + group('isPrimitive on nodes', () { + test('default isPrimitive is false', () { + final n = HierarchyOccurrence(name: 'sub'); + expect(n.isPrimitive, isFalse); + }); + }); + + group('buildAddresses ports-first ordering', () { + test('ports get lower signal indices than internal signals', () { + final root = HierarchyOccurrence( + name: 'Top', + signals: [ + SignalOccurrence(name: 'internal_a', width: 8), + SignalOccurrence( + name: 'clk', width: 1, direction: 'input', portIndex: 0), + SignalOccurrence(name: 'internal_b', width: 4), + SignalOccurrence( + name: 'out', width: 8, direction: 'output', portIndex: 1), + ], + )..buildAddresses(); + + final byName = {for (final s in root.signals) s.name: s}; + + // Ports should get indices 0 and 1 + expect(byName['clk']!.address, equals(const OccurrenceAddress([0]))); + expect(byName['out']!.address, equals(const OccurrenceAddress([1]))); + + // Internal signals get indices 2 and 3 + expect( + byName['internal_a']!.address, equals(const OccurrenceAddress([2]))); + expect( + byName['internal_b']!.address, equals(const OccurrenceAddress([3]))); + }); + + test('portIndex matches signal address index', () { + final root = HierarchyOccurrence( + name: 'Mod', + signals: [ + SignalOccurrence( + name: 'a', width: 1, direction: 'input', portIndex: 0), + SignalOccurrence( + name: 'b', width: 1, direction: 'input', portIndex: 1), + SignalOccurrence( + name: 'y', width: 1, direction: 'output', portIndex: 2), + SignalOccurrence(name: 'net0', width: 1), + ], + )..buildAddresses(); + + for (final s in root.signals) { + if (s.isPort) { + // portIndex should equal the last element of the address path + expect(s.address!.path.last, equals(s.portIndex), + reason: '${s.name}: portIndex=${s.portIndex} ' + 'but address index=${s.address!.path.last}'); + } + } + }); + + test('portCount returns correct count', () { + final occ = HierarchyOccurrence( + name: 'X', + signals: [ + SignalOccurrence( + name: 'a', width: 1, direction: 'input', portIndex: 0), + SignalOccurrence(name: 'b', width: 1), + SignalOccurrence( + name: 'c', width: 1, direction: 'output', portIndex: 1), + ], + ); + expect(occ.portCount, equals(2)); + }); + + test('all-ports occurrence: indices match list order', () { + final occ = HierarchyOccurrence( + name: 'Buf', + signals: [ + SignalOccurrence( + name: 'in', width: 8, direction: 'input', portIndex: 0), + SignalOccurrence( + name: 'out', width: 8, direction: 'output', portIndex: 1), + ], + )..buildAddresses(); + + expect(occ.signals[0].address, equals(const OccurrenceAddress([0]))); + expect(occ.signals[1].address, equals(const OccurrenceAddress([1]))); + }); + + test('all-internal occurrence: indices unchanged', () { + final occ = HierarchyOccurrence( + name: 'Internal', + signals: [ + SignalOccurrence(name: 'x', width: 1), + SignalOccurrence(name: 'y', width: 1), + ], + )..buildAddresses(); + + expect(occ.signals[0].address, equals(const OccurrenceAddress([0]))); + expect(occ.signals[1].address, equals(const OccurrenceAddress([1]))); + }); + + test('nested: ports-first ordering applies at every level', () { + final child = HierarchyOccurrence( + name: 'sub', + signals: [ + SignalOccurrence(name: 'net', width: 1), + SignalOccurrence( + name: 'p', width: 1, direction: 'input', portIndex: 0), + ], + ); + final root = HierarchyOccurrence( + name: 'Top', + children: [child], + signals: [ + SignalOccurrence(name: 'net_top', width: 1), + SignalOccurrence( + name: 'clk', width: 1, direction: 'input', portIndex: 0), + ], + )..buildAddresses(); + + // Root: clk (port) at 0, net_top (internal) at 1 + final rootByName = {for (final s in root.signals) s.name: s}; + expect(rootByName['clk']!.address!.path.last, equals(0)); + expect(rootByName['net_top']!.address!.path.last, equals(1)); + + // Child: p (port) at 0, net (internal) at 1 + final childByName = {for (final s in child.signals) s.name: s}; + expect(childByName['p']!.address!.path.last, equals(0)); + expect(childByName['net']!.address!.path.last, equals(1)); + }); + }); + + group('SignalOccurrence.portIndex', () { + test('portIndex is null for internal signals', () { + final s = SignalOccurrence(name: 'net', width: 1); + expect(s.portIndex, isNull); + expect(s.isPort, isFalse); + }); + + test('portIndex is set for port signals', () { + final s = SignalOccurrence( + name: 'clk', + width: 1, + direction: 'input', + portIndex: 3, + ); + expect(s.portIndex, equals(3)); + expect(s.isPort, isTrue); + }); + }); +} diff --git a/packages/rohd_hierarchy/test/regex_search_test.dart b/packages/rohd_hierarchy/test/regex_search_test.dart new file mode 100644 index 000000000..51f134f21 --- /dev/null +++ b/packages/rohd_hierarchy/test/regex_search_test.dart @@ -0,0 +1,546 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// regex_search_test.dart +// Tests for regex-based hierarchy search. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +void main() { + group('Regex search - HierarchyService', () { + late HierarchyService hierarchy; + + setUpAll(() { + // Build a test hierarchy: + // + // Top + // CPU + // ALU signals: [a, b, result, carry_out] + // Decoder signals: [opcode, enable] + // RegFile signals: [clk, reset, d0, d1, d2, d15] + // Memory + // Cache signals: [clk, addr, data, hit] + // DRAM signals: [clk, cas, ras] + // IO + // UART signals: [clk, tx, rx] + // signals (Top): [clk, reset] + + final alu = HierarchyOccurrence( + name: 'ALU', + signals: [ + SignalOccurrence(name: 'a', width: 8), + SignalOccurrence(name: 'b', width: 8), + SignalOccurrence(name: 'result', width: 8), + SignalOccurrence(name: 'carry_out', width: 1), + ], + ); + + final decoder = HierarchyOccurrence( + name: 'Decoder', + signals: [ + SignalOccurrence(name: 'opcode', width: 4), + SignalOccurrence(name: 'enable', width: 1), + ], + ); + + final regFile = HierarchyOccurrence( + name: 'RegFile', + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'reset', width: 1), + SignalOccurrence(name: 'd0', width: 8), + SignalOccurrence(name: 'd1', width: 8), + SignalOccurrence(name: 'd2', width: 8), + SignalOccurrence(name: 'd15', width: 8), + ], + ); + + final cpu = HierarchyOccurrence( + name: 'CPU', + children: [alu, decoder, regFile], + ); + + final cache = HierarchyOccurrence( + name: 'Cache', + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'addr', width: 16), + SignalOccurrence(name: 'data', width: 32), + SignalOccurrence(name: 'hit', width: 1), + ], + ); + + final dram = HierarchyOccurrence( + name: 'DRAM', + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'cas', width: 1), + SignalOccurrence(name: 'ras', width: 1), + ], + ); + + final memory = HierarchyOccurrence( + name: 'Memory', + children: [cache, dram], + ); + + final uart = HierarchyOccurrence( + name: 'UART', + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'tx', width: 1), + SignalOccurrence(name: 'rx', width: 1), + ], + ); + + final io = HierarchyOccurrence( + name: 'IO', + children: [uart], + ); + + final root = HierarchyOccurrence( + name: 'Top', + children: [cpu, memory, io], + signals: [ + SignalOccurrence(name: 'clk', width: 1), + SignalOccurrence(name: 'reset', width: 1), + SignalOccurrence(name: 'data_m', width: 8), + SignalOccurrence(name: 'addr_m', width: 16), + SignalOccurrence(name: 'flag_m', width: 1), + ], + ); + + hierarchy = BaseHierarchyAdapter.fromTree(root); + }); + + // ── Exact match ── + + test('exact path matches single signal', () { + final results = hierarchy.searchSignalPathsRegex('Top/CPU/ALU/result'); + expect(results, contains('Top/CPU/ALU/result')); + expect(results.length, 1); + }); + + test('dot in regex pattern is treated as regex metachar, not separator', + () { + // In regex mode, `.` is NOT a hierarchy separator — only `/` is. + // `Top.CPU` is a single segment meaning "Top" + any char + "CPU". + final results = hierarchy.searchSignalPathsRegex('Top.CPU.ALU.result'); + // No match because the hierarchy root is "Top", not "Top.CPU.ALU" + expect(results, isEmpty); + }); + + // ── Wildcard at one level ── + + test('.* matches all signals in a module', () { + final results = hierarchy.searchSignalPathsRegex('Top/CPU/ALU/.*'); + expect( + results, + containsAll([ + 'Top/CPU/ALU/a', + 'Top/CPU/ALU/b', + 'Top/CPU/ALU/result', + 'Top/CPU/ALU/carry_out', + ])); + expect(results.length, 4); + }); + + test('.* matches all children at a module level', () { + final results = hierarchy.searchSignalPathsRegex('Top/.*/clk'); + // Should match CPU/RegFile/clk but not deeper (** would be needed + // for that). .* represents any single-level child of Top. + // Top has children CPU, Memory, IO — none of them have clk directly + // (Top's own signals aren't "children"). Actually let's check: + // Top/.*/clk means: Top / (any child) / clk as signal + // That doesn't match because clk is in deeper modules. + // This should return empty for signals one level below Top. + expect(results, isEmpty); + }); + + test('.* matches modules at one level for signal search', () { + // Top/CPU/.*/clk — matches ALU, Decoder, RegFile; only RegFile has clk + final results = hierarchy.searchSignalPathsRegex('Top/CPU/.*/clk'); + expect(results, contains('Top/CPU/RegFile/clk')); + expect(results.length, 1); + }); + + // ── Glob-star ** ── + + test('** matches signals at any depth', () { + final results = hierarchy.searchSignalPathsRegex('Top/**/clk'); + expect( + results, + containsAll([ + 'Top/CPU/RegFile/clk', + 'Top/Memory/Cache/clk', + 'Top/Memory/DRAM/clk', + 'Top/IO/UART/clk', + ])); + // Top's own clk is also accessible through ** matching zero levels + expect(results, contains('Top/clk')); + }); + + test('** at beginning matches everything', () { + final results = hierarchy.searchSignalPathsRegex('**/clk'); + // All clk signals anywhere + expect(results.length, greaterThanOrEqualTo(5)); + expect( + results, + containsAll([ + 'Top/clk', + 'Top/CPU/RegFile/clk', + 'Top/Memory/Cache/clk', + 'Top/Memory/DRAM/clk', + 'Top/IO/UART/clk', + ])); + }); + + test('** between levels matches across boundaries', () { + final results = hierarchy.searchSignalPathsRegex('Top/CPU/**/d0'); + expect(results, contains('Top/CPU/RegFile/d0')); + expect(results.length, 1); + }); + + test('** with regex signal pattern', () { + final results = hierarchy.searchSignalPathsRegex('Top/**/d[0-9]+'); + expect( + results, + containsAll([ + 'Top/CPU/RegFile/d0', + 'Top/CPU/RegFile/d1', + 'Top/CPU/RegFile/d2', + 'Top/CPU/RegFile/d15', + ])); + expect(results.length, 4); + }); + + // ── Regex character classes ── + + test('character class in signal name', () { + final results = + hierarchy.searchSignalPathsRegex('Top/CPU/RegFile/d[0-2]'); + expect( + results, + containsAll([ + 'Top/CPU/RegFile/d0', + 'Top/CPU/RegFile/d1', + 'Top/CPU/RegFile/d2', + ])); + expect(results, isNot(contains('Top/CPU/RegFile/d15'))); + }); + + // ── Alternation ── + + test('alternation in signal name', () { + final results = hierarchy.searchSignalPathsRegex('Top/**/(?:clk|reset)'); + expect( + results, + containsAll([ + 'Top/clk', + 'Top/reset', + 'Top/CPU/RegFile/clk', + 'Top/CPU/RegFile/reset', + 'Top/Memory/Cache/clk', + 'Top/Memory/DRAM/clk', + 'Top/IO/UART/clk', + ])); + expect(results.length, 7); + }); + + test('alternation in module name', () { + final results = hierarchy.searchSignalPathsRegex('Top/(CPU|IO)/.*/clk'); + expect( + results, + containsAll([ + 'Top/CPU/RegFile/clk', + 'Top/IO/UART/clk', + ])); + }); + + // ── Module search ── + + test('searchOccurrencePathsRegex finds modules', () { + final results = hierarchy.searchOccurrencePathsRegex('Top/CPU/.*'); + expect( + results, + containsAll([ + 'Top/CPU/ALU', + 'Top/CPU/Decoder', + 'Top/CPU/RegFile', + ])); + }); + + test('searchOccurrencePathsRegex with **', () { + final results = hierarchy.searchOccurrencePathsRegex('Top/**/DRAM'); + expect(results, contains('Top/Memory/DRAM')); + }); + + // ── Enriched results ── + + test('searchSignalsRegex returns SignalSearchResult objects', () { + final results = hierarchy.searchSignalsRegex('Top/CPU/ALU/result'); + expect(results.length, 1); + // signalId uses the normalised hierarchySeparator ('/') format + // from the tree walker — findSignalById normalises both '.' and '/'. + expect(results.first.signalId, 'Top/CPU/ALU/result'); + expect(results.first.signal, isNotNull); + expect(results.first.signal!.name, 'result'); + }); + + test('searchSignalsRegex returns results with SignalOccurrence objects', + () { + final results = hierarchy.searchSignalsRegex('Top/**/carry_out'); + expect(results.length, 1); + expect(results.first.signal, isNotNull); + expect(results.first.signal!.name, 'carry_out'); + expect(results.first.signal!.width, 1); + }); + + test('searchOccurrencesRegex returns OccurrenceSearchResult objects', () { + final results = hierarchy.searchOccurrencesRegex('Top/**/Cache'); + expect(results.length, 1); + expect(results.first.occurrenceId, 'Top/Memory/Cache'); + }); + + // ── Limit ── + + test('limit controls maximum results', () { + final results = hierarchy.searchSignalPathsRegex('Top/**/.+', limit: 3); + expect(results.length, 3); + }); + + // ── Glob-style wildcards ── + + test('glob * at start matches suffix pattern', () { + // User's scenario: "*m" should match signals ending in "m". + final results = hierarchy.searchSignalPathsRegex('Top/*_m'); + expect( + results, + containsAll([ + 'Top/data_m', + 'Top/addr_m', + 'Top/flag_m', + ])); + expect(results.length, 3); + }); + + test('glob * at end matches prefix pattern', () { + final results = hierarchy.searchSignalPathsRegex('Top/CPU/RegFile/d*'); + expect( + results, + containsAll([ + 'Top/CPU/RegFile/d0', + 'Top/CPU/RegFile/d1', + 'Top/CPU/RegFile/d2', + 'Top/CPU/RegFile/d15', + ])); + expect(results.length, 4); + }); + + test('glob * in the middle matches infix pattern', () { + // *d*a* should match names containing 'd' followed eventually by 'a' + final results = + hierarchy.searchSignalPathsRegex('Top/Memory/Cache/*d*a*'); + expect(results, contains('Top/Memory/Cache/data')); + }); + + test('glob * matches all signals (like .*)', () { + final results = hierarchy.searchSignalPathsRegex('Top/CPU/ALU/*'); + expect( + results, + containsAll([ + 'Top/CPU/ALU/a', + 'Top/CPU/ALU/b', + 'Top/CPU/ALU/result', + 'Top/CPU/ALU/carry_out', + ])); + expect(results.length, 4); + }); + + test('glob * in module level matches any child', () { + final results = hierarchy.searchSignalPathsRegex('Top/*/clk'); + // Top's immediate module-children are CPU, Memory, IO — none of + // them have a direct clk signal, so this is empty. + expect(results, isEmpty); + }); + + test('glob * combined with ** for deep search', () { + final results = hierarchy.searchSignalPathsRegex('Top/**/*_m'); + expect( + results, + containsAll([ + 'Top/data_m', + 'Top/addr_m', + 'Top/flag_m', + ])); + expect(results.length, 3); + }); + + // ── Empty / no match ── + + test('empty pattern returns nothing', () { + expect(hierarchy.searchSignalPathsRegex(''), isEmpty); + expect(hierarchy.searchOccurrencePathsRegex(''), isEmpty); + }); + + test('non-matching pattern returns nothing', () { + expect(hierarchy.searchSignalPathsRegex('Top/NonExistent/foo'), isEmpty); + }); + + // ── ** at various positions ── + + test('trailing ** collects all signals below', () { + final results = hierarchy.searchSignalPathsRegex('Top/Memory/**'); + // Should collect all signals in Memory subtree + expect( + results, + containsAll([ + 'Top/Memory/Cache/clk', + 'Top/Memory/Cache/addr', + 'Top/Memory/Cache/data', + 'Top/Memory/Cache/hit', + 'Top/Memory/DRAM/clk', + 'Top/Memory/DRAM/cas', + 'Top/Memory/DRAM/ras', + ])); + expect(results.length, 7); + }); + + test('multiple ** segments work', () { + final results = + hierarchy.searchSignalPathsRegex('**/(CPU|Memory)/**/clk'); + expect( + results, + containsAll([ + 'Top/CPU/RegFile/clk', + 'Top/Memory/Cache/clk', + 'Top/Memory/DRAM/clk', + ])); + }); + }); + + group('searchOccurrences dispatches to regex', () { + late HierarchyService hierarchy; + + setUpAll(() { + // Build hierarchy: + // Top + // CPU + // ALU + // Decoder + // MuxUnit + // Memory + // Cache + // DRAM + // IO + // UART + + final alu = HierarchyOccurrence( + name: 'ALU', + ); + + final decoder = HierarchyOccurrence( + name: 'Decoder', + ); + + final muxUnit = HierarchyOccurrence( + name: 'MuxUnit', + ); + + final cpu = HierarchyOccurrence( + name: 'CPU', + children: [alu, decoder, muxUnit], + ); + + final cache = HierarchyOccurrence( + name: 'Cache', + ); + + final dram = HierarchyOccurrence( + name: 'DRAM', + ); + + final memory = HierarchyOccurrence( + name: 'Memory', + children: [cache, dram], + ); + + final uart = HierarchyOccurrence( + name: 'UART', + ); + + final io = HierarchyOccurrence( + name: 'IO', + children: [uart], + ); + + final root = HierarchyOccurrence( + name: 'Top', + children: [cpu, memory, io], + ); + + hierarchy = BaseHierarchyAdapter.fromTree(root); + }); + + test('searchOccurrences with glob pattern finds modules', () { + // Pattern: *Mux* should find MuxUnit (auto-prepended with **/) + final results = hierarchy.searchOccurrences('*Mux*'); + expect(results, isNotEmpty, + reason: + 'searchOccurrences should dispatch to regex for glob patterns'); + expect(results.any((r) => r.name == 'MuxUnit'), isTrue); + }); + + test('searchOccurrences with ** finds deep modules', () { + final results = hierarchy.searchOccurrences('**/*Mux*'); + expect(results, isNotEmpty); + expect(results.any((r) => r.name == 'MuxUnit'), isTrue); + }); + + test('searchOccurrences with .* matches at one level', () { + // */.* matches any child one level below root + final results = hierarchy.searchOccurrences('*/.*/.*'); + expect(results.length, greaterThanOrEqualTo(3), + reason: 'Should match ALU, Decoder, MuxUnit, Cache, DRAM, UART'); + }); + + test('searchOccurrences with explicit path pattern', () { + // */CPU/.* matches children of CPU + final results = hierarchy.searchOccurrences('*/CPU/.*'); + expect(results.length, 3); + expect(results.any((r) => r.name == 'ALU'), isTrue); + expect(results.any((r) => r.name == 'Decoder'), isTrue); + expect(results.any((r) => r.name == 'MuxUnit'), isTrue); + }); + + test('searchOccurrences with alternation', () { + final results = hierarchy.searchOccurrences('**/(ALU|DRAM)'); + expect(results.length, 2); + expect(results.any((r) => r.name == 'ALU'), isTrue); + expect(results.any((r) => r.name == 'DRAM'), isTrue); + }); + + test('searchOccurrences without regex uses plain matching', () { + // Plain query without glob chars uses substring matching + final results = hierarchy.searchOccurrences('Mux'); + expect(results, isNotEmpty); + expect(results.any((r) => r.name == 'MuxUnit'), isTrue); + }); + + test('searchOccurrences with leading **/ is not double-prepended', () { + final results = hierarchy.searchOccurrences('**/UART'); + expect(results.length, 1); + expect(results.first.name, 'UART'); + }); + + test('searchOccurrences with leading */ is not double-prepended', () { + final results = hierarchy.searchOccurrences('*/CPU'); + expect(results.length, 1); + expect(results.first.name, 'CPU'); + }); + }); +} diff --git a/packages/rohd_hierarchy/test/rohd_signal_resolve_test.dart b/packages/rohd_hierarchy/test/rohd_signal_resolve_test.dart new file mode 100644 index 000000000..fa22a21b4 --- /dev/null +++ b/packages/rohd_hierarchy/test/rohd_signal_resolve_test.dart @@ -0,0 +1,72 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// rohd_signal_resolve_test.dart +// Tests for resolving ROHD dot-separated signal IDs. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +void main() { + late HierarchyOccurrence root; + late BaseHierarchyAdapter adapter; + + setUpAll(() { + root = HierarchyOccurrence( + name: 'abcd', + signals: [ + SignalOccurrence(name: 'clk', width: 1, direction: 'input'), + SignalOccurrence(name: 'resetn', width: 1, direction: 'input'), + SignalOccurrence(name: 'arvalid_s', width: 1, direction: 'input'), + ], + children: [ + HierarchyOccurrence( + name: 'sub', + signals: [ + SignalOccurrence(name: 'data', width: 8, direction: 'output'), + ], + ), + ], + ); + + adapter = BaseHierarchyAdapter.fromTree(root); + root.buildAddresses(); + }); + + SignalOccurrence? resolve(String dotPath) { + final addr = OccurrenceAddress.tryFromPathname(dotPath, root); + if (addr == null) { + return null; + } + return adapter.signalByAddress(addr); + } + + group('findSignalById resolves ROHD dot-separated signal IDs', () { + test('resolves top-level clk', () { + final sig = resolve('abcd.clk'); + expect(sig, isNotNull); + expect(sig!.path(), 'abcd/clk'); + }); + + test('resolves top-level resetn', () { + final sig = resolve('abcd.resetn'); + expect(sig, isNotNull); + expect(sig!.path(), 'abcd/resetn'); + }); + + test('resolves top-level arvalid_s', () { + final sig = resolve('abcd.arvalid_s'); + expect(sig, isNotNull); + expect(sig!.path(), 'abcd/arvalid_s'); + }); + + test('resolves nested sub.data', () { + final sig = resolve('abcd.sub.data'); + expect(sig, isNotNull); + expect(sig!.path(), 'abcd/sub/data'); + }); + }); +} diff --git a/packages/rohd_hierarchy/test/signal_search_result_test.dart b/packages/rohd_hierarchy/test/signal_search_result_test.dart new file mode 100644 index 000000000..2669c859f --- /dev/null +++ b/packages/rohd_hierarchy/test/signal_search_result_test.dart @@ -0,0 +1,236 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// signal_search_result_test.dart +// Tests for SignalSearchResult and ModuleSearchResult display helpers. +// +// 2026 April +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +void main() { + group('SignalSearchResult display helpers', () { + test('displayPath strips top module', () { + const result = SignalSearchResult( + signalId: 'Top/counter/clk', + path: ['Top', 'counter', 'clk'], + ); + expect(result.displayPath, equals('counter/clk')); + }); + + test('displayPath for top-level signal', () { + const result = SignalSearchResult( + signalId: 'Top/clk', + path: ['Top', 'clk'], + ); + expect(result.displayPath, equals('clk')); + }); + + test('displayPath for single-segment path', () { + const result = SignalSearchResult( + signalId: 'clk', + path: ['clk'], + ); + expect(result.displayPath, equals('clk')); + }); + + test('displaySegments strips top module', () { + const result = SignalSearchResult( + signalId: 'Top/sub1/sub2/clk', + path: ['Top', 'sub1', 'sub2', 'clk'], + ); + expect(result.displaySegments, equals(['sub1', 'sub2', 'clk'])); + }); + + test('intermediateOccurrenceNames extracts middle segments', () { + const result = SignalSearchResult( + signalId: 'Top/sub1/sub2/clk', + path: ['Top', 'sub1', 'sub2', 'clk'], + ); + expect(result.intermediateOccurrenceNames, equals(['sub1', 'sub2'])); + }); + + test('intermediateOccurrenceNames empty for top-level signal', () { + const result = SignalSearchResult( + signalId: 'Top/clk', + path: ['Top', 'clk'], + ); + expect(result.intermediateOccurrenceNames, isEmpty); + }); + + test('intermediateOccurrenceNames empty for single-level nesting', () { + const result = SignalSearchResult( + signalId: 'Top/sub1/clk', + path: ['Top', 'sub1', 'clk'], + ); + // sub1 is both the containing block and an intermediate instance + expect(result.intermediateOccurrenceNames, equals(['sub1'])); + }); + + test('name returns last path segment', () { + const result = SignalSearchResult( + signalId: 'Top/counter/clk', + path: ['Top', 'counter', 'clk'], + ); + expect(result.name, equals('clk')); + }); + + test('equality based on signalId', () { + const a = SignalSearchResult( + signalId: 'Top/clk', + path: ['Top', 'clk'], + ); + const b = SignalSearchResult( + signalId: 'Top/clk', + path: ['Top', 'clk'], + ); + expect(a, equals(b)); + expect(a.hashCode, equals(b.hashCode)); + }); + }); + + group('HierarchySearchResult.normalizeQuery', () { + test('converts dots to slashes', () { + expect( + HierarchySearchResult.normalizeQuery('top.cpu.clk'), + equals('top/cpu/clk'), + ); + }); + + test('preserves slashes', () { + expect( + HierarchySearchResult.normalizeQuery('top/cpu/clk'), + equals('top/cpu/clk'), + ); + }); + + test('handles mixed separators', () { + expect( + HierarchySearchResult.normalizeQuery('top.cpu/clk'), + equals('top/cpu/clk'), + ); + }); + + test('handles empty query', () { + expect(HierarchySearchResult.normalizeQuery(''), equals('')); + }); + }); + + group('ModuleSearchResult display helpers', () { + late HierarchyOccurrence aluNode; + + setUp(() { + aluNode = HierarchyOccurrence( + name: 'ALU', + ); + }); + + test('displayPath strips top module', () { + final result = OccurrenceSearchResult( + occurrenceId: 'Top/CPU/ALU', + path: const ['Top', 'CPU', 'ALU'], + occurrence: aluNode, + ); + expect(result.displayPath, equals('CPU/ALU')); + }); + + test('displaySegments strips top module', () { + final result = OccurrenceSearchResult( + occurrenceId: 'Top/CPU/ALU', + path: const ['Top', 'CPU', 'ALU'], + occurrence: aluNode, + ); + expect(result.displaySegments, equals(['CPU', 'ALU'])); + }); + + test('displayPath for single-segment path', () { + final topNode = HierarchyOccurrence( + name: 'Top', + ); + final result = OccurrenceSearchResult( + occurrenceId: 'Top', + path: const ['Top'], + occurrence: topNode, + ); + expect(result.displayPath, equals('Top')); + }); + + test('equality based on moduleId', () { + final a = OccurrenceSearchResult( + occurrenceId: 'Top/CPU/ALU', + path: const ['Top', 'CPU', 'ALU'], + occurrence: aluNode, + ); + final b = OccurrenceSearchResult( + occurrenceId: 'Top/CPU/ALU', + path: const ['Top', 'CPU', 'ALU'], + occurrence: aluNode, + ); + expect(a, equals(b)); + expect(a.hashCode, equals(b.hashCode)); + }); + }); + + group('ModuleSearchResult.normalizeQuery', () { + test('converts dots to slashes', () { + expect( + HierarchySearchResult.normalizeQuery('top.cpu'), + equals('top/cpu'), + ); + }); + }); + + group('searchSignals integration with display helpers', () { + late HierarchyService hierarchy; + + setUpAll(() { + // Build: Top -> counter (with clk, data[8] signals) + final counter = HierarchyOccurrence( + name: 'counter', + signals: [ + SignalOccurrence( + name: 'clk', + width: 1, + ), + SignalOccurrence( + name: 'data', + width: 8, + ), + ], + ); + + final root = HierarchyOccurrence( + name: 'Top', + children: [counter], + signals: [ + SignalOccurrence( + name: 'reset', + width: 1, + direction: 'input', + ), + ], + ); + + hierarchy = BaseHierarchyAdapter.fromTree(root); + }); + + test('searchSignals returns enriched results', () { + final results = hierarchy.searchSignals('clk'); + expect(results, isNotEmpty); + final result = results.first; + expect(result.signalId, contains('clk')); + expect(result.displayPath, equals('counter/clk')); + expect(result.intermediateOccurrenceNames, equals(['counter'])); + }); + + test('searchSignals for top-level port', () { + final results = hierarchy.searchSignals('reset'); + expect(results, isNotEmpty); + final result = results.first; + expect(result.displayPath, equals('reset')); + expect(result.intermediateOccurrenceNames, isEmpty); + }); + }); +} From 5a176d71f97f4a31a2f17f4954e76a7d7f364845 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Sun, 5 Jul 2026 17:13:54 -0700 Subject: [PATCH 2/6] suggested fix is to use download instead of Download for site instability --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cb333f2a0..6bb9116ce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,7 +48,7 @@ To run the complete ROHD test suite for development, you need to install [Icarus #### On your own system -[Visual Studio Code (VSCode)](https://code.visualstudio.com/) is a great IDE for development. You can find installation instructions for VSCode here: +[Visual Studio Code (VSCode)](https://code.visualstudio.com/) is a great IDE for development. You can find installation instructions for VSCode here: The Dart extension extends VSCode with support for the Dart programming language and provides tools for effectively editing, refactoring and running. Check out the detailed information: From d80cf5557422ff126989501296e9ff41e4583b11 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 7 Jul 2026 12:41:11 -0700 Subject: [PATCH 3/6] Propagate devtools waveform updates --- .../lib/src/hierarchy_occurrence.dart | 4 + .../test/filter_bank_integration_test.dart | 9 ++ .../lib/rohd_devtools/ui/signal_table.dart | 30 +---- rohd_devtools_extension/pubspec.yaml | 2 +- .../tree_structure_page_test.dart | 118 ++++++++++-------- 5 files changed, 84 insertions(+), 79 deletions(-) diff --git a/packages/rohd_hierarchy/lib/src/hierarchy_occurrence.dart b/packages/rohd_hierarchy/lib/src/hierarchy_occurrence.dart index 5d991f391..13ddd6192 100644 --- a/packages/rohd_hierarchy/lib/src/hierarchy_occurrence.dart +++ b/packages/rohd_hierarchy/lib/src/hierarchy_occurrence.dart @@ -141,6 +141,10 @@ class HierarchyOccurrence { List get outputs => signals.where((s) => s.direction == 'output').toList(); + /// Returns only inout signals. + List get inouts => + signals.where((s) => s.direction == 'inout').toList(); + /// Number of port signals in this occurrence. int get portCount => signals.where((s) => s.isPort).length; diff --git a/packages/rohd_hierarchy/test/filter_bank_integration_test.dart b/packages/rohd_hierarchy/test/filter_bank_integration_test.dart index 4ed1ee309..6a8fe173a 100644 --- a/packages/rohd_hierarchy/test/filter_bank_integration_test.dart +++ b/packages/rohd_hierarchy/test/filter_bank_integration_test.dart @@ -637,6 +637,15 @@ void main() { ); }); + test('inouts getter returns only inout ports', () { + final inouts = service.root.inouts; + expect(inouts, isNotEmpty, reason: 'inouts should include dataBus'); + expect(inouts.map((s) => s.name), contains('dataBus')); + for (final s in inouts) { + expect(s.direction, 'inout'); + } + }); + test('ports getter includes inout ports', () { final allPorts = service.root.ports; final inouts = allPorts.where((p) => p.direction == 'inout').toList(); diff --git a/rohd_devtools_extension/lib/rohd_devtools/ui/signal_table.dart b/rohd_devtools_extension/lib/rohd_devtools/ui/signal_table.dart index 8e97328d8..1b66861da 100644 --- a/rohd_devtools_extension/lib/rohd_devtools/ui/signal_table.dart +++ b/rohd_devtools_extension/lib/rohd_devtools/ui/signal_table.dart @@ -90,29 +90,12 @@ class _SignalTableState extends State { TableRow _generateSignalRow(SignalModel signal) { return TableRow( children: [ + SizedBox(height: 32, child: Center(child: Text(signal.name))), + SizedBox(height: 32, child: Center(child: Text(signal.direction))), + SizedBox(height: 32, child: Center(child: Text(signal.value))), SizedBox( height: 32, - child: Center( - child: Text(signal.name), - ), - ), - SizedBox( - height: 32, - child: Center( - child: Text(signal.direction), - ), - ), - SizedBox( - height: 32, - child: Center( - child: Text(signal.value), - ), - ), - SizedBox( - height: 32, - child: Center( - child: Text(signal.width.toString()), - ), + child: Center(child: Text(signal.width.toString())), ), ], ); @@ -124,10 +107,7 @@ class _SignalTableState extends State { child: Center( child: Text( text, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 15, - ), + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15), ), ), ); diff --git a/rohd_devtools_extension/pubspec.yaml b/rohd_devtools_extension/pubspec.yaml index 0aa366e78..8b5bb226f 100644 --- a/rohd_devtools_extension/pubspec.yaml +++ b/rohd_devtools_extension/pubspec.yaml @@ -29,7 +29,7 @@ dev_dependencies: flutter_lints: ^3.0.1 build_runner: ^2.4.7 mocktail: ^1.0.2 - bloc_lint: ^0.1.0 + bloc_lint: ^0.3.7 flutter: uses-material-design: true diff --git a/rohd_devtools_extension/test/modules/tree_structure/tree_structure_page_test.dart b/rohd_devtools_extension/test/modules/tree_structure/tree_structure_page_test.dart index 8ccd28d4c..15287d70e 100644 --- a/rohd_devtools_extension/test/modules/tree_structure/tree_structure_page_test.dart +++ b/rohd_devtools_extension/test/modules/tree_structure/tree_structure_page_test.dart @@ -29,71 +29,83 @@ void main() { }); testWidgets( - 'displays ModuleTreeCard when state is RohdServiceLoaded with treeModel', - (tester) async { - final treeModel = MockTreeModel(); + 'displays ModuleTreeCard when state is RohdServiceLoaded with treeModel', + (tester) async { + final treeModel = MockTreeModel(); - when(() => rohdServiceCubit.state) - .thenReturn(RohdServiceLoaded(treeModel)); - when(() => rohdServiceCubit.stream) - .thenAnswer((_) => Stream.value(RohdServiceLoaded(treeModel))); - when(() => treeSearchTermCubit.state).thenReturn(null); - when(() => treeSearchTermCubit.stream) - .thenAnswer((_) => Stream.value(null)); + when( + () => rohdServiceCubit.state, + ).thenReturn(RohdServiceLoaded(treeModel)); + when( + () => rohdServiceCubit.stream, + ).thenAnswer((_) => Stream.value(RohdServiceLoaded(treeModel))); + when(() => treeSearchTermCubit.state).thenReturn(null); + when( + () => treeSearchTermCubit.stream, + ).thenAnswer((_) => Stream.value(null)); - await tester.pumpWidget( - MultiBlocProvider( - providers: [ - BlocProvider.value(value: rohdServiceCubit), - BlocProvider.value(value: treeSearchTermCubit), - ], - child: MaterialApp( - home: Scaffold( - body: TreeStructurePage(screenSize: const Size(2000, 1000)), + await tester.pumpWidget( + MultiBlocProvider( + providers: [ + BlocProvider.value(value: rohdServiceCubit), + BlocProvider.value( + value: treeSearchTermCubit, + ), + ], + child: MaterialApp( + home: Scaffold( + body: TreeStructurePage(screenSize: const Size(2000, 1000)), + ), ), ), - ), - ); + ); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(); - expect(find.byType(ModuleTreeCard), findsOneWidget); - }); + expect(find.byType(ModuleTreeCard), findsOneWidget); + }, + ); testWidgets( - 'displays SignalDetailsCard when state is RohdServiceLoaded with selected module', - (tester) async { - final treeModel = MockTreeModel(); - final signalModelList = [ - MockSignalModel(), - MockSignalModel() - ]; - when(() => rohdServiceCubit.state) - .thenReturn(RohdServiceLoaded(treeModel)); - when(() => rohdServiceCubit.stream) - .thenAnswer((_) => Stream.value(RohdServiceLoaded(treeModel))); - when(() => treeModel.inputs).thenReturn(signalModelList); - when(() => treeModel.outputs).thenReturn(signalModelList); - when(() => treeSearchTermCubit.stream) - .thenAnswer((_) => Stream.value(null)); + 'displays SignalDetailsCard when state is RohdServiceLoaded with selected module', + (tester) async { + final treeModel = MockTreeModel(); + final signalModelList = [ + MockSignalModel(), + MockSignalModel(), + ]; + when( + () => rohdServiceCubit.state, + ).thenReturn(RohdServiceLoaded(treeModel)); + when( + () => rohdServiceCubit.stream, + ).thenAnswer((_) => Stream.value(RohdServiceLoaded(treeModel))); + when(() => treeModel.inputs).thenReturn(signalModelList); + when(() => treeModel.outputs).thenReturn(signalModelList); + when( + () => treeSearchTermCubit.stream, + ).thenAnswer((_) => Stream.value(null)); - await tester.pumpWidget( - MultiBlocProvider( - providers: [ - BlocProvider.value(value: rohdServiceCubit), - BlocProvider.value(value: treeSearchTermCubit), - ], - child: MaterialApp( - home: Scaffold( - body: TreeStructurePage(screenSize: const Size(800, 600)), + await tester.pumpWidget( + MultiBlocProvider( + providers: [ + BlocProvider.value(value: rohdServiceCubit), + BlocProvider.value( + value: treeSearchTermCubit, + ), + ], + child: MaterialApp( + home: Scaffold( + body: TreeStructurePage(screenSize: const Size(800, 600)), + ), ), ), - ), - ); + ); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(); - expect(find.byType(SignalDetailsCard), findsOneWidget); - }); + expect(find.byType(SignalDetailsCard), findsOneWidget); + }, + ); }); } From 64ddb18de28811fa994eb2b9d2079a84dbdee667 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 7 Jul 2026 16:04:44 -0700 Subject: [PATCH 4/6] Use ROHD icons for devtools web app --- .../web/icons/Icon-192.png | Bin 5292 -> 4911 bytes .../web/icons/Icon-512.png | Bin 8252 -> 50043 bytes .../web/icons/Icon-maskable-192.png | Bin 5594 -> 4911 bytes .../web/icons/Icon-maskable-512.png | Bin 20998 -> 50043 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/rohd_devtools_extension/web/icons/Icon-192.png b/rohd_devtools_extension/web/icons/Icon-192.png index b749bfef07473333cf1dd31e9eed89862a5d52aa..acd165142d5e74105a26e389c46231011d4df16a 100644 GIT binary patch literal 4911 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 diff --git a/rohd_devtools_extension/web/icons/Icon-512.png b/rohd_devtools_extension/web/icons/Icon-512.png index 88cfd48dff1169879ba46840804b412fe02fefd6..bb667c609122ee453334839b1af4b3eea08e982d 100644 GIT binary patch literal 50043 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 diff --git a/rohd_devtools_extension/web/icons/Icon-maskable-192.png b/rohd_devtools_extension/web/icons/Icon-maskable-192.png index eb9b4d76e525556d5d89141648c724331630325d..acd165142d5e74105a26e389c46231011d4df16a 100644 GIT binary patch literal 4911 zcmYjVc{tSH_kYhA%OI1njAbmDF_bWvm?Y~MBfF#$HCablCVSSIv2PKItb>xGLK4|V zmPlntk@%Q=vXs;)TPVNzJkR%!-}Bt}z0Y&*>)dmnd(U~Dd*11e_E!6Z<%9tM*k?^5 zIP*r;=E+nxLbgHMPv#;)|)7 zzrS*&-P_x@Ou82JQ$K#j7)1UE9r<_jtVSjq6H|d%4Baq}*^BSqmvT|&f9<+>NcEqA zxEv4H?JyOt7<0I1U54lqAZS>E#Epw0fl`<)e*~WL790DRztwLXWtvV#Qy^0`Ro3?> zE&ej%tA)k+IIcByM06G}4n&+_XV;j` z0^9CS0%f|Wy8OZtwy79C1F+xLQZLs1)BJ)cx=1Wm3je`MDdiKxz02_x=+DaV$5MSH zsX?1-$HCvtmQO*+(SFgaf18h5?JUzW=`Uh&K{8MBh7lPh^kej8x&g>NTcC1g4hBR5 zf`ZrIojy#@=J=quAdZll zqdc8U=nfcLC|0f;)iXk?ql*=;$PZ8s&I(-y{wiu-_+F+hd1XBWNn*pM`D3B{C|RP1 zY=By;I9_T?uAy-F4H&vj`{+2>r_a+PK1rGdz5F5=pK()XBJo!wW0eApFy?BZ%P$g6 zsDe!4Mi6(P9<&I@0XrZo;D@Et?HdnYkhG8Djx8}mkRPI&d!j|fq)(>UqzzSiOsiyU zUO0I?ST|0&C1Qe3m}Jysq;rZ zv8}*Qt35pOS+xe|pc{3UDyL#;7EUA!kS!SXI}3AQ!F0_Y3+p7bcdGQJ<&OF$b;6R5 zCLjaQ*gPA}gV%R}3ud}9M3s9-)V3b6T?Vo6^vv5)@}R7~Idht?V4w`qgX${XkIhSi zlD*|5Sc5nW?Yh4$fO=a55NmWmS+-tanOg%8tZw%I2e>2i>fgytVNYD9MTO6Pu-q;B4!_y zYcdO36Q~Hbnp;CTJ#)G@q{Rv6yo;@DRg}WT$W8(Q#1OoRsOpfnCGVmm`4HmW+!X*zyIVX;aZ8D8+2t5390irv9Md-4`KkXi*%QI#UlZwu$ef<7%gDmGe2IaP6F9-6 zBOED?waYobt2~8kWVxG1WOY~J$X(}SbZv0v=-Pq0X66{9g0N_4l13bK;CuVnb?dMM zm}L)bsf?tndY@y!kA@X5rq}+Q)uw6C|L_c1ND~ILfQFNdAau|Aq?y9SV!8!Qp0-2R zpvn3xiR-EY9d?XI;0b0S!HXu~vf`z~mEhpW?rxTYrT?j&cUfL@3qZ?c5jj{FAW$|> zB^>VU3`|-wZ0IHv>x?-Y4X!N5ie+tjvKT#B4{-IN-wDIjm>%{kVs(!cYpdBdT~`zM zRzMb{YGZn+wgS>~pU|Vx*RO)e2&fitzJ8g%zeJi7z;ShMIN=S3B%xbj3e!&?mQP58 z%F$1zjXY5eGnJm9t|s9?mA*nY9F$6CrYG2`GR_o zy{C+2o)ryA70U>|qA!{npAG{S`RdAWJP_*(E1VT%T`H`4mKLv^BerG?rM84C<#Ei= zA0FGw*{fwQK}PkgIq7EB(g4nHRwQsdR@ek?lp(ejKVaSa#Tz+w%0w>j$=RD>pcCJq zyBbsh>_9#w5Rbp9*Z&HT2E|P5nBPC32?lH}&4L!^s85@086)8C$T6znEs?{lPZom) z{&0Y!z$$cHm^cH2*PFgQkWZ$DP$Z3O-c0Ctj$r^6pcdYTP;`{-k^4$TR}&7{eGed;tn+>GwOF3xDsCBYlqH7~bVoDUv<=@bSclw|Srq2M4`3cwsro7bR5SkD zt>bN1NA|{i;Bm^kuW$|o-Pe*ayHuVC6?4v6ZZ~y>0D&J81gsnU#(JZy%$3cxx%g=R zJ)?$$&uu8gYKMSjtdBL23-M#BkK9`CELak%Aq6wDpZ~7)_H;pBYZ~&jeh&fulsK3- ze$#u5h4Uh0o2CR0T=*!-Ig_@CQ_lp`Ty5}-OGVkHKpon>_%b{4U+vgZ;b*|FZqZ~KGT9j)MkQfS_Xq>+L4+1Wp z051PEZKd&YDpLx>V#n^^I!4-5-_JFCd4+2&v7TwG0GA+BLev{k5!ykDOM>M;r>*#O zL(J)O@v~ z?|Xda48Lr6HLO#_I|D7^_>z8P8q4#d*s(7>oOKgm*N5xVue9yk<5P|B(^ESO^nxmn zU+bjJ7)89x*^u{4hLQ{Ltk9iK*5&e@S{`)+E8)nwwj4!{)JU8QkF9CH|L^1 zm5+^^NLwL*`yOKIpt)v@y9pXpe(qnPXYLNR4pTUgE|L1npz85H`<&X3Ci^{9pWWwK zAuKKI2{L*gpq5$?TSwT4r(a-2K<}u{?lmPjsh-!fqNR4tK64-GRw`fXE;Uh2iJD>J zadvTclU!qJs;XLlrR%404^%xMdPreRec(H5u$e6{r|^YgIx1bi_W_^OKN*4Lbl$mQ z@{ZdfVTXx#{~bq#?LJ-N$43lTsOIesXEScGL`ebu1)8-}7RU9OEllC@;QsXmL3vP$ zmjG9abDpVjL45()d{jWSVht?sNS}&ePRwV+k&AAJ5{<gHrji#$S`~66@7&j&sCi<>KeC?EkKL35~*Q~?MhN0S2 zNNZUTzwSEIImd>+@+$lBvQwLTin z?!LxHH5!2C`v0;FXgR#;e<0FT)z@PW+eLs3_S;Q6@QKxMF{Zmk{L{j*k{zT5@mArj zgF&iGg5L=z+a|SQZhC?H@{Jlq@ro2~17t^-s>7`i`?#xG>=~{$zaS`~{N|PU;>|Mf zXLsxU&yG%@pz$lr4{DB-bpgRZ%M9rLU|%NU<;hi4j~qvZF*D`VB&_<*=+yfN6)b3O z1joT}aLmy}h(on}RKI%hS^j2FxAtW{og!d}Y-srIFLIZ}wc>Pex>55^oWq>erWSZI zG#81fE_@YA!#KbXleZEdKjoXXvOc0QJViocOq+_5(0Bx8L%An-l!P6>VqM~7I~vPL zZu((|#+d#rg2c+UN>Wypdr<1J+XkZFWPX@B>^x=@Ph0K$Z0VcbC4; zxazA5$ynOLhTFL#As@wz!Rh{%wq$tP|^Chck2@wQzf z)vB0msQm_!;h|-5&ne0x;H|6&`6TnRp0r)EF)Yi0vfws@1&x`B2wy%UWQ&6muR(Ja zPr!HeupiJevKZ56GJK`!HxH@Zg7kWUMIB5|tSt$}g;k2yU5^hFnB62U?Zo-`9610D zg47g4ZZw`^zFY%Ef@UoY0uKs?yK{A%pV7Xv_i`-!LdpbQ^_T@+mbIhkT{ikJ+jfD! zxDV7zNicP_E5G!~6&g5jUE*c7uCV_eY*oq*HEUQqc4p)SJg+hX2|dYEaj^rj1_=|vLI>vK9z%ooXzBQb(ce< zXr5r-0qA$Zhr}VZE<9L@Q_T0_CQm3%ha6*f^&)6bjUpoP_mOdU@6%ujWnTz#Fm;%@LNC6mjTU& zI9QV^9lGDkygy)1Jc=C}c_;eztvnWlB%ocH-86gkyIRga$L(1c4-VjYAyC?6SdOR) zNix2b?3r3BJ)f%ao zKXsGW0l^6*Mn!eiDTVU((u|v)BYhX0&RgMlteE52Ssp{P+?vr8kecrVNzI45=HRq= zYoVLZxf5PgY(#wz(Y>$VNGq`B`DTGBBPc8{PQ1IlpZPU(;@?(9|4!cGw#sG*;nH@& zJs(kR0Z+bfK;P;z0Fvj-m5^LdEk$2jBlhZfqCzRo;kE{PH2>&#!5^nc7QNBAe!JrLZQF$hv$Z+xk-!4iE&9uP* zcU4w>uYO-XcnQgGODm9=(mW5@&0P}f+)+PKKNN$6>N5u?d2nxLgstOQZ$!y`YCepE;#WU(F(itMf|K z%N~e-vdldbQp?f3PYNxbXUWc84ppCh5}MZi!>Up31xUo{ez~?HpRrg+`Fj*GvE-4I zWmvpeGLF#iMmFZ!P$RRJyIyE%#6?RM!(YpOz3d?b@EHUcpd^^Gf0(J;Q}JsAxvxLu z>DFe)jE6D+;3Vx4s3Kzv{=TYhXM`3HgCkRF+*3t2!XH9N8ut)Pg7Wi!SSt|z5R^CH zef$-rcr;$JRcfo`=cC3`%BWX+m3LPYtfqg6N}5LMiXmLgcOV*$R~~s}h?D>~hF&en zM1M6-+z`4XcI086?9#!NW?>H!!*5i8qtu%a40frJ>a6!;V-S2=Bx zsRVVcdRM7-B7w|9@IS!&q%>_;Pk@36vCun*!uGw&c#Z&Vpbg4iq>=AfCy# zqq>rYDCkz91b8XT$P+A+$m0nGOE=;TTM6ID=8;gHc=Q$@^_ga(tM3uv#kiv@!%PeD z>8O*AA;b5=Ta)ZL!;~A8%;nj{Y+!&inFL;pKb6|0#*DgidDSub4Fr2=tgGVzP9a*{ z&dT-uCtWv3m60QO9(!T$fAV*MYGsUN&SzPcgk{pxAEL3KEOGJT7!$Elej~TkVTu_j z#)gWfL&;f1xeyJyo}>BpMw)gtRAHhyi7Dx&sZs>o_;6yan{!4YTJ22c`>{uaz(&J?Qzrqt5esgp+hJNqB-GRj=U%#fpK!-8EFy zH1G*`P%gdAcmGbCoR3Z5$X7)cwICwvSMpqi<7A&MCsBQI^SHX#BdHm`;L6YI*Erq5 z%+YW?3$>=r>T1W~{BjKNFJmNzp6GNf^xbFTFFSvgwQv(=e*7hl@QqKS50#~#^IQPxBmuh|i9sXnt4ch@VT0F7% zMtrs@KWIOo+QV@lSs66A>2pz6-`9Jk=0vv&u?)^F@HZ)-6HT=B7LF;rdj zskUyBfbojcX#CS>WrIWo9D=DIwcXM8=I5D{SGf$~=gh-$LwY?*)cD%38%sCc?5OsX z-XfkyL-1`VavZ?>(pI-xp-kYq=1hsnyP^TLb%0vKRSo^~r{x?ISLY1i7KjSp z*0h&jG(Rkkq2+G_6eS>n&6>&Xk+ngOMcYrk<8KrukQHzfx675^^s$~<@d$9X{VBbg z2Fd4Z%g`!-P}d#`?B4#S-9x*eNlOVRnDrn#jY@~$jfQ-~3Od;A;x-BI1BEDdvr`pI z#D)d)!2_`GiZOUu1crb!hqH=ezs0qk<_xDm_Kkw?r*?0C3|Io6>$!kyDl;eH=aqg$B zsH_|ZD?jP2dc=)|L>DZmGyYKa06~5?C2Lc0#D%62p(YS;%_DRCB1k(+eLGXVMe+=4 zkKiJ%!N6^mxqM=wq`0+yoE#VHF%R<{mMamR9o_1JH8jfnJ?NPLs$9U!9!dq8 z0B{dI2!M|sYGH&9TAY34OlpIsQ4i5bnbG>?cWwat1I13|r|_inLE?FS@Hxdxn_YZN z3jfUO*X9Q@?HZ>Q{W0z60!bbGh557XIKu1?)u|cf%go`pwo}CD=0tau-}t@R2OrSH zQzZr%JfYa`>2!g??76=GJ$%ECbQh7Q2wLRp9QoyiRHP7VE^>JHm>9EqR3<$Y=Z1K^SHuwxCy-5@z3 zVM{XNNm}yM*pRdLKp??+_2&!bp#`=(Lh1vR{~j%n;cJv~9lXeMv)@}Odta)RnK|6* zC+IVSWumLo%{6bLDpn)Gz>6r&;Qs0^+Sz_yx_KNz9Dlt^ax`4>;EWrIT#(lJ_40<= z750fHZ7hI{}%%5`;lwkI4<_FJw@!U^vW;igL0k+mK)-j zYuCK#mCDK3F|SC}tC2>m$ZCqNB7ac-0UFBJ|8RxmG@4a4qdjvMzzS&h9pQmu^x&*= zGvapd1#K%Da&)8f?<9WN`2H^qpd@{7In6DNM&916TRqtF4;3`R|Nhwbw=(4|^Io@T zIjoR?tB8d*sO>PX4vaIHF|W;WVl6L1JvSmStgnRQq zTX4(>1f^5QOAH{=18Q2Vc1JI{V=yOr7yZJf4Vpfo zeHXdhBe{PyY;)yF;=ycMW@Kb>t;yE>;f79~AlJ8k`xWucCxJfsXf2P72bAavWL1G#W z;o%kdH(mYCM{$~yw4({KatNGim49O2HY6O07$B`*K7}MvgI=4x=SKdKVb8C$eJseA$tmSFOztFd*3W`J`yIB_~}k%Sd_bPBK8LxH)?8#jM{^%J_0|L z!gFI|68)G}ex5`Xh{5pB%GtlJ{Z5em*e0sH+sU1UVl7<5%Bq+YrHWL7?X?3LBi1R@_)F-_OqI1Zv`L zb6^Lq#H^2@d_(Z4E6xA9Z4o3kvf78ZDz!5W1#Mp|E;rvJz&4qj2pXVxKB8Vg0}ek%4erou@QM&2t7Cn5GwYqy%{>jI z)4;3SAgqVi#b{kqX#$Mt6L8NhZYgonb7>+r#BHje)bvaZ2c0nAvrN3gez+dNXaV;A zmyR0z@9h4@6~rJik-=2M-T+d`t&@YWhsoP_XP-NsVO}wmo!nR~QVWU?nVlQjNfgcTzE-PkfIX5G z1?&MwaeuzhF=u)X%Vpg_e@>d2yZwxl6-r3OMqDn8_6m^4z3zG##cK0Fsgq8fcvmhu z{73jseR%X%$85H^jRAcrhd&k!i^xL9FrS7qw2$&gwAS8AfAk#g_E_tP;x66fS`Mn@SNVrcn_N;EQm z`Mt3Z%rw%hDqTH-s~6SrIL$hIPKL5^7ejkLTBr46;pHTQDdoErS(B>``t;+1+M zvU&Se9@T_BeK;A^p|n^krIR+6rH~BjvRIugf`&EuX9u69`9C?9ANVL8l(rY6#mu^i z=*5Q)-%o*tWl`#b8p*ZH0I}hn#gV%|jt6V_JanDGuekR*-wF`u;amTCpGG|1;4A5$ zYbHF{?G1vv5;8Ph5%kEW)t|am2_4ik!`7q{ymfHoe^Z99c|$;FAL+NbxE-_zheYbV z3hb0`uZGTsgA5TG(X|GVDSJyJxsyR7V5PS_WSnYgwc_D60m7u*x4b2D79r5UgtL18 zcCHWk+K6N1Pg2c;0#r-)XpwGX?|Iv)^CLWqwF=a}fXUSM?n6E;cCeW5ER^om#{)Jr zJR81pkK?VoFm@N-s%hd7@hBS0xuCD0-UDVLDDkl7Ck=BAj*^ps`393}AJ+Ruq@fl9 z%R(&?5Nc3lnEKGaYMLmRzKXow1+Gh|O-LG7XiNxkG^uyv zpAtLINwMK}IWK65hOw&O>~EJ}x@lDBtB`yKeV1%GtY4PzT%@~wa1VgZn7QRwc7C)_ zpEF~upeDRg_<#w=dLQ)E?AzXUQpbKXYxkp>;c@aOr6A|dHA?KaZkL0svwB^U#zmx0 zzW4^&G!w7YeRxt<9;d@8H=u(j{6+Uj5AuTluvZZD4b+#+6Rp?(yJ`BC9EW9!b&KdPvzJYe5l7 zMJ9aC@S;sA0{F0XyVY{}FzW0Vh)0mPf_BX82E+CD&)wf2!x@{RO~XBYu80TONl3e+ zA7W$ra6LcDW_j4s-`3tI^VhG*sa5lLc+V6ONf=hO@q4|p`CinYqk1Ko*MbZ6_M05k zSwSwkvu;`|I*_Vl=zPd|dVD0lh&Ha)CSJJvV{AEdF{^Kn_Yfsd!{Pc1GNgw}(^~%)jk5~0L~ms|Rez1fiK~s5t(p1ci5Gq$JC#^JrXf?8 z-Y-Zi_Hvi>oBzV8DSRG!7dm|%IlZg3^0{5~;>)8-+Nk&EhAd(}s^7%MuU}lphNW9Q zT)DPo(ob{tB7_?u;4-qGDo!sh&7gHaJfkh43QwL|bbFVi@+oy;i;M zM&CP^v~lx1U`pi9PmSr&Mc<%HAq0DGH?Ft95)WY`P?~7O z`O^Nr{Py9M#Ls4Y7OM?e%Y*Mvrme%=DwQaye^Qut_1pOMrg^!5u(f9p(D%MR%1K>% zRGw%=dYvw@)o}Fw@tOtPjz`45mfpn;OT&V(;z75J*<$52{sB65$gDjwX3Xa!x_wE- z!#RpwHM#WrO*|~f7z}(}o7US(+0FYLM}6de>gQdtPazXz?OcNv4R^oYLJ_BQOd_l172oSK$6!1r@g+B@0ofJ4*{>_AIxfe-#xp>(1 z@Y3Nfd>fmqvjL;?+DmZk*KsfXJf<%~(gcLwEez%>1c6XSboURUh&k=B)MS>6kw9bY z{7vdev7;A}5fy*ZE23DS{J?8at~xwVk`pEwP5^k?XMQ7u64;KmFJ#POzdG#np~F&H ze-BUh@g54)dsS%nkBb}+GuUEKU~pHcYIg4vSo$J(J|U36bs0Use+3A&IMcR%6@jv$ z=+QI+@wW@?iu}Hpyzlvj-EYeop{f65GX0O%>w#0t|V z1-svWk`hU~m`|O$kw5?Yn5UhI%9P-<45A(v0ld1n+%Ziq&TVpBcV9n}L9Tus-TI)f zd_(g+nYCDR@+wYNQm1GwxhUN4tGMLCzDzPqY$~`l<47{+l<{FZ$L6(>J)|}!bi<)| zE35dl{a2)&leQ@LlDxLQOfUDS`;+ZQ4ozrleQwaR-K|@9T{#hB5Z^t#8 zC-d_G;B4;F#8A2EBL58s$zF-=SCr`P#z zNCTnHF&|X@q>SkAoYu>&s9v@zCpv9lLSH-UZzfhJh`EZA{X#%nqw@@aW^vPcfQrlPs(qQxmC|4tp^&sHy!H!2FH5eC{M@g;ElWNzlb-+ zxpfc0m4<}L){4|RZ>KReag2j%Ot_UKkgpJN!7Y_y3;Ssz{9 z!K3isRtaFtQII5^6}cm9RZd5nTp9psk&u1C(BY`(_tolBwzV_@0F*m%3G%Y?2utyS zY`xM0iDRT)yTyYukFeGQ&W@ReM+ADG1xu@ruq&^GK35`+2r}b^V!m1(VgH|QhIPDE X>c!)3PgKfL&lX^$Z>Cpu&6)6jvi^Z! diff --git a/rohd_devtools_extension/web/icons/Icon-maskable-512.png b/rohd_devtools_extension/web/icons/Icon-maskable-512.png index d69c56691fbdb0b7efa65097c7cc1edac12a6d3e..bb667c609122ee453334839b1af4b3eea08e982d 100644 GIT binary patch literal 50043 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 MPgg&ebxsLQ025IeI{*Lx From a5e936c98bff981dca0eef066706ac6e4f4c0a8e Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 8 Jul 2026 10:23:05 -0700 Subject: [PATCH 5/6] coverage_gaps are already folded into other tests, updated README for more precise definitions --- packages/rohd_hierarchy/README.md | 10 +- .../test/coverage_gaps_test.dart | 120 ------------------ 2 files changed, 7 insertions(+), 123 deletions(-) delete mode 100644 packages/rohd_hierarchy/test/coverage_gaps_test.dart diff --git a/packages/rohd_hierarchy/README.md b/packages/rohd_hierarchy/README.md index 4b03c519f..0406e0cd4 100644 --- a/packages/rohd_hierarchy/README.md +++ b/packages/rohd_hierarchy/README.md @@ -20,13 +20,17 @@ without re-transmitting structural context. A design dictionary captures the **hierarchy and connectivity** of a hardware design: -- **Occurrences** — unfolded instances of module definitions in the - hierarchy tree. Each has a `name`, an optional `definition` (the +- **Occurrences** — unfolded module instances in the hierarchy tree. + Each has a `name`, an optional `definition` (the module type), child occurrences, and signals. - **Signals** — named wires within an occurrence. Each has a `name`, `width`, optional `direction` (input/output/inout), and optional `value`. +In the occurrence view of a design, every `Logic` wire and every module +instance is unique. Both are occurrences: either signal/logic occurrences +or module occurrences. + The full "unfolded" view of a design is its **address space**: every occurrence and every signal reachable by walking the hierarchy tree. @@ -59,7 +63,7 @@ transport layer. ### Data models -- **`HierarchyOccurrence`** — An occurrence of a module definition in the +- **`HierarchyOccurrence`** — An occurrence of a module instance in the unfolded hierarchy tree, with children, signals, name, an optional `definition` (module type), and a primitive flag. Call `buildAddresses()` to assign a canonical `OccurrenceAddress` to every occurrence and signal diff --git a/packages/rohd_hierarchy/test/coverage_gaps_test.dart b/packages/rohd_hierarchy/test/coverage_gaps_test.dart deleted file mode 100644 index 1e29bcfe5..000000000 --- a/packages/rohd_hierarchy/test/coverage_gaps_test.dart +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (C) 2026 Intel Corporation -// SPDX-License-Identifier: BSD-3-Clause -// -// coverage_gaps_test.dart -// Tests for API surface not covered by other test files: -// - BaseHierarchyAdapter.root StateError on uninitialized access -// - SignalOccurrence as port -// - HierarchyOccurrence.parent -// - SignalOccurrence.value -// -// 2026 April -// Author: Desmond Kirkpatrick - -import 'package:rohd_hierarchy/rohd_hierarchy.dart'; -import 'package:test/test.dart'; - -/// Concrete subclass that does NOT set root, so we can test the -/// StateError thrown by uninitialized access. -class _UnsetAdapter extends BaseHierarchyAdapter {} - -void main() { - group('BaseHierarchyAdapter.root', () { - test('throws StateError when root is not set', () { - final adapter = _UnsetAdapter(); - expect(() => adapter.root, throwsStateError); - }); - }); - - group('SignalOccurrence as port', () { - test('creates a port signal with defaults', () { - final p = SignalOccurrence(name: 'clk', width: 1, direction: 'input'); - expect(p.name, 'clk'); - expect(p.direction, 'input'); - expect(p.width, 1); - expect(p.isPort, isTrue); - expect(p.isInput, isTrue); - }); - - test('creates a port signal with explicit overrides', () { - final p = SignalOccurrence( - name: 'data', - direction: 'output', - width: 32, - isComputed: true, - ); - HierarchyOccurrence(name: 'Top', signals: [p]).buildAddresses(); - expect(p.name, 'data'); - expect(p.width, 32); - expect(p.direction, 'output'); - expect(p.path(), 'Top/data'); - expect(p.parent!.path(), 'Top'); - expect(p.isComputed, isTrue); - expect(p.isOutput, isTrue); - }); - }); - - group('HierarchyOccurrence.parent', () { - test('parent is null for root', () { - final root = HierarchyOccurrence(name: 'Top')..buildAddresses(); - expect(root.parent, isNull); - }); - - test('parent is set for child nodes after buildAddresses', () { - final child = HierarchyOccurrence(name: 'sub'); - final root = HierarchyOccurrence(name: 'Top', children: [child]) - ..buildAddresses(); - expect(child.parent, same(root)); - expect(child.path(), 'Top/sub'); - }); - }); - - group('SignalOccurrence.value', () { - test('value is null by default', () { - final s = SignalOccurrence(name: 'a', width: 1); - expect(s.value, isNull); - }); - - test('value stores the provided runtime value', () { - final s = SignalOccurrence(name: 'a', width: 8, value: 'ff'); - expect(s.value, 'ff'); - }); - }); - - group('HierarchyOccurrence.definition', () { - test('type is null when not provided', () { - final n = HierarchyOccurrence(name: 'a'); - expect(n.definition, isNull); - }); - - test('type is stored when provided', () { - final n = HierarchyOccurrence(name: 'a', definition: 'Counter'); - expect(n.definition, 'Counter'); - }); - }); - - group('SignalOccurrence.parent', () { - test('parent is null before buildAddresses', () { - final s = SignalOccurrence(name: 'a', width: 1); - expect(s.parent, isNull); - }); - - test('parent is set after buildAddresses', () { - final s = SignalOccurrence(name: 'a', width: 1); - HierarchyOccurrence( - name: 'Top', - children: [ - HierarchyOccurrence(name: 'sub', signals: [s]) - ], - ).buildAddresses(); - expect(s.parent!.path(), 'Top/sub'); - }); - }); - - group('isPrimitive on nodes', () { - test('default isPrimitive is false', () { - final n = HierarchyOccurrence(name: 'sub'); - expect(n.isPrimitive, isFalse); - }); - }); -} From 371ba840ecaa0124a2dd923cfbaf9721815de01b Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 8 Jul 2026 11:01:18 -0700 Subject: [PATCH 6/6] constants file and resolving stale comments --- packages/rohd_hierarchy/README.md | 5 +- .../lib/src/hierarchy_constants.dart | 14 ++++ .../lib/src/hierarchy_models.dart | 1 + .../lib/src/hierarchy_occurrence.dart | 3 +- .../lib/src/hierarchy_search_result.dart | 7 +- .../lib/src/hierarchy_service.dart | 83 ++++++++++--------- .../lib/src/occurrence_address.dart | 12 +-- .../rohd_hierarchy/lib/src/prefix_query.dart | 5 +- .../rohd_hierarchy/lib/src/regex_query.dart | 5 +- .../lib/src/signal_occurrence.dart | 3 +- 10 files changed, 82 insertions(+), 56 deletions(-) create mode 100644 packages/rohd_hierarchy/lib/src/hierarchy_constants.dart diff --git a/packages/rohd_hierarchy/README.md b/packages/rohd_hierarchy/README.md index 0406e0cd4..f99e9cc88 100644 --- a/packages/rohd_hierarchy/README.md +++ b/packages/rohd_hierarchy/README.md @@ -93,7 +93,7 @@ transport layer. - **`HierarchyQuery`** — Abstract base class for pluggable search strategies. The matching logic is decoupled from tree traversal. - **`PrefixQuery`** — Prefix-substring matching. Segments split on `/` - or `.` are matched case-insensitively via `startsWith` (signals) or + or `.` are matched case-sensitively via `startsWith` (signals) or `contains` (occurrences). Created via `HierarchyQuery.prefix()`. - **`RegexQuery`** — Regex/glob matching. Each segment is compiled as a regex. Supports `*` (any chars), `?` (one char), `**` (zero or more @@ -165,8 +165,7 @@ final pathname = dict.addressToPathname(addr!); #### Prefix search (default) -Segments are split on `/` or `.` and matched as case-insensitive -substrings: +Segments are split on `/` or `.` and matched as case-sensitive substrings: ```dart // Find all signals whose path contains 'cpu' then 'clk' diff --git a/packages/rohd_hierarchy/lib/src/hierarchy_constants.dart b/packages/rohd_hierarchy/lib/src/hierarchy_constants.dart new file mode 100644 index 000000000..afb8b8722 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/hierarchy_constants.dart @@ -0,0 +1,14 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// hierarchy_constants.dart +// Shared constants for hierarchy path formatting and parsing. +// +// 2026 July +// Author: Desmond Kirkpatrick + +/// Canonical separator used in hierarchy paths returned by this package. +const String hierarchyPathSeparator = '/'; + +/// Default maximum number of results returned by hierarchy search methods. +const int defaultHierarchySearchLimit = 100; diff --git a/packages/rohd_hierarchy/lib/src/hierarchy_models.dart b/packages/rohd_hierarchy/lib/src/hierarchy_models.dart index 6d686e053..2f7cb3f76 100644 --- a/packages/rohd_hierarchy/lib/src/hierarchy_models.dart +++ b/packages/rohd_hierarchy/lib/src/hierarchy_models.dart @@ -7,6 +7,7 @@ // 2026 January // Author: Desmond Kirkpatrick +export 'hierarchy_constants.dart'; export 'hierarchy_occurrence.dart'; export 'hierarchy_search_result.dart'; export 'occurrence_address.dart'; diff --git a/packages/rohd_hierarchy/lib/src/hierarchy_occurrence.dart b/packages/rohd_hierarchy/lib/src/hierarchy_occurrence.dart index 13ddd6192..06fbf28f7 100644 --- a/packages/rohd_hierarchy/lib/src/hierarchy_occurrence.dart +++ b/packages/rohd_hierarchy/lib/src/hierarchy_occurrence.dart @@ -8,6 +8,7 @@ // Author: Desmond Kirkpatrick import 'package:meta/meta.dart'; +import 'package:rohd_hierarchy/src/hierarchy_constants.dart'; import 'package:rohd_hierarchy/src/occurrence_address.dart'; import 'package:rohd_hierarchy/src/signal_occurrence.dart'; @@ -66,7 +67,7 @@ class HierarchyOccurrence { /// /// Uses [separator] between path segments (default `/`). /// Returns just [name] for the root (no parent). - String path({String separator = '/'}) { + String path({String separator = hierarchyPathSeparator}) { if (_parent == null) { return name; } diff --git a/packages/rohd_hierarchy/lib/src/hierarchy_search_result.dart b/packages/rohd_hierarchy/lib/src/hierarchy_search_result.dart index 3f40b4c82..00bb0167c 100644 --- a/packages/rohd_hierarchy/lib/src/hierarchy_search_result.dart +++ b/packages/rohd_hierarchy/lib/src/hierarchy_search_result.dart @@ -9,6 +9,8 @@ import 'package:meta/meta.dart'; +import 'package:rohd_hierarchy/src/hierarchy_constants.dart'; + /// Base class for hierarchy search results. /// /// Holds the common fields shared by signal and occurrence search @@ -36,7 +38,7 @@ abstract class HierarchySearchResult { /// /// For `Top/counter/clk` this returns `counter/clk`. /// For a single-segment path returns the original [id]. - String get displayPath => displaySegments.join('/'); + String get displayPath => displaySegments.join(hierarchyPathSeparator); /// Path segments with the top-level module name stripped. /// @@ -46,7 +48,8 @@ abstract class HierarchySearchResult { /// Normalize a user query for hierarchy search. /// /// Converts common separators (`.`) to the canonical `/` separator. - static String normalizeQuery(String query) => query.replaceAll('.', '/'); + static String normalizeQuery(String query) => + query.replaceAll('.', hierarchyPathSeparator); @override bool operator ==(Object other) => diff --git a/packages/rohd_hierarchy/lib/src/hierarchy_service.dart b/packages/rohd_hierarchy/lib/src/hierarchy_service.dart index 08c2add90..791293429 100644 --- a/packages/rohd_hierarchy/lib/src/hierarchy_service.dart +++ b/packages/rohd_hierarchy/lib/src/hierarchy_service.dart @@ -9,15 +9,13 @@ import 'package:rohd_hierarchy/src/hierarchy_models.dart'; -/// Default path separator used when constructing paths from the tree. -const String _hierarchySeparator = '/'; - /// A source-agnostic interface for navigating hardware hierarchy. /// /// All search and navigation is driven by walking the [HierarchyOccurrence] /// tree. Occurrences hold their [HierarchyOccurrence.name], /// [HierarchyOccurrence.children], and [HierarchyOccurrence.signals]. Full -/// paths are constructed on the fly by joining names with [_hierarchySeparator] +/// paths are constructed on the fly by joining names with +/// [hierarchyPathSeparator] /// — no pre-baked path strings are needed for search. /// /// Key methods: @@ -30,10 +28,6 @@ abstract mixin class HierarchyService { /// The root occurrence for the hierarchy. HierarchyOccurrence get root; - /// Maximum number of results returned by search methods when no explicit - /// `limit` is provided. - static const int _defaultSearchLimit = 100; - // ───────────── Address-based occurrence/signal lookup ──────────────── /// Find an occurrence by its [OccurrenceAddress]. O(depth). @@ -121,16 +115,19 @@ abstract mixin class HierarchyService { final sigIdx = indices.last; return (sigIdx >= 0 && sigIdx < walked.node.signals.length) ? [...walked.parts, walked.node.signals[sigIdx].name] - .join(_hierarchySeparator) + .join(hierarchyPathSeparator) : null; } - return walked.parts.join(_hierarchySeparator); + return walked.parts.join(hierarchyPathSeparator); } - /// Resolve a waveform-style ID (dot-separated, e.g. `"dut.adder.clk"`) - /// to a [OccurrenceAddress]. + /// Resolve a signal identifier from a VCD or FST waveform file to an + /// [OccurrenceAddress]. /// - /// Normalises `.` → `/` then delegates to [pathnameToAddress]. + /// VCD/FST waveform readers often expose hierarchy names as dot-separated + /// strings such as `"dut.adder.clk"`. This delegates to + /// [pathnameToAddress], which accepts both dot-separated waveform IDs and + /// slash-separated hierarchy paths. OccurrenceAddress? waveformIdToAddress(String waveformId) => pathnameToAddress(waveformId); @@ -144,7 +141,7 @@ abstract mixin class HierarchyService { /// /// Returns up to [limit] results. List searchSignalPaths(String query, {int? limit}) { - final effectiveLimit = limit ?? _defaultSearchLimit; + final effectiveLimit = limit ?? defaultHierarchySearchLimit; if (query.trim().isEmpty) { return const []; } @@ -182,9 +179,9 @@ abstract mixin class HierarchyService { return true; } - final normalizedQuery = searchTerm.replaceAll('.', '/'); + final normalizedQuery = searchTerm.replaceAll('.', hierarchyPathSeparator); final queryParts = normalizedQuery - .split('/') + .split(hierarchyPathSeparator) .map((s) => s.trim()) .where((s) => s.isNotEmpty) .toList(); @@ -218,7 +215,7 @@ abstract mixin class HierarchyService { /// contains glob or regex metacharacters (`*`, `?`, `[`, `(`, `|`, /// `+`). Otherwise uses [searchSignalPaths] for prefix-based matching. List searchSignals(String query, {int? limit}) { - final effectiveLimit = limit ?? _defaultSearchLimit; + final effectiveLimit = limit ?? defaultHierarchySearchLimit; if (hasRegexChars(query)) { final pattern = (query.startsWith('**/') || query.startsWith('*/')) ? query @@ -237,7 +234,7 @@ abstract mixin class HierarchyService { /// /// Returns up to [limit] results. List searchOccurrencePaths(String query, {int? limit}) { - final effectiveLimit = limit ?? _defaultSearchLimit; + final effectiveLimit = limit ?? defaultHierarchySearchLimit; if (query.trim().isEmpty) { return const []; } @@ -253,7 +250,7 @@ abstract mixin class HierarchyService { /// Like [searchOccurrencePaths] but returns the [HierarchyOccurrence] objects /// themselves instead of path strings. List matchOccurrences(String query, {int? limit}) { - final effectiveLimit = limit ?? _defaultSearchLimit; + final effectiveLimit = limit ?? defaultHierarchySearchLimit; if (query.trim().isEmpty) { return const []; } @@ -270,9 +267,9 @@ abstract mixin class HierarchyService { /// filter on children at that level. Returns up to [limit] full paths /// (with `/` appended for nodes that have children). List autocompletePaths(String partialPath, {int? limit}) { - final effectiveLimit = limit ?? _defaultSearchLimit; - final normalized = partialPath.replaceAll('.', _hierarchySeparator); - final endsWithSep = normalized.endsWith(_hierarchySeparator); + final effectiveLimit = limit ?? defaultHierarchySearchLimit; + final normalized = partialPath.replaceAll('.', hierarchyPathSeparator); + final endsWithSep = normalized.endsWith(hierarchyPathSeparator); final parts = _splitPath(partialPath); // Navigate to the deepest complete segment. @@ -310,15 +307,16 @@ abstract mixin class HierarchyService { current.name.startsWith(prefix)) { final rootPath = current.name; suggestions.add(current.children.isNotEmpty - ? '$rootPath$_hierarchySeparator' + ? '$rootPath$hierarchyPathSeparator' : rootPath); } for (final child in current.children) { if (prefix.isEmpty || child.name.startsWith(prefix)) { - final path = [...completedParts, child.name].join(_hierarchySeparator); + final pathParts = [...completedParts, child.name]; + final path = pathParts.join(hierarchyPathSeparator); suggestions.add( - child.children.isNotEmpty ? '$path$_hierarchySeparator' : path); + child.children.isNotEmpty ? '$path$hierarchyPathSeparator' : path); if (suggestions.length >= effectiveLimit) { break; } @@ -334,7 +332,7 @@ abstract mixin class HierarchyService { /// contains glob or regex metacharacters (`*`, `?`, `[`, `(`, `|`, /// `+`). Otherwise uses [searchOccurrencePaths] for prefix-based matching. List searchOccurrences(String query, {int? limit}) { - final effectiveLimit = limit ?? _defaultSearchLimit; + final effectiveLimit = limit ?? defaultHierarchySearchLimit; if (hasRegexChars(query)) { final pattern = (query.startsWith('**/') || query.startsWith('*/')) ? query @@ -373,7 +371,7 @@ abstract mixin class HierarchyService { /// 'Top/CPU/d[0-9]+' — signals like d0, d1, d12 in Top/CPU /// ``` List searchSignalPathsRegex(String pattern, {int? limit}) { - final effectiveLimit = limit ?? _defaultSearchLimit; + final effectiveLimit = limit ?? defaultHierarchySearchLimit; if (pattern.trim().isEmpty) { return const []; } @@ -396,7 +394,7 @@ abstract mixin class HierarchyService { /// /// Returns up to [limit] full hierarchical occurrence paths. List searchOccurrencePathsRegex(String pattern, {int? limit}) { - final effectiveLimit = limit ?? _defaultSearchLimit; + final effectiveLimit = limit ?? defaultHierarchySearchLimit; if (pattern.trim().isEmpty) { return const []; } @@ -439,8 +437,8 @@ abstract mixin class HierarchyService { /// Split a query or path on `/` or `.` into non-empty segments. static List _splitPath(String input) => input - .replaceAll('.', _hierarchySeparator) - .split(_hierarchySeparator) + .replaceAll('.', hierarchyPathSeparator) + .split(hierarchyPathSeparator) .map((s) => s.trim()) .where((s) => s.isNotEmpty) .toList(); @@ -450,8 +448,8 @@ abstract mixin class HierarchyService { /// Use this when the result is for display or building [SignalSearchResult] /// path parts — not for matching. static List _splitPathPreserveCase(String input) => input - .replaceAll('.', _hierarchySeparator) - .split(_hierarchySeparator) + .replaceAll('.', hierarchyPathSeparator) + .split(hierarchyPathSeparator) .where((s) => s.isNotEmpty) .toList(); @@ -522,7 +520,7 @@ abstract mixin class HierarchyService { } if (signalQuery.isEmpty || signal.name.startsWith(signalQuery)) { final fullPath = - [...pathSoFar, signal.name].join(_hierarchySeparator); + [...pathSoFar, signal.name].join(hierarchyPathSeparator); results.add(fullPath); } } @@ -569,7 +567,7 @@ abstract mixin class HierarchyService { // If all query parts are matched, this node is a result if (nextIdx >= queryParts.length) { - final fullPath = pathSoFar.join(_hierarchySeparator); + final fullPath = pathSoFar.join(hierarchyPathSeparator); results.add(fullPath); if (results.length >= limit) { return; @@ -634,8 +632,11 @@ abstract mixin class HierarchyService { /// Unlike [_splitPath] (which also splits on `.`), regex patterns use only /// `/` as the hierarchy separator because `.` has meaning inside regular /// expressions (e.g. `.*`, `a.b`). - List _splitRegexPattern(String input) => - input.split('/').map((s) => s.trim()).where((s) => s.isNotEmpty).toList(); + List _splitRegexPattern(String input) => input + .split(hierarchyPathSeparator) + .map((s) => s.trim()) + .where((s) => s.isNotEmpty) + .toList(); /// Convert glob-style `*` and `?` wildcards to regex equivalents. /// @@ -720,7 +721,9 @@ abstract mixin class HierarchyService { if (results.length >= limit) { return; } - results.add([...pathSoFar, signal.name].join(_hierarchySeparator)); + final fullPath = + [...pathSoFar, signal.name].join(hierarchyPathSeparator); + results.add(fullPath); } } else { // sigIdx points to a non-** regex that should match signal names. @@ -733,8 +736,8 @@ abstract mixin class HierarchyService { return; } if (sigSeg.regex!.hasMatch(signal.name)) { - results - .add([...pathSoFar, signal.name].join(_hierarchySeparator)); + results.add( + [...pathSoFar, signal.name].join(hierarchyPathSeparator)); } } } @@ -780,7 +783,7 @@ abstract mixin class HierarchyService { // All segments consumed (or only trailing **'s remain) → match. if (_allGlobStarAfter(segments, nextIdx)) { - results.add(pathSoFar.join(_hierarchySeparator)); + results.add(pathSoFar.join(hierarchyPathSeparator)); if (results.length >= limit) { return; } diff --git a/packages/rohd_hierarchy/lib/src/occurrence_address.dart b/packages/rohd_hierarchy/lib/src/occurrence_address.dart index b8a9c8bba..06f059c88 100644 --- a/packages/rohd_hierarchy/lib/src/occurrence_address.dart +++ b/packages/rohd_hierarchy/lib/src/occurrence_address.dart @@ -10,6 +10,7 @@ import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; +import 'package:rohd_hierarchy/src/hierarchy_constants.dart'; import 'package:rohd_hierarchy/src/hierarchy_occurrence.dart'; /// Efficient hierarchical address using indices instead of strings. @@ -86,9 +87,10 @@ class OccurrenceAddress { /// Resolve a pathname string (e.g. `"Top/counter/clk"` or /// `"Top.counter.clk"`) to a [OccurrenceAddress] by walking [root]. /// - /// Supports both `/` and `.` as separators. If the first segment - /// matches [root]'s name, it is skipped — the root - /// occurrence is always at the empty address. + /// Supports both `/` hierarchy paths and dot-separated signal identifiers + /// commonly produced by VCD/FST waveform files. If the first segment matches + /// [root]'s name, it is skipped — the root occurrence is always at the empty + /// address. /// /// The last segment is first tried as a **signal** name within the /// current occurrence; if that fails it is tried as a **child** @@ -110,8 +112,8 @@ class OccurrenceAddress { ) { final rootAddr = root.address ?? OccurrenceAddress.root; final parts = pathname - .replaceAll('.', '/') - .split('/') + .replaceAll('.', hierarchyPathSeparator) + .split(hierarchyPathSeparator) .where((s) => s.isNotEmpty) .toList(); diff --git a/packages/rohd_hierarchy/lib/src/prefix_query.dart b/packages/rohd_hierarchy/lib/src/prefix_query.dart index 2ce617683..4f12b7ad7 100644 --- a/packages/rohd_hierarchy/lib/src/prefix_query.dart +++ b/packages/rohd_hierarchy/lib/src/prefix_query.dart @@ -7,6 +7,7 @@ // 2026 May // Author: Desmond Kirkpatrick +import 'package:rohd_hierarchy/src/hierarchy_constants.dart'; import 'package:rohd_hierarchy/src/hierarchy_query.dart'; /// Prefix-substring query: segments are matched via `startsWith` (signals) @@ -21,8 +22,8 @@ class PrefixQuery extends HierarchyQuery { super.target = SearchTarget.signals, }) : super(crossesBoundaries: false) { segments = rawQuery - .replaceAll('.', '/') - .split('/') + .replaceAll('.', hierarchyPathSeparator) + .split(hierarchyPathSeparator) .map((s) => s.trim()) .where((s) => s.isNotEmpty) .toList(); diff --git a/packages/rohd_hierarchy/lib/src/regex_query.dart b/packages/rohd_hierarchy/lib/src/regex_query.dart index 6c0ebfe41..4b367b7e6 100644 --- a/packages/rohd_hierarchy/lib/src/regex_query.dart +++ b/packages/rohd_hierarchy/lib/src/regex_query.dart @@ -7,6 +7,7 @@ // 2026 May // Author: Desmond Kirkpatrick +import 'package:rohd_hierarchy/src/hierarchy_constants.dart'; import 'package:rohd_hierarchy/src/hierarchy_query.dart'; /// Regex/glob query: each segment is a compiled regex, with `**` support @@ -15,7 +16,7 @@ import 'package:rohd_hierarchy/src/hierarchy_query.dart'; /// ## Segment syntax /// /// The query string is split on `/` into segments. Each segment is -/// independently compiled as a case-insensitive [RegExp] anchored to the +/// independently compiled as a case-sensitive [RegExp] anchored to the /// full occurrence or signal name (`^…$`). This means: /// /// - **Plain names** match exactly: `Top/CPU/clk`. @@ -76,7 +77,7 @@ class RegexQuery extends HierarchyQuery { super.target = SearchTarget.signals, }) : super(crossesBoundaries: false) { final parts = rawQuery - .split('/') + .split(hierarchyPathSeparator) .map((s) => s.trim()) .where((s) => s.isNotEmpty) .toList(); diff --git a/packages/rohd_hierarchy/lib/src/signal_occurrence.dart b/packages/rohd_hierarchy/lib/src/signal_occurrence.dart index d780cff8d..3fb57772f 100644 --- a/packages/rohd_hierarchy/lib/src/signal_occurrence.dart +++ b/packages/rohd_hierarchy/lib/src/signal_occurrence.dart @@ -8,6 +8,7 @@ // Author: Desmond Kirkpatrick import 'package:meta/meta.dart'; +import 'package:rohd_hierarchy/src/hierarchy_constants.dart'; import 'package:rohd_hierarchy/src/hierarchy_occurrence.dart'; import 'package:rohd_hierarchy/src/occurrence_address.dart'; @@ -249,7 +250,7 @@ class SignalOccurrence { /// Joins the parent occurrence's path with this signal's [name] using /// [separator]. Falls back to just [name] if parent is not yet set /// (e.g. in test fixtures before `buildAddresses`). - String path({String separator = '/'}) { + String path({String separator = hierarchyPathSeparator}) { if (_parent == null) { return name; }