From 5885e689b6e846b208f6d3fbf565c31774c06a7b Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Tue, 25 Aug 2026 15:25:52 -0700 Subject: [PATCH 1/3] Add shared signal value format registry Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../lib/src/hierarchy_models.dart | 1 + .../lib/src/occurrence_trie.dart | 115 ++++++++ .../test/occurrence_trie_test.dart | 54 ++++ .../packages/rohd_devtools_widgets/README.md | 42 +++ .../lib/rohd_devtools_widgets.dart | 3 + .../lib/src/signal_value_format_registry.dart | 271 ++++++++++++++++++ .../rohd_devtools_widgets/pubspec.yaml | 2 + .../signal_value_format_registry_test.dart | 224 +++++++++++++++ 8 files changed, 712 insertions(+) create mode 100644 packages/rohd_hierarchy/lib/src/occurrence_trie.dart create mode 100644 packages/rohd_hierarchy/test/occurrence_trie_test.dart create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart create mode 100644 rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart diff --git a/packages/rohd_hierarchy/lib/src/hierarchy_models.dart b/packages/rohd_hierarchy/lib/src/hierarchy_models.dart index 2f7cb3f76..dc79dbb6c 100644 --- a/packages/rohd_hierarchy/lib/src/hierarchy_models.dart +++ b/packages/rohd_hierarchy/lib/src/hierarchy_models.dart @@ -12,5 +12,6 @@ export 'hierarchy_occurrence.dart'; export 'hierarchy_search_result.dart'; export 'occurrence_address.dart'; export 'occurrence_search_result.dart'; +export 'occurrence_trie.dart'; export 'signal_occurrence.dart'; export 'signal_search_result.dart'; diff --git a/packages/rohd_hierarchy/lib/src/occurrence_trie.dart b/packages/rohd_hierarchy/lib/src/occurrence_trie.dart new file mode 100644 index 000000000..938c96f30 --- /dev/null +++ b/packages/rohd_hierarchy/lib/src/occurrence_trie.dart @@ -0,0 +1,115 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// occurrence_trie.dart +// Compact storage for values keyed by hierarchy occurrence addresses. +// +// 2026 August +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/src/occurrence_address.dart'; + +/// A prefix-sharing map from [OccurrenceAddress] values to values of type [T]. +/// +/// Common address prefixes are stored once, making this more compact than a +/// conventional map when many values belong to the same hierarchy subtree. +class OccurrenceTrie { + /// The root node, which represents [OccurrenceAddress.root]. + final _OccurrenceTrieNode _root = _OccurrenceTrieNode(); + + /// Whether this trie contains no values. + bool get isEmpty => _root.isEmpty; + + /// The value stored at [address], if any. + T? operator [](OccurrenceAddress address) { + var node = _root; + for (final index in _validatedPath(address)) { + final child = node.children[index]; + if (child == null) { + return null; + } + node = child; + } + return node.value; + } + + /// Associates [value] with [address]. + /// + /// This is equivalent to [set], without returning the previous value. + void operator []=(OccurrenceAddress address, T value) { + set(address, value); + } + + /// Associates [value] with [address]. + /// + /// Returns the value previously stored at [address], if any. + T? set(OccurrenceAddress address, T value) { + var node = _root; + for (final index in _validatedPath(address)) { + node = node.children.putIfAbsent(index, _OccurrenceTrieNode.new); + } + final previous = node.value; + node.value = value; + return previous; + } + + /// Removes and returns the value stored at [address], if any. + T? remove(OccurrenceAddress address) { + final path = _validatedPath(address); + final nodes = <_OccurrenceTrieNode>[_root]; + var node = _root; + for (final index in path) { + final child = node.children[index]; + if (child == null) { + return null; + } + nodes.add(child); + node = child; + } + + final previous = node.value; + if (previous == null) { + return null; + } + node.value = null; + for (var index = path.length - 1; index >= 0; index--) { + final child = nodes[index + 1]; + if (!child.isEmpty) { + break; + } + nodes[index].children.remove(path[index]); + } + return previous; + } + + /// Removes every value from this trie. + void clear() { + _root + ..value = null + ..children.clear(); + } + + /// Returns [address]'s valid, non-negative path. + static List _validatedPath(OccurrenceAddress address) { + if (address.path.any((index) => index < 0)) { + throw ArgumentError.value( + address, + 'address', + 'An occurrence address must contain non-negative indices.', + ); + } + return address.path; + } +} + +/// A node in an [OccurrenceTrie]. +class _OccurrenceTrieNode { + /// Descendants indexed by their address path component. + final Map> children = {}; + + /// The value stored at this node, if one has been assigned. + T? value; + + /// Whether this node has neither a value nor descendants. + bool get isEmpty => value == null && children.isEmpty; +} diff --git a/packages/rohd_hierarchy/test/occurrence_trie_test.dart b/packages/rohd_hierarchy/test/occurrence_trie_test.dart new file mode 100644 index 000000000..3faa8a92a --- /dev/null +++ b/packages/rohd_hierarchy/test/occurrence_trie_test.dart @@ -0,0 +1,54 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// occurrence_trie_test.dart +// Tests for compact occurrence-address trie storage. +// +// 2026 August +// Author: Desmond Kirkpatrick + +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; +import 'package:test/test.dart'; + +void main() { + test('stores values with shared occurrence-address prefixes', () { + final trie = OccurrenceTrie(); + const first = OccurrenceAddress([0, 2, 4]); + const second = OccurrenceAddress([0, 2, 5]); + + expect(trie.set(first, 'first'), isNull); + trie[OccurrenceAddress.root] = 'root'; + expect(trie.set(second, 'second'), isNull); + + expect(trie[OccurrenceAddress.root], 'root'); + expect(trie[first], 'first'); + expect(trie[second], 'second'); + expect(trie[const OccurrenceAddress([0, 2, 6])], isNull); + }); + + test('prunes an address branch after removing its final value', () { + final trie = OccurrenceTrie(); + const first = OccurrenceAddress([0, 2, 4]); + const second = OccurrenceAddress([0, 2, 5]); + trie + ..set(first, 'first') + ..set(second, 'second'); + + expect(trie.remove(first), 'first'); + expect(trie[first], isNull); + expect(trie[second], 'second'); + expect(trie.remove(second), 'second'); + expect(trie.isEmpty, isTrue); + }); + + test('accepts root addresses and rejects negative path indices', () { + final trie = OccurrenceTrie(); + + trie[OccurrenceAddress.root] = 'root'; + expect(trie[OccurrenceAddress.root], 'root'); + expect( + () => trie[const OccurrenceAddress([0, -1])], + throwsArgumentError, + ); + }); +} diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/README.md b/rohd_devtools_extension/packages/rohd_devtools_widgets/README.md index e40b328bb..fd2abee35 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/README.md +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/README.md @@ -26,6 +26,48 @@ across DevTools packages. - ROHD extension client/status abstractions: `RohdExtensionClient`, `NullExtensionClient`, `RohdModuleInfo`, and `RohdFormatInfo`. +## Widgets & Utilities + +### UI Controls & Buttons + +- **`MarkdownHelpButton`** — A help button that displays Markdown content from an asset file in a dialog. Supports tooltip text and rich formatting. + +- **`ExportPngButton`** — A camera icon button for triggering PNG export functionality. Includes customizable tooltip text. + +- **`CrossProbeButton`** — A toolbar button for toggling cross-probing between viewers. Shows a bidirectional arrows icon that reflects the active/inactive state. + +### Overlays & Layout + +- **`AppBarOverlay`** — An auto-hiding AppBar that slides in from the top edge when the mouse approaches. When disabled, behaves like a standard AppBar. + +### Export & Capture + +- **`captureBoundaryToPng`** — Captures a `RepaintBoundary` as PNG and saves or downloads it. + +- **`showExportToast`** — Shows export feedback and status messages. + +### Cross-Probing + +- **`CrossProbeService`** — Service for managing cross-probe state between multiple viewers/debuggers. Handles bidirectional signal selection synchronization. + +- **`buildGotoSourceMenuItems`** — Builds source-navigation menu items for ROHD DevTools surfaces. + +### Signal & Bit Field Utilities + +- **`expandLogicType`**, **`formatFieldValue`**, and **`formatTypeTooltip`** — Format ROHD logic types and values for display. + +- **`BitFieldDef`**, **`showBitRangeDialog`**, and **`showDefineBitFieldsDialog`** — Define and edit bit-field ranges. + +- **`buildBitExpansionMenuItems`** and **`resolveBitExpansionMenuValue`** — Build and resolve the "Expand Bits" and "Define Bit Fields" actions used across signal selection overlays and panels. + +- **`SignalValueFormatRegistry`** — Shared registry for signal display-format preferences, allowing consistent formatting across multiple viewers. + +### Extension Integration + +- **`RohdExtensionClient`** — Abstract interface for querying the ROHD VS Code extension. Supports multiple implementations (DevTools, VS Code webview, offline mode). + +- **`RohdSourceFormat`** and **`RohdFormatInfo`** — Describe source formats and their availability. + ## Usage Add this package as a path dependency from a ROHD DevTools package and import the shared widgets you need: diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart index 452567fae..ca31b9ace 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/rohd_devtools_widgets.dart @@ -36,6 +36,9 @@ export 'src/bit_field_utils.dart'; // Shared "Expand Bits" / "Define Bit Fields" popup-menu helpers export 'src/bit_expansion_menu.dart'; +// Shared signal display-format preferences and value formatting +export 'src/signal_value_format_registry.dart'; + // ROHD extension client export 'src/rohd_extension_status.dart'; export 'src/rohd_extension_client.dart'; diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart new file mode 100644 index 000000000..5d224154a --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart @@ -0,0 +1,271 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// signal_value_format_registry.dart +// Shared signal display-format preferences and value formatting. +// +// 2026 August +// Author: Desmond Kirkpatrick + +import 'package:flutter/foundation.dart'; +import 'package:rohd/rohd.dart' + show LogicValue, LogicValueConstructionException; +import 'package:rohd_hierarchy/rohd_hierarchy.dart' + show OccurrenceAddress, OccurrenceTrie; + +/// The available display formats for signal values. +enum SignalValueFormat { + /// The source waveform representation. + waveform, + + /// A binary representation. + binary, + + /// A hexadecimal representation. + hexadecimal, + + /// An unsigned decimal representation. + unsignedDecimal, + + /// A two's-complement signed decimal representation. + signedDecimal, + + /// An octal representation. + octal, + + /// An ASCII representation. + ascii, +} + +/// A format preference for one signal occurrence address. +class SignalValueFormatPreference { + /// Creates a preference for [address] using [format]. + SignalValueFormatPreference( + this.address, + this.format, + ); + + /// The occurrence address, including the signal index. + final OccurrenceAddress address; + + /// The selected display format. + final SignalValueFormat format; +} + +/// Shared display-format preferences keyed by occurrence address. +/// +/// Viewer packages publish [SignalValueFormat] values; embedded surfaces use +/// the same values without depending on viewer-local format enums. +class SignalValueFormatRegistry { + /// Prevents instantiation. + SignalValueFormatRegistry._(); + + /// Stores the registered preference for each signal occurrence. + static OccurrenceTrie _formatTrie = + OccurrenceTrie(); + + /// Tracks registry changes. + static final ValueNotifier _changes = ValueNotifier(0); + + /// Notifies listeners whenever occurrence-format preferences change. + static ValueListenable get changes => _changes; + + /// Replaces all occurrence-format preferences with [preferences]. + static void update(Iterable preferences) { + final replacement = OccurrenceTrie(); + for (final preference in preferences) { + _validateSignalAddress(preference.address); + replacement[preference.address] = preference.format; + } + _formatTrie = replacement; + _notifyListeners(); + } + + /// Removes all occurrence-format preferences. + static void clear() { + if (_formatTrie.isEmpty) { + return; + } + _formatTrie.clear(); + _notifyListeners(); + } + + /// Sets [format] for each signal occurrence in [addresses]. + static void setFormatFor( + Iterable addresses, + SignalValueFormat format, + ) { + final requestedAddresses = addresses.toList(growable: false); + for (final address in requestedAddresses) { + _validateSignalAddress(address); + } + + var changed = false; + for (final address in requestedAddresses) { + changed = (_formatTrie.set(address, format) != format) || changed; + } + if (changed) { + _notifyListeners(); + } + } + + /// Converts a serialized format name to its corresponding enum value. + /// + /// Returns `null` when [value] is not a known format name. + static SignalValueFormat? formatFromString(String value) { + for (final format in SignalValueFormat.values) { + if (format.name == value) { + return format; + } + } + return null; + } + + /// Converts [format] to its serialized format name. + static String formatToString(SignalValueFormat format) => format.name; + + /// Returns the requested format for [address], or the waveform default. + static SignalValueFormat formatFor(OccurrenceAddress address) { + return formatForAny([address]); + } + + /// Returns the first registered format matching [addresses]. + static SignalValueFormat formatForAny( + Iterable addresses, { + SignalValueFormat fallback = SignalValueFormat.waveform, + }) { + for (final address in addresses) { + if (address == null) { + continue; + } + _validateSignalAddress(address); + final format = _formatTrie[address]; + if (format != null) { + return format; + } + } + return fallback; + } + + /// Increments the registry change generation after a successful mutation. + static void _notifyListeners() => _changes.value++; + + /// Ensures [address] identifies a signal rather than an occurrence. + static void _validateSignalAddress(OccurrenceAddress address) { + if (address.path.isEmpty) { + throw ArgumentError.value( + address, + 'address', + 'A signal occurrence address must not be empty.', + ); + } + } + + /// Returns whether [value] contains unknown (`x` or `z`) digits. + static bool _containsUnknownDigits(String value) { + final lower = value.toLowerCase(); + final apostrophe = lower.indexOf("'"); + final digits = apostrophe > 0 && apostrophe + 2 <= lower.length + ? lower.substring(apostrophe + 2) + : lower.startsWith('0x') || lower.startsWith('0b') + ? lower.substring(2) + : lower; + return digits.contains('x') || digits.contains('z'); + } + + /// Formats a ROHD radix literal according to [format]. + static String formatValue( + String value, + SignalValueFormat format, + int width, + ) { + final waveformValue = _waveformValue(value, width); + if (waveformValue == null) { + return _canonicalWaveformValue(value, width); + } + final (logicValue, canonical) = waveformValue; + if (format == SignalValueFormat.waveform || + _containsUnknownDigits(canonical)) { + return canonical; + } + return switch (format) { + SignalValueFormat.binary => logicValue.toRadixString( + leadingZeros: true, + includeWidth: false, + sepChar: '', + ), + SignalValueFormat.hexadecimal => + logicValue.toRadixString(radix: 16, sepChar: ''), + SignalValueFormat.unsignedDecimal => + logicValue.toRadixString(radix: 10, includeWidth: false, sepChar: ''), + SignalValueFormat.signedDecimal => + logicValue.toBigInt().toSigned(logicValue.width).toString(), + SignalValueFormat.octal => + '0o${logicValue.toRadixString(radix: 8, includeWidth: false, sepChar: '')}', + SignalValueFormat.ascii => () { + final byteCount = (logicValue.width + 7) ~/ 8; + return String.fromCharCodes( + List.generate( + byteCount, + (index) { + final shift = (byteCount - index - 1) * 8; + final code = + ((logicValue.toBigInt() >> shift) & BigInt.from(0xff)) + .toInt(); + return code >= 0x20 && code <= 0x7e ? code : 0x2e; + }, + ), + ); + }(), + SignalValueFormat.waveform => canonical, + }; + } + + /// Parses [value] as a waveform literal and returns it with its canonical + /// waveform representation. + static (LogicValue, String)? _waveformValue(String value, int width) { + final trimmed = value.trim().replaceAll('\u0000', ''); + if (trimmed.isEmpty || _containsUnknownDigits(trimmed)) { + return null; + } + final lower = trimmed.toLowerCase(); + final displayWidth = width > 0 ? width : 1; + final isRadixLiteral = RegExp(r"^\d+'[bqodh]").hasMatch(lower); + final digits = lower.startsWith('0x') || lower.startsWith('0b') + ? lower.substring(2) + : lower; + final radix = lower.startsWith('0x') + ? 'h' + : lower.startsWith('0b') + ? 'b' + : isRadixLiteral + ? lower[lower.indexOf("'") + 1] + : digits.codeUnits.every( + (codeUnit) => codeUnit == 0x30 || codeUnit == 0x31, + ) + ? 'b' + : digits.codeUnits.any( + (codeUnit) => + (codeUnit >= 0x61 && codeUnit <= 0x66) || + (codeUnit >= 0x41 && codeUnit <= 0x46), + ) + ? 'h' + : 'd'; + final radixLiteral = isRadixLiteral ? lower : "$displayWidth'$radix$digits"; + try { + final logicValue = LogicValue.ofRadixString(radixLiteral); + return ( + logicValue, + isRadixLiteral ? trimmed : logicValue.toString(), + ); + } on LogicValueConstructionException { + return null; + } + } + + /// Returns [value] in its canonical waveform representation when possible. + static String _canonicalWaveformValue(String value, int width) { + final waveformValue = _waveformValue(value, width); + return waveformValue?.$2 ?? value.trim().replaceAll('\u0000', ''); + } +} diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/pubspec.yaml b/rohd_devtools_extension/packages/rohd_devtools_widgets/pubspec.yaml index dcb1b7a95..3f24d6f92 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/pubspec.yaml +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/pubspec.yaml @@ -8,6 +8,8 @@ environment: dependencies: flutter: {sdk: flutter} rohd: ^0.6.9 + rohd_hierarchy: + path: ../../../packages/rohd_hierarchy web: ^1.0.0 dev_dependencies: flutter_test: {sdk: flutter} diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart new file mode 100644 index 000000000..187f7a223 --- /dev/null +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart @@ -0,0 +1,224 @@ +// Copyright (C) 2026 Intel Corporation +// SPDX-License-Identifier: BSD-3-Clause +// +// signal_value_format_registry_test.dart +// Tests for shared signal display-format preferences and value formatting. +// +// 2026 August +// Author: Desmond Kirkpatrick + +import 'package:flutter_test/flutter_test.dart'; +import 'package:rohd_devtools_widgets/rohd_devtools_widgets.dart'; +import 'package:rohd_hierarchy/rohd_hierarchy.dart'; + +void main() { + tearDown(SignalValueFormatRegistry.clear); + + test('formats bare binary and hexadecimal waveform values', () { + expect( + SignalValueFormatRegistry.formatValue( + '0000', + SignalValueFormat.waveform, + 4, + ), + "4'h0", + ); + expect( + SignalValueFormatRegistry.formatValue( + '11111111', + SignalValueFormat.signedDecimal, + 8, + ), + '-1', + ); + expect( + SignalValueFormatRegistry.formatValue( + '11111111', + SignalValueFormat.hexadecimal, + 8, + ), + "8'hff", + ); + expect( + SignalValueFormatRegistry.formatValue( + 'ff', + SignalValueFormat.unsignedDecimal, + 8, + ), + '255', + ); + expect( + SignalValueFormatRegistry.formatValue( + '0x0', + SignalValueFormat.unsignedDecimal, + 4, + ), + '0', + ); + }); + + test('uses ROHD radix literals for typed format conversions', () { + expect( + SignalValueFormatRegistry.formatValue( + "8'd255", + SignalValueFormat.hexadecimal, + 8, + ), + "8'hff", + ); + expect( + SignalValueFormatRegistry.formatValue( + '1010', + SignalValueFormat.octal, + 4, + ), + '0o12', + ); + expect( + SignalValueFormatRegistry.formatValue( + '0x4142', + SignalValueFormat.ascii, + 16, + ), + 'AB', + ); + expect( + SignalValueFormatRegistry.formatValue( + '0x${List.filled(33, '41').join()}', + SignalValueFormat.ascii, + 264, + ), + List.filled(33, 'A').join(), + ); + }); + + test('looks up an occurrence address from the format trie', () { + SignalValueFormatRegistry.setFormatFor( + [ + const OccurrenceAddress([0, 2, 4]), + ], + SignalValueFormat.signedDecimal, + ); + + expect( + SignalValueFormatRegistry.formatFor(const OccurrenceAddress([0, 2, 4])), + SignalValueFormat.signedDecimal, + ); + }); + + test('looks up a fallback occurrence address', () { + SignalValueFormatRegistry.setFormatFor( + [ + const OccurrenceAddress([0, 2, 4]), + ], + SignalValueFormat.unsignedDecimal, + ); + + expect( + SignalValueFormatRegistry.formatForAny([ + const OccurrenceAddress([7, 8, 9]), + const OccurrenceAddress([0, 2, 4]), + ]), + SignalValueFormat.unsignedDecimal, + ); + }); + + test('stores shared address prefixes once in the format trie', () { + SignalValueFormatRegistry.update([ + SignalValueFormatPreference( + const OccurrenceAddress([0, 2, 4]), + SignalValueFormat.unsignedDecimal, + ), + SignalValueFormatPreference( + const OccurrenceAddress([0, 2, 5]), + SignalValueFormat.signedDecimal, + ), + ]); + + expect( + SignalValueFormatRegistry.formatFor(const OccurrenceAddress([0, 2, 4])), + SignalValueFormat.unsignedDecimal, + ); + expect( + SignalValueFormatRegistry.formatFor(const OccurrenceAddress([0, 2, 5])), + SignalValueFormat.signedDecimal, + ); + expect( + SignalValueFormatRegistry.formatFor(const OccurrenceAddress([0, 2, 6])), + SignalValueFormat.waveform, + ); + }); + + test('rejects an invalid signal occurrence address', () { + expect( + () => SignalValueFormatRegistry.setFormatFor( + const [OccurrenceAddress([])], + SignalValueFormat.unsignedDecimal, + ), + throwsArgumentError, + ); + expect( + () => SignalValueFormatRegistry.formatFor( + const OccurrenceAddress([0, -1]), + ), + throwsArgumentError, + ); + }); + + test('preserves registry state when a preference batch is invalid', () { + const address = OccurrenceAddress([0, 2, 4]); + SignalValueFormatRegistry.setFormatFor( + [address], + SignalValueFormat.unsignedDecimal, + ); + final changesBefore = SignalValueFormatRegistry.changes.value; + + expect( + () => SignalValueFormatRegistry.update([ + SignalValueFormatPreference(address, SignalValueFormat.signedDecimal), + SignalValueFormatPreference( + OccurrenceAddress.root, + SignalValueFormat.hexadecimal, + ), + ]), + throwsArgumentError, + ); + expect(SignalValueFormatRegistry.formatFor(address), + SignalValueFormat.unsignedDecimal); + expect(SignalValueFormatRegistry.changes.value, changesBefore); + }); + + test('preserves registry state when an address batch is invalid', () { + const address = OccurrenceAddress([0, 2, 4]); + SignalValueFormatRegistry.setFormatFor( + [address], + SignalValueFormat.unsignedDecimal, + ); + final changesBefore = SignalValueFormatRegistry.changes.value; + + expect( + () => SignalValueFormatRegistry.setFormatFor( + [address, OccurrenceAddress.root], + SignalValueFormat.signedDecimal, + ), + throwsArgumentError, + ); + expect(SignalValueFormatRegistry.formatFor(address), + SignalValueFormat.unsignedDecimal); + expect(SignalValueFormatRegistry.changes.value, changesBefore); + }); + + test('converts between serialized names and format enum values', () { + expect( + SignalValueFormatRegistry.formatFromString('signedDecimal'), + SignalValueFormat.signedDecimal, + ); + expect( + SignalValueFormatRegistry.formatToString( + SignalValueFormat.signedDecimal, + ), + 'signedDecimal', + ); + expect(SignalValueFormatRegistry.formatFromString('unknown'), isNull); + }); +} From 2592d933f87c0450480ead85b6547f2bac024595 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 26 Aug 2026 10:46:13 -0700 Subject: [PATCH 2/3] improve handling of signal values --- .../lib/src/occurrence_trie.dart | 11 ------- .../test/occurrence_trie_test.dart | 9 +++--- .../lib/src/signal_value_format_registry.dart | 30 ++++++++++++++++--- .../signal_value_format_registry_test.dart | 30 +++++++++++++++++++ 4 files changed, 60 insertions(+), 20 deletions(-) diff --git a/packages/rohd_hierarchy/lib/src/occurrence_trie.dart b/packages/rohd_hierarchy/lib/src/occurrence_trie.dart index 938c96f30..f6d65b2cb 100644 --- a/packages/rohd_hierarchy/lib/src/occurrence_trie.dart +++ b/packages/rohd_hierarchy/lib/src/occurrence_trie.dart @@ -34,23 +34,12 @@ class OccurrenceTrie { } /// Associates [value] with [address]. - /// - /// This is equivalent to [set], without returning the previous value. void operator []=(OccurrenceAddress address, T value) { - set(address, value); - } - - /// Associates [value] with [address]. - /// - /// Returns the value previously stored at [address], if any. - T? set(OccurrenceAddress address, T value) { var node = _root; for (final index in _validatedPath(address)) { node = node.children.putIfAbsent(index, _OccurrenceTrieNode.new); } - final previous = node.value; node.value = value; - return previous; } /// Removes and returns the value stored at [address], if any. diff --git a/packages/rohd_hierarchy/test/occurrence_trie_test.dart b/packages/rohd_hierarchy/test/occurrence_trie_test.dart index 3faa8a92a..7a3bbf678 100644 --- a/packages/rohd_hierarchy/test/occurrence_trie_test.dart +++ b/packages/rohd_hierarchy/test/occurrence_trie_test.dart @@ -16,9 +16,9 @@ void main() { const first = OccurrenceAddress([0, 2, 4]); const second = OccurrenceAddress([0, 2, 5]); - expect(trie.set(first, 'first'), isNull); + trie[first] = 'first'; trie[OccurrenceAddress.root] = 'root'; - expect(trie.set(second, 'second'), isNull); + trie[second] = 'second'; expect(trie[OccurrenceAddress.root], 'root'); expect(trie[first], 'first'); @@ -30,9 +30,8 @@ void main() { final trie = OccurrenceTrie(); const first = OccurrenceAddress([0, 2, 4]); const second = OccurrenceAddress([0, 2, 5]); - trie - ..set(first, 'first') - ..set(second, 'second'); + trie[first] = 'first'; + trie[second] = 'second'; expect(trie.remove(first), 'first'); expect(trie[first], isNull); diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart index 5d224154a..f482e64bc 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart @@ -102,7 +102,9 @@ class SignalValueFormatRegistry { var changed = false; for (final address in requestedAddresses) { - changed = (_formatTrie.set(address, format) != format) || changed; + final previous = _formatTrie[address]; + _formatTrie[address] = format; + changed = (previous != format) || changed; } if (changed) { _notifyListeners(); @@ -225,7 +227,7 @@ class SignalValueFormatRegistry { /// waveform representation. static (LogicValue, String)? _waveformValue(String value, int width) { final trimmed = value.trim().replaceAll('\u0000', ''); - if (trimmed.isEmpty || _containsUnknownDigits(trimmed)) { + if (trimmed.isEmpty) { return null; } final lower = trimmed.toLowerCase(); @@ -241,7 +243,11 @@ class SignalValueFormatRegistry { : isRadixLiteral ? lower[lower.indexOf("'") + 1] : digits.codeUnits.every( - (codeUnit) => codeUnit == 0x30 || codeUnit == 0x31, + (codeUnit) => + codeUnit == 0x30 || + codeUnit == 0x31 || + codeUnit == 0x78 || + codeUnit == 0x7a, ) ? 'b' : digits.codeUnits.any( @@ -256,10 +262,26 @@ class SignalValueFormatRegistry { final logicValue = LogicValue.ofRadixString(radixLiteral); return ( logicValue, - isRadixLiteral ? trimmed : logicValue.toString(), + _containsUnknownDigits(trimmed) + ? logicValue.toRadixString( + radix: switch (radix) { + 'b' => 2, + 'q' => 4, + 'o' => 8, + 'd' => 10, + 'h' => 16, + _ => throw StateError('Unsupported radix: $radix'), + }, + sepChar: '', + ) + : isRadixLiteral + ? trimmed + : logicValue.toString(), ); } on LogicValueConstructionException { return null; + } on FormatException { + return null; } } diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart index 187f7a223..a5613ac0a 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart @@ -57,6 +57,25 @@ void main() { ); }); + test('canonicalizes bare waveform values with unknown bits', () { + expect( + SignalValueFormatRegistry.formatValue( + '10x0', + SignalValueFormat.waveform, + 4, + ), + "4'b10x0", + ); + expect( + SignalValueFormatRegistry.formatValue( + 'f0xz', + SignalValueFormat.hexadecimal, + 16, + ), + "16'hf0XZ", + ); + }); + test('uses ROHD radix literals for typed format conversions', () { expect( SignalValueFormatRegistry.formatValue( @@ -92,6 +111,17 @@ void main() { ); }); + test('returns invalid radix literals unchanged', () { + expect( + SignalValueFormatRegistry.formatValue( + "8'b2", + SignalValueFormat.waveform, + 8, + ), + "8'b2", + ); + }); + test('looks up an occurrence address from the format trie', () { SignalValueFormatRegistry.setFormatFor( [ From 3da7713bdaf1f8e45839b14b259f1a7cfd209849 Mon Sep 17 00:00:00 2001 From: "Desmond A. Kirkpatrick" Date: Wed, 26 Aug 2026 16:05:56 -0700 Subject: [PATCH 3/3] use regex for identifying value format --- .../lib/src/signal_value_format_registry.dart | 24 +++++++++---------- .../signal_value_format_registry_test.dart | 8 +++++++ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart index f482e64bc..7b1c0e3e9 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/lib/src/signal_value_format_registry.dart @@ -60,6 +60,15 @@ class SignalValueFormatRegistry { /// Prevents instantiation. SignalValueFormatRegistry._(); + /// Matches binary digits, including unknown values. + static final RegExp _binaryDigits = RegExp(r'^[01xz]+$'); + + /// Matches hexadecimal digits, including unknown values. + static final RegExp _hexadecimalDigits = RegExp(r'^[0-9a-fxz]+$'); + + /// Matches a character that distinguishes hexadecimal from decimal. + static final RegExp _hexadecimalMarker = RegExp(r'[a-fxz]'); + /// Stores the registered preference for each signal occurrence. static OccurrenceTrie _formatTrie = OccurrenceTrie(); @@ -242,19 +251,10 @@ class SignalValueFormatRegistry { ? 'b' : isRadixLiteral ? lower[lower.indexOf("'") + 1] - : digits.codeUnits.every( - (codeUnit) => - codeUnit == 0x30 || - codeUnit == 0x31 || - codeUnit == 0x78 || - codeUnit == 0x7a, - ) + : _binaryDigits.hasMatch(digits) ? 'b' - : digits.codeUnits.any( - (codeUnit) => - (codeUnit >= 0x61 && codeUnit <= 0x66) || - (codeUnit >= 0x41 && codeUnit <= 0x46), - ) + : _hexadecimalDigits.hasMatch(digits) && + _hexadecimalMarker.hasMatch(digits) ? 'h' : 'd'; final radixLiteral = isRadixLiteral ? lower : "$displayWidth'$radix$digits"; diff --git a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart index a5613ac0a..fd742e953 100644 --- a/rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart +++ b/rohd_devtools_extension/packages/rohd_devtools_widgets/test/signal_value_format_registry_test.dart @@ -47,6 +47,14 @@ void main() { ), '255', ); + expect( + SignalValueFormatRegistry.formatValue( + '255', + SignalValueFormat.unsignedDecimal, + 8, + ), + '255', + ); expect( SignalValueFormatRegistry.formatValue( '0x0',