From 70e2adf93acad3fe4f540b4c8b180c2edb3fc36f Mon Sep 17 00:00:00 2001 From: "julian.speith" Date: Fri, 11 Sep 2026 10:01:10 +0200 Subject: [PATCH 1/8] Bring the release notes up to date with everything since 4.5.0 Audited the Unreleased section against the 129 commits since v4.5.0. Two plugins new since then had no entry at all: the clock tree extractor was not mentioned, and the DOT viewer had two incremental lines but no entry saying the plugin exists and what it does. The core bookkeeping speedup, the module pin event coalescing, the delay gate type property, the Boolean function and Liberty parser fixes, the hal_py log functions, the simulation wizard features, five GUI fixes and four build changes were missing as well. Seven terse entries now say what the user sees and why, one entry sat in the wrong group, one described defects that never shipped, and one claimed both shortest-path overloads existed before when only one did. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 87 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 73 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65014457fd2..bce7109b0e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ All notable changes to this project will be documented in this file. * Core * fixed crash when passing a `nullptr` pin to `Net::remove_source` or `Net::remove_destination`, which is also reachable from Python * changed `Net` and `Gate` to identify a pin by pointer identity instead of by value when looking up an endpoint + * sped up deleting gates, nets and modules, which searched the object vectors of the netlist and of the owning module linearly for the element to remove; every such vector now keeps the position of its elements in a map (`utils::indexed_vector_push_back` and `indexed_vector_erase`) and removal is a constant-time swap with the last element + * sped up `Module::is_parent_module_of`, which walked the whole subtree of the module, for a top-level module the entire hierarchy, once per endpoint whenever module nets are recomputed; it now walks up the parent chain of the queried module, bounded by the depth of the hierarchy. Together with the constant-time removal, parsing the 216 MB OpenTitan Earl Grey Verilog netlist of 25,906 modules and 2.3 million endpoints went from 709 s to 31 s + * fixed `utils::split` reading the last character of an empty string, which is undefined behaviour * progress and layout reporting * added `ProgressScope` and `LayoutLocker` to the core, which report progress and suppress layout updates through the user interface plugin looked up at runtime, replacing the copies of `GuiLayoutLocker` in the dataflow analysis and module identification plugins * added `UIPluginInterface::set_progress` and `plugin_manager::get_ui_plugin`, so that a plugin no longer needs to provide a `GuiExtensionInterface` and register a callback just to report its progress @@ -17,6 +20,7 @@ All notable changes to this project will be documented in this file. * fixed the progress overlay of the graph view staying up until it is clicked away after a dataflow analysis that was started from a script instead of from the plugin dialog, only the dialog reported the analysis as finished although both report its progress * fixed the progress overlay of the graph view never being dismissed after a module identification run, the call reporting the analysis as finished was commented out * fixed the progress overlay of the graph view being dismissed while the layout updates deferred during a dataflow analysis were still being applied, which left the graph view showing its spinner + * fixed the GUI freezing while a dataflow analysis started from its dialog reported its progress: the worker threads of the analysis updated the progress overlay directly, called `processEvents` from a thread that is not the GUI thread, and did so while holding the mutex that guards the results. Progress reported from another thread is now posted to the GUI thread, and the overlay names the phase of the analysis instead of a generic message * program options * added `ProgramOptions::add_flags` that takes the flags and parameters as vectors so that they can be assembled at runtime * netlist traversal @@ -29,9 +33,16 @@ All notable changes to this project will be documented in this file. * deprecated the three `netlist_utils::get_shortest_path` overloads in favour of `NetlistTraversalDecorator::get_shortest_path`, and `netlist_utils::get_ff_dependency_matrix` in favour of the one in the Boolean influence plugin, which also reports how strongly each flip-flop depends on another rather than only whether it does * moved `get_gate_chain` and `get_complex_gate_chain` from `netlist_utils` onto `NetlistTraversalDecorator`, where the rest of the traversal lives and where a binding can keep the netlist alive for as long as Python refers to the gates it returns * renamed `NetlistTraversalDecorator::get_next_combinational_gates` to `get_combinational_cone`, which is what it returns -- every combinational gate up to the sequential boundary, not a next layer of anything -- and removed the raw result-map cache parameter from it and from `get_next_sequential_gates`, whose reuse contract nothing enforced. Repeated traversals share results through a sealed `TraversalCache` instead. The Python bindings of both now also default `forbidden_pins` to an empty set like the C++ side always did - * added `NetlistTraversalDecorator::get_shortest_path` overloads that end at any gate of a module and that connect two modules, which existed only as free functions in `netlist_utils` before + * added `NetlistTraversalDecorator::get_shortest_path` overloads that end at any gate of a module and that connect two modules. The former existed only as a free function in `netlist_utils` before, the latter is new * gate library * fixed reloading a gate library destroying the library a netlist was built against, which silently replaced every gate type of that netlist. Gate libraries are now owned through a `shared_ptr` and outlive both the netlists and the Python handles that refer to them + * added the `GateTypeProperty::delay` gate type property for dedicated delay cells, such as those of a clock distribution network, which HGL files read and write as `delay` + * module pins + * sped up assigning gates to a module and removing them from it, `Module::get_pin_by_net` scanned every pin of the module and runs for every net of every gate that enters or leaves it; the pins are now indexed by net + * added `PinChangedBulkScope`, which collects the pin events of a bulk operation and sends a single `PinEvent::PinsReload` per affected module in their place, telling a listener to re-read the pins of that module. Assigning gates to modules opens one, which took creating and deleting 24 modules on a 7144 gate netlist from 43040 `pin_changed` events to 48; each event triggered from a Python script cost a round trip to the GUI thread, which was 91% of the script's runtime. Interactive pin changes keep their fine-grained events, and the pins tree of the GUI keeps its expanded groups and its selection across the reload + * fixed `Module::set_pin_group_direction` sending a `GroupTypeChange` pin event instead of `GroupDirChange`, so the pins tree of the GUI did not show the new direction of the group + * fixed the pin events collected while several pins are assigned to a group being delivered in pin ID order rather than in the order the rows come into being, so the pins tree of the GUI could insert a pin at a row its group item did not have yet and crash or show the pins out of order; an out-of-range row is now clamped as well + * changed the default pin order of both `Module::create_pin_group` overloads from ascending to descending, in C++ and in Python, matching the order the GUI, the dataflow analysis and the module identification produce. In Python, `start_index` now defaults to the index of the last pin for a descending group and to 0 for an ascending one * Boolean functions * added `to_string` to `SMT::QueryConfig`, `SMT::Model` and `SMT::SolverResult`, so that all four SMT types offer it the way `SMT::Constraint` already did instead of only an `operator<<` * fixed the printed form of an `SMT::Model` starting with a stray comma, `{, A:5}` instead of `{A:5}` @@ -45,6 +56,9 @@ All notable changes to this project will be documented in this file. * fixed `SMT::Solver::has_local_solver_for` testing `SolverCall::Binary` in both of its branches, so the branch for `SolverCall::Library` was unreachable and library availability always reported `false` * fixed `SMT::SymbolicState::set` using `emplace`, which leaves an existing binding untouched, so setting a variable a second time did nothing and a loop stepping a symbolic state forward silently kept the value it started with * added simplification rules for the word level operations, which the single-bit simplification through ABC cannot reach: extensions to the width the value already has, nested extensions and slices, slices that fall into one half of a concatenation or into either part of an extension, unsigned comparisons against zero and the maximum, equality of a value with its own negation, and single bit equalities and selections + * fixed `BooleanFunction::to_string` reading past the end of its digit table when a bit-vector that contains an `X` or `Z` is converted to an octal or hexadecimal string, the undefined bit was folded into the table index before the digit was replaced by `X` + * fixed `BooleanFunction::from_string` rejecting Liberty expressions that combine Liberty-only syntax such as `A'` or `+` with spaces around the operators, which the Liberty grammar reads as additional AND operations; if neither grammar accepts an expression, it is parsed once more with every space removed, added as `BooleanFunctionParser::ParserType::LibertyNoSpace` + * added `SMT::SymbolicState::get_bindings`, which returns the variables bound in a symbolic state indexed by their name, so that a caller resolving many variables of one state no longer builds a Boolean function as the lookup key for each * Python bindings * fixed the four `boolean_influence` functions that return influences per net handing out the nets without keeping the netlist alive: they return dicts keyed by net, and nothing protected a borrowed object sitting in a dict key * added a warning, once per function and process, when a deprecated `NetlistUtils` function is called from Python, naming its replacement. `[[deprecated]]` warns whoever compiles, and a script has no compiler @@ -55,6 +69,7 @@ All notable changes to this project will be documented in this file. * fixed `NetlistGraph` never being freed by Python: its factories hand over ownership but it was bound with a non-owning holder, so every graph built from a netlist leaked, more than a gigabyte over 1500 graphs on a 3458 gate netlist * fixed `GateLibrary::get_path` and the `path` property returning the name of the library instead of its path * fixed `GuiApi::getSelectedModules` and `getSelectedItems` not tying the returned modules to the netlist + * fixed `GuiApi::selectGate`, `selectNet` and `selectModule` with `clear_current_selection` set, which is the default, keeping the previous selection: the pending selection was cleared but the current one was united back into it right afterwards * added Python bindings for `NetlistGraph::from_gates`, `is_shadow_vertex`, and `get_all_vertices_from_gate` * added Python bindings for `ProgramOptions`, `ProgramArguments`, and `FacExtensionInterface` * added Python bindings for the remaining functions of `plugin_manager` and exposed the `initialize` and `silent` parameters of `get_plugin_instance` @@ -70,6 +85,11 @@ All notable changes to this project will be documented in this file. * fixed three enum values that were bound to a different value of their own enum, which made them indistinguishable from Python: `GateTypeProperty.fifo` was bound to `ram`, `module_identification.CandidateType.addition_offset` to `addition`, and `gui_extension_demo.ParameterType.Module` to `Gate` * added `to_string` and `__str__` to `SMT.QueryConfig`, `SMT.Constraint`, `SMT.Model` and `SMT.SolverResult`, printing any of them showed an object address before * fixed every binding that carries the `hal::borrowed()` call policy segfaulting when it is called with arguments that its first overload does not accept, which happens with pybind11 3.1 and newer: the post-call hook is now also run for an overload whose arguments did not load and is handed a sentinel instead of an object. `Netlist.create_module` with a name, a parent, and a list of gates was the first such call in the binding smoke test, which is why the macOS CI, whose Homebrew pybind11 moved to 3.1.0 in September 2026, failed while the Ubuntu jobs on older pybind11 did not + * added `log_trace`, `log_debug`, `log_info`, `log_warning`, `log_error` and `log_critical` to `hal_py`, which take the channel first like the C++ macros and prefix every severity but `info` with the file and line of the calling Python code + * changed `BooleanFunction.nodes` from a method to a read-only property, which is what its documentation always claimed + * fixed `GateLibrary.get_gate_location_data_identifiers` being unreachable from Python, as it was registered under the name of `get_gate_location_data_category` + * fixed keyword arguments that did not match the documentation: `GateType.add_boolean_function(name)` was `pin_name`, `Module.contains_module(recursive)` was spelled `recusive`, `NetlistFactory.load_netlist_from_string(netlist_string)` was `hdl_string`, and `GateType.has_property(property)` had no keyword at all + * added the missing `delay` value of `GateTypeProperty`, which the enum gained in C++ but the binding never listed * Plugins * Boolean influence * fixed `get_ff_dependency_matrix` dereferencing an uninitialized pointer on every call, which segfaulted before it returned anything. The cache it passes on was never initialized, and a pointer that is not null passed the callee's check for one @@ -90,8 +110,22 @@ All notable changes to this project will be documented in this file. * added the HAWKEYE S-box database to the build directory so that it is found at runtime, and clarified that `identify_sbox` returning an empty string means no match rather than an error * graph algorithm * added `NetlistGraph::from_gates` that builds a graph from a subset of the gates of a netlist, optionally representing a gate by a primary and a shadow vertex so that feedback through it does not close a cycle + * fixed `NetlistGraph` destroying an igraph graph that was never created when `from_netlist` or `copy` fail before creating it, and made `NetlistGraph` non-copyable, as the implicitly generated copy shared the igraph internals between two objects that both freed them + * clock tree extractor + * added the `clock_tree_extractor` plugin, which recovers the clock distribution network of a gate-level netlist. Starting at the clock pin of every flip-flop it walks against the signal direction through buffers, inverters, delay gates, clock gates and toggle-flip-flop clock dividers up to the global input nets that drive them, and returns the result as a `ClockTree`, a directed igraph graph whose vertices are gates and nets of the netlist. Reconvergent clock networks, i.e. meshes, are extracted as well. Available from Python as the `clock_tree_extractor` module + * added `ClockTree::from_netlist` that extracts the clock tree of a netlist, leaving out latches, power and ground nets, and the control inputs of clock gates, and reporting every unrouted or multi-driven clock net it skips as a warning + * added `ClockTree::export_dot` that writes the tree as a DOT graph, in which every gate carries its instance name, gate type and, where the netlist provides them, its `X` and `Y` coordinates as node attributes, is shaped by kind (buffer, inverter, delay gate, flip-flop, clock gate), and every edge downstream of an inverter switches colour, so that the polarity of the clock at each gate can be read off the graph + * added `ClockTree::get_subtree` that returns the clock tree below a gate or net as a `ClockTree` of its own, optionally starting one level up at the parent of the given object if it has exactly one + * added `ClockTree::get_neighbors`, bound to Python as `get_parents` and `get_childs`, that returns the gates and nets directly upstream or downstream of an object in the clock tree + * added `ClockTree::get_gates`, `get_nets`, `get_all` and `get_netlist` that list the gates and nets of a clock tree and hand back the netlist it was built from + * added `ClockTree::get_vertex_from_ptr`, `get_ptr_from_vertex`, `get_vertices_from_ptrs` and `get_ptrs_from_vertices` that translate between gates or nets and their igraph vertex IDs, and `ClockTree::get_igraph` that exposes the underlying `igraph_t` to C++ callers, so that any igraph algorithm can be run on the clock tree * dataflow analysis - * fixed broken initialization of DANA plugin when starting via CLI + * fixed running the dataflow analysis from the command line with `--dataflow`, which failed with "no gate types specified" as the CLI path configured neither the gate types nor the control pin types that the plugin dialog sets, and wrote no result. It now analyzes flip-flops with clock, enable, reset and set as control pins and writes `graph.dot` and `groups.txt` to the directory given by `--path`. `write_dot` and `write_txt` also no longer report "replacing invalid file extension" for a path whose extension is already correct + * fixed the dataflow analysis blocking a thread when it wrote its results, progress was reported while the lock that guards the results was held + * changed `create_modules` to create the pin groups of a DANA module in descending order, listing the highest index first as the GUI does by default, instead of renaming the single-pin group that the module already carried. The pin indices are unchanged + * added `Result::open_dot_in_viewer`, which the plugin dialog calls after writing the `.dot` file so that the dataflow graph opens in the DOT viewer if that plugin is loaded. `Result::write_dot` now returns the path it wrote to, as it replaces a wrong extension + * module identification + * changed the pin groups of an identified module to be descending, listing the highest index first as the GUI does by default. The pin indices are unchanged, and the `CTRL` group is now of type `control` instead of `enable` * FSM solver * reworked the API of `solve_fsm` into a single function that is set up through a `Configuration` object, which also selects between the SMT and the brute force approach * changed `solve_fsm` to report the value of each configured output of the FSM in each state, annotating the states of the DOT graph with it @@ -102,6 +136,8 @@ All notable changes to this project will be documented in this file. * removed the debug output that `solve_fsm` printed to stdout on every run * fixed `solve_fsm` interpreting a user-provided initial state with the wrong bit order, which made the exploration start from a different state than the one requested * added tests for the `solve_fsm` plugin, which had none so far + * Liberty parser + * fixed the Liberty parser rejecting a `type` group that declares `bit_to`, the value was left in the token stream after the attribute was read and parsing then failed on what it took to be the end of the statement. The value is implied by `bit_from`, `bit_width` and `downto` and is still ignored * netlist preprocessing * fixed `remove_redundant_gates` treating two flip-flops as duplicates although they start out at different values, as the fingerprint it groups them by covers the gate type and the fan-in but not the initial value, and flip-flops are merged on that fingerprint alone without the equivalence check that combinational gates get. This affects 11 of the 13 flip-flop types of the Xilinx UNISIM library, all of which carry an `INIT` value * added an optional gate scope to the preprocessing functions of `netlist_preprocessing` and `xilinx_toolbox`, restricting which gates may be modified or deleted and defaulting to the entire netlist @@ -112,36 +148,59 @@ All notable changes to this project will be documented in this file. * fixed `split_luts` crashing on a `LUT6_2` that only uses one of its two output pins, which is the common case the function is meant to handle * fixed the documentation of `split_shift_registers`, which claimed that only `SRL16E` is supported although `SRLC32E` is handled as well * added tests for the `xilinx_toolbox` plugin, which had none so far + * changed the members of `xilinx_toolbox::LOC` to be default-initialised, `loc_type` to `PIN` and both coordinates to `0`, so that the LOC of a package pin, for which the XDC parser sets no coordinates, no longer carries uninitialised values * bit-order propagation * changed the interface to speak in a `BitOrder`, which is the order of one module pin group, and a `BitOrderResult`, which is what a propagation reports, in place of a map from pairs of module and pin group to a map from net to index. A bit order is now an object rather than a container, so Python can be given one without losing track of the netlist it belongs to, and a result iterates by module and pin group ID rather than by the addresses they happen to sit at * added tests for the plugin, which had none * fixed bug in the bitorder propagation algorithm that would assign a wrong propagation order if pingroups with direction none were given as parameters * simulation - * added feature, selecting a waveform in viewer selects net in graph view as well - * fixed bug in waveform viewer, make sure that deleting a controller causes closing the tab + * added selecting the nets of the waveforms selected in the waveform viewer in the graph view as well, and removed the debug dump of the SALEAE directory that was printed to the console whenever waveforms were loaded + * fixed the waveform viewer keeping the tab of a simulation controller that was deleted, e.g. from Python, and dereferencing the deleted controller from it + * fixed the value of a waveform group being computed with the first net as LSB in `WaveDataGroup::recalcData`, `WaveDataProviderGroup` and `WaveGroupValue` while the rest of the viewer takes the first net as MSB since 4.5.0, so the same group showed different values depending on where it was evaluated + * changed the input columns of the simulation wizard to list the nets of a descending pin group from the highest index down, so that the bits of an entered value land on the nets in the order the group declares + * fixed the waveform tree losing the expanded or collapsed state of its groups whenever its items are reordered, e.g. after a drag and drop + * fixed dropping a waveform onto a group in the waveform tree always inserting it as the first entry of the group regardless of the drop position + * simulation wizard + * added `Load data from file` to the manual input page, which fills the input table from a SALEAE directory, a VCD or a CSV file so that the data can be edited before simulating + * added a `Display values as hex numbers` switch to the input table, and changed the table to accept values in the `0x`, `0o`, `0b` and the Verilog `'h`, `'o`, `'b`, `'d` notations as well as `"` for the code of a character, each checked against the width of the pin group; before, a multi-bit column only took a bare hexadecimal number + * fixed editing a time in the middle of the input table replacing it with the previous time plus 1000 as if it were the last row; it is now kept if it lies between its neighbours and replaced by their midpoint otherwise + * fixed the wizard starting with the wave data and net groups of the previous run, its SALEAE directory and the controller are now reset when the wizard opens + * changed the window title from `Empty Wizard` to `Simulation Wizard` and greyed out the input method that is not selected * fixed the documentation of `NetlistSimulatorController::initialize`, which described the behaviour of the legacy `NetlistSimulator`: it claimed that no gates or clocks may be added afterwards and that `simulate` calls it automatically, neither of which holds since its body became empty * dot viewer - * added 'hover over node' feature in dot viewer - * fixed the DOT viewer drawing the line break escapes of a node label verbatim instead of breaking the line, and drawing a red debug rectangle around any label that does not fit its node + * added the `dot_viewer` plugin, which renders a Graphviz `.dot` file inside the GUI and, for a file written by another HAL plugin, ties the graph to the netlist. Its Python module is `dot_viewer` and its one function `load_dot_file(path, creator_plugin="")` returns whether the file was displayed. The viewer is opened from the plugin dialog or from `load_dot_file`, zooms with the mouse wheel and the zoom shortcuts of the graph view, pans by dragging with Shift held or with the middle mouse button if the graph view's middle-button panning is enabled, highlights the node under the mouse together with its edges, offers a grid toggle and a toolbar menu that chooses per node and edge whether the colors come from the DOT file or from the dark or light style of HAL, and honours the `\n` line breaks of a node label. The plugin that wrote the file is taken from the `creator_plugin` argument, else from a `created by HAL plugin` comment in the file, else asked for in a dialog. For a dataflow analysis graph, selecting a node selects the module in HAL and the other way round, a node follows its module when the module is renamed, and the context menu of an edge isolates the shortest path between its two modules in a new view; for an FSM solver graph, selecting a transition puts every net into a `0 state`, `1 state` or `x state` grouping according to the value the transition condition requires of it, the context menu of a transition lists those nets, and selecting a net in HAL highlights it in every transition it appears in + * renamed `DotViewerCallFromTread` to `DotViewerCallFromThread` * GUI * fixed the GUI hanging for minutes when a module with many gates is selected, `ModuleModel` emitted a row insert signal per item while the model was already being reset, which made the attached filter proxy remap its rows once per item * fixed the GUI stalling when a large module is unfolded, the tree views measured every row individually and shaped the text of each gate name just to learn how tall the row is * changed the module elements tree to not rebuild itself twice per selection change - * fixed bug in code and comment editor: avoid hang ups when RegExp-search returns zero-length matches - * added information to GUI setting file so that widgets position and size from previous session gets restored - * added option to focus on pin in pin context menu - * changed default order to 'descending' when creating a pin group via Python command - * changed behavior of GUI plugin manager to keep only those plugins loaded which are requested by user + * fixed the Python editor and the comment editor hanging when the search string is empty or the regular expression matches an empty string, `find` returned the same zero-length match forever; an empty search string now clears the highlighting + * added restoring the layout of the previous session on start: for every widget, plugin widgets included, its dock area, position, visibility, size and, if it was detached, the position of its window are written to the user settings file together with the splitter sizes when a netlist is closed and applied when the next one is opened + * added `Set focus to pin` to the context menu of a pin in the pin tree of a gate and of a module, which moves the sub-focus of the graph view to that pin + * changed the plugin manager of the GUI to unload a plugin, and the dependencies it pulled in, right after loading it to read its description, so that only plugins requested by the user or required by such a plugin stay loaded * fixed the GUI dropping an unrelated gate from the selection instead of the net itself when a selected net is deleted * fixed the GUI re-laying out its graph views once per gate while a preprocessing function invoked from a context menu deletes or replaces many of them * added context menu entries to the GUI for `remove_buffers`, `unify_ff_outputs`, `split_luts`, and `split_shift_registers`, each applicable to the current selection or to the entire netlist + * fixed the GUI crashing when the selection is to be brought into view before the graph view has rendered its layout, e.g. by `GuiApi::selectGate` with `navigate_to_selection` set right after opening a netlist, the scene had no item for the selected element yet + * fixed unloading the waveform viewer or the DOT viewer plugin leaving its dock button behind, the widget was deleted without being removed from its anchor + * changed duplicating a view to place the modules and gates of the copy where they are in the original instead of laying the copy out anew + * fixed a module that reuses the ID of a deleted module being shown with the colour icon of the deleted one in the selection details, the icon cache was never told about removed modules + * fixed the GUI at times not registering the first change to a netlist after start, and so not offering to save it on close, the flag that guards the notification was never initialized * module pin groups - * fixed bug in pin model which must not crash when deleting a non-empty pin group - * fixed bug by disallowing deletion of group comprising a single pin with same name + * fixed the GUI crashing when a pin group that still holds pins is deleted: the pins tree removed the group item while the pin items still hung below it, and the events that followed for those pins looked them up through the detached group + * fixed the GUI crashing when `Delete pin group` is chosen for a group that holds a single pin named like the group: there is nothing to do for such a group and the action built for it was a null pointer that was then dereferenced; the entry now does nothing for such a group + * changed the pin tree of a module to show the number of pins of each group and its order, ↑ for ascending and ↓ for descending, in a `Size/Index` column right after the name, which also holds the index of each pin; the pin tree of a gate shows the number of pins and the order of each group in its `Index` column + * fixed toggling a pin group between ascending and descending losing the direction and the type of the group, and deleting a pin group creating the single-pin groups for its pins without direction and type + * added `Automatically rename pins` to the context menu of a pin group, which renames every pin of the group to `()` * Build and dependencies * changed the GUI from Qt 5 to Qt 6, which is now required to build the GUI + * updated the vendored QuaZip from 1.3 to 1.5 as part of the move to Qt 6 + * added the Graphviz development libraries to the build dependencies, `libgraphviz-dev` on Ubuntu, which the DOT viewer plugin links against and which is built by default; pass `-DPL_DOT_VIEWER=OFF` to build without them + * added support for Ubuntu 26.04, which is now built and tested in CI next to 22.04, 24.04 and macOS + * fixed `install_dependencies.sh` on macOS writing the Homebrew prefix into the shell configuration as the unexpanded text `$BREW_PREFIX`, so the `PATH` entries it added for Qt, LLVM, flex and bison pointed nowhere; a line that was added this way is left in place and can be deleted + * fixed the documentation build: Sphinx is now run through the Python interpreter that `hal_py` is linked against, as an unrelated `sphinx-build` crashed on importing it, and `hal_py` is imported before autodoc touches a plugin module, without which the `boolean_influence`, `dataflow` and `module_identification` pages were empty. Doxygen warnings went from 295 to 2 and Sphinx warnings from 15 to 0, and every namespace, class and struct now carries a description * added a test that checks the Python bindings never hand out a borrowed pointer without keeping its owner alive, and never give a class bound with a non-owning holder to a factory that returns a `unique_ptr`. It covers plugins kept in a repository of their own as well, and holds free, static and submodule-level functions to the same rule as methods, which `hal::borrowed()` made fixable - * updated the vendored igraph dependency from 0.10.12 to 1.0.1 and ported the graph algorithm and HAWKEYE plugins to the igraph 1.0 API + * updated the vendored igraph dependency from 0.10.12 to 1.0.1 and ported the graph algorithm and HAWKEYE plugins to the igraph 1.0 API; building against a system igraph (`USE_VENDORED_IGRAPH=OFF`) now requires igraph 1.0, and the vendored igraph is no longer built with warnings as errors, which is what broke the macOS build once Apple clang 18 added `-Wuninitialized-const-pointer` * removed the tests below `tests/python_binding`, which were neither referenced by the build nor by any workflow and called API that no longer exists ## [4.5.0](v4.5.0) - 2025-09-23 12:00:00+02:00 (urgency: medium) From 232106cbc45f108c009ab690ccd1728f83353c75 Mon Sep 17 00:00:00 2001 From: "julian.speith" Date: Fri, 11 Sep 2026 10:01:10 +0200 Subject: [PATCH 2/8] Default both create_pin_group overloads of a module to descending Commit 55bf6c12c2f changed the default of the overload without an ID to descending, in C++ and in both Python bindings, but left the overload with an ID at ascending. The same call therefore built a descending group from Python and an ascending one from C++, and all four doc strings kept saying the default is ascending. Descending is what the GUI, the dataflow analysis and the module identification produce, so both overloads now default to it and the doc strings say so. Co-Authored-By: Claude Fable 5.1 --- include/hal_core/netlist/module.h | 6 +++--- src/python_bindings/bindings/module.cpp | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/include/hal_core/netlist/module.h b/include/hal_core/netlist/module.h index 3bc0ce8f595..0d0e20b4d62 100644 --- a/include/hal_core/netlist/module.h +++ b/include/hal_core/netlist/module.h @@ -484,7 +484,7 @@ namespace hal * @param[in] pins - The pins to be assigned to the pin group. Defaults to an empty vector. * @param[in] direction - The direction of the pin group, if any. Defaults to `PinDirection::none`. * @param[in] type - The type of the pin group, if any. Defaults to `PinType::none`. - * @param[in] ascending - Set `true` for ascending pin order (from 0 to n-1), `false` otherwise (from n-1 to 0). Defaults to `true`. + * @param[in] ascending - Set `true` for ascending pin order (from 0 to n-1), `false` otherwise (from n-1 to 0). Defaults to `false`. * @param[in] start_index - The start index of the pin group. Defaults to `0`. * @param[in] delete_empty_groups - Set `true` to delete groups that are empty after the pins have been assigned to the new group, `false` to keep empty groups. Defaults to `true`. * @param[in] force_name - Set `true` to enforce the name, `false` otherwise. If a pin group with the same name already exists, the existing pin group will be renamed. Defaults to `false`. @@ -495,7 +495,7 @@ namespace hal const std::vector pins = {}, PinDirection direction = PinDirection::none, PinType type = PinType::none, - bool ascending = true, + bool ascending = false, u32 start_index = 0, bool delete_empty_groups = true, bool force_name = false); @@ -508,7 +508,7 @@ namespace hal * @param[in] pins - The pins to be assigned to the pin group. Defaults to an empty vector. * @param[in] direction - The direction of the pin group, if any. Defaults to `PinDirection::none`. * @param[in] type - The type of the pin group, if any. Defaults to `PinType::none`. - * @param[in] ascending - Set `true` for ascending pin order (from 0 to n-1), `false` otherwise (from n-1 to 0). Defaults to `true`. + * @param[in] ascending - Set `true` for ascending pin order (from 0 to n-1), `false` otherwise (from n-1 to 0). Defaults to `false`. * @param[in] start_index - The start index of the pin group. Defaults to `0`. * @param[in] delete_empty_groups - Set `true` to delete groups that are empty after the pins have been assigned to the new group, `false` to keep empty groups. Defaults to `true`. * @param[in] force_name - Set `true` to enforce the name, `false` otherwise. If a pin group with the same name already exists, the existing pin group will be renamed. Defaults to `false`. diff --git a/src/python_bindings/bindings/module.cpp b/src/python_bindings/bindings/module.cpp index 6cce93c0f9f..ca3e4d5ede0 100644 --- a/src/python_bindings/bindings/module.cpp +++ b/src/python_bindings/bindings/module.cpp @@ -728,8 +728,8 @@ namespace hal :param list[hal_py.ModulePin] pins: The pins to be assigned to the pin group. Defaults to an empty list. :param hal_py.PinDirection direction: The direction of the pin group, if any. Defaults to ``hal_py.PinDirection.none``. :param hal_py.PinType type: The type of the pin group, if any. Defaults to ``hal_py.PinType.none``. - :param bool ascending: Set ``True`` for ascending pin order (from 0 to n-1), ``False`` otherwise (from n-1 to 0). Defaults to ``True``. - :param int start_index: The start index of the pin group. Defaults to ``0``. + :param bool ascending: Set ``True`` for ascending pin order (from 0 to n-1), ``False`` otherwise (from n-1 to 0). Defaults to ``False``. + :param int start_index: The start index of the pin group. Defaults to ``0`` for an ascending group and to the index of the last pin for a descending one. :param bool delete_empty_groups: Set ``True`` to delete groups that are empty after the pins have been assigned to the new group, ``False`` to keep empty groups. Defaults to ``True``. :param bool force_name: Set ``True`` to enforce the name, ``False`` otherwise. If a pin group with the same name already exists, the existing pin group will be renamed. Defaults to ``False``. :returns: The pin group on success, ``None`` otherwise. @@ -781,9 +781,9 @@ namespace hal :param list[hal_py.ModulePin] pins: The pins to be assigned to the pin group. Defaults to an empty list. :param hal_py.PinDirection direction: The direction of the pin group, if any. Defaults to ``hal_py.PinDirection.none``. :param hal_py.PinType type: The type of the pin group, if any. Defaults to ``hal_py.PinType.none``. - :param bool ascending: Set ``True`` for ascending pin order (from 0 to n-1), ``False`` otherwise (from n-1 to 0). Defaults to ``True``. - :param int start_index: The start index of the pin group. Defaults to ``0``. - :param bool delete_empty_groups: Set ``True``` to delete groups that are empty after the pins have been assigned to the new group, ```False``` to keep empty groups. Defaults to ```True```. + :param bool ascending: Set ``True`` for ascending pin order (from 0 to n-1), ``False`` otherwise (from n-1 to 0). Defaults to ``False``. + :param int start_index: The start index of the pin group. Defaults to ``0`` for an ascending group and to the index of the last pin for a descending one. + :param bool delete_empty_groups: Set ``True`` to delete groups that are empty after the pins have been assigned to the new group, ``False`` to keep empty groups. Defaults to ``True``. :param bool force_name: Set ``True`` to enforce the name, ``False`` otherwise. If a pin group with the same name already exists, the existing pin group will be renamed. Defaults to ``False``. :returns: The pin group on success, ``None`` otherwise. :rtype: hal_py.ModulePinGroup or None From 64592350dfe6cde13109b091af845843e4729eae Mon Sep 17 00:00:00 2001 From: "julian.speith" Date: Fri, 11 Sep 2026 10:01:10 +0200 Subject: [PATCH 3/8] Bind the delay gate type property to Python GateTypeProperty::delay was added in C++ for the clock tree extractor but the binding lists the enum values by hand and stopped at scan, so hal_py.GateTypeProperty.delay did not exist. Co-Authored-By: Claude Fable 5.1 --- src/python_bindings/bindings/gate_type.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/python_bindings/bindings/gate_type.cpp b/src/python_bindings/bindings/gate_type.cpp index 7740389f1fb..471011bc464 100644 --- a/src/python_bindings/bindings/gate_type.cpp +++ b/src/python_bindings/bindings/gate_type.cpp @@ -22,6 +22,7 @@ namespace hal .value("pll", GateTypeProperty::pll, R"(PLL gate type.)") .value("oscillator", GateTypeProperty::oscillator, R"(Oscillator gate type.)") .value("scan", GateTypeProperty::scan, R"(Scan gate type.)") + .value("delay", GateTypeProperty::delay, R"(Delay gate type.)") .value("c_buffer", GateTypeProperty::c_buffer, R"(Buffer gate type.)") .value("c_inverter", GateTypeProperty::c_inverter, R"(Inverter gate type.)") .value("c_and", GateTypeProperty::c_and, R"(AND gate type.)") From 484cfc388a828586e960d50a3217b0ba9a1ab83e Mon Sep 17 00:00:00 2001 From: "julian.speith" Date: Fri, 11 Sep 2026 10:01:10 +0200 Subject: [PATCH 4/8] Connect a flip-flop clocked straight from an input net to its clock tree When the clock net of a flip-flop is a global input net, the extraction inserted the net as a vertex and moved on without an edge to the flip-flop, so on a netlist without clock buffers every flip-flop was an isolated root. The branch for nets reached through a gate adds that edge; this one now does as well. Tests cover the buffered and the direct case, the plugin had none. The Python bindings keep the netlist alive behind a returned tree, protect the objects handed out by get_ptrs_from_vertices the way the other getters do, and carry doc strings with return types so the binding lifetime check covers the class from now on. -march=native is gone from the build: nothing in the plugin needs it and it made the library non-portable. The plugin is whitelisted in plugins/.gitignore, which had been bypassed by force-adding its files. Co-Authored-By: Claude Fable 5.1 --- plugins/.gitignore | 2 + plugins/clock_tree_extractor/CMakeLists.txt | 3 +- .../python/python_bindings.cpp | 114 +++++++++++++++--- .../clock_tree_extractor/src/clock_tree.cpp | 2 + .../clock_tree_extractor/test/CMakeLists.txt | 13 ++ .../test/clock_tree_extractor.cpp | 100 +++++++++++++++ 6 files changed, 219 insertions(+), 15 deletions(-) create mode 100644 plugins/clock_tree_extractor/test/CMakeLists.txt create mode 100644 plugins/clock_tree_extractor/test/clock_tree_extractor.cpp diff --git a/plugins/.gitignore b/plugins/.gitignore index 4d488ba201d..0ffcd9fa994 100644 --- a/plugins/.gitignore +++ b/plugins/.gitignore @@ -3,6 +3,8 @@ !bitorder_propagation/**/* !boolean_influence* !boolean_influence/**/* +!clock_tree_extractor* +!clock_tree_extractor/**/* !dataflow_analysis* !dataflow_analysis/**/* !dot_viewer* diff --git a/plugins/clock_tree_extractor/CMakeLists.txt b/plugins/clock_tree_extractor/CMakeLists.txt index 1fccdf0e035..79624f6aafc 100644 --- a/plugins/clock_tree_extractor/CMakeLists.txt +++ b/plugins/clock_tree_extractor/CMakeLists.txt @@ -18,7 +18,8 @@ if(PL_CLOCK_TREE_EXTRACTOR OR BUILD_ALL_PLUGINS) HEADER ${CLOCK_TREE_EXTRACTOR_INC} SOURCES ${CLOCK_TREE_EXTRACTOR_SRC} ${CLOCK_TREE_EXTRACTOR_PYTHON_SRC} LINK_LIBRARIES graph_algorithm - COMPILE_OPTIONS "-march=native" ) + add_subdirectory(test) + endif() diff --git a/plugins/clock_tree_extractor/python/python_bindings.cpp b/plugins/clock_tree_extractor/python/python_bindings.cpp index a6e3ab8341e..43309d9d51d 100644 --- a/plugins/clock_tree_extractor/python/python_bindings.cpp +++ b/plugins/clock_tree_extractor/python/python_bindings.cpp @@ -122,7 +122,9 @@ namespace hal :rtype: set[str] )" ); - py::class_( m, "ClockTree", R"()" ) + py::class_( m, "ClockTree", R"( + The clock distribution network of a netlist as a directed graph whose vertices are gates and nets. + )" ) .def_static( "from_netlist", []( const Netlist *netlist ) -> std::unique_ptr { @@ -137,7 +139,16 @@ namespace hal }, py::arg( "netlist" ), py::return_value_policy::move, - R"()" ) + py::keep_alive<0, 1>(), + R"( + Extract the clock tree of a netlist. + + Starting at the clock pin of every flip-flop, the extraction walks against the signal direction through buffers, inverters, delay gates, clock gates and toggle flip-flops up to the global input nets that drive them. + + :param hal_py.Netlist netlist: The netlist. + :returns: The clock tree on success, ``None`` otherwise. + :rtype: clock_tree_extractor.ClockTree or None + )" ) .def( "export", []( const cte::ClockTree &self, const std::string &pathname ) -> bool { @@ -151,7 +162,13 @@ namespace hal return false; }, py::arg( "pathname" ), - R"()" ) + R"( + Write the clock tree to a DOT file. + + :param str pathname: The path of the file to write. + :returns: ``True`` on success, ``False`` otherwise. + :rtype: bool + )" ) .def( "get_subtree", []( const cte::ClockTree &self, @@ -169,7 +186,16 @@ namespace hal py::arg( "ptr" ), py::arg( "parent" ) = false, py::return_value_policy::move, - R"()" ) + py::keep_alive<0, 1>(), + R"( + Get the clock tree below a gate or net as a clock tree of its own. + + :param ptr: The gate or net. + :type ptr: hal_py.Gate or hal_py.Net + :param bool parent: Set ``True`` to start one level up, at the parent of the given object, if it has exactly one. Defaults to ``False``. + :returns: The subtree on success, ``None`` otherwise. + :rtype: clock_tree_extractor.ClockTree or None + )" ) .def( "get_all", []( const cte::ClockTree &self ) -> py::list { @@ -189,7 +215,12 @@ namespace hal return result; }, borrowed(), - R"()" ) + R"( + Get all gates and nets of the clock tree. + + :returns: A list of gates and nets. + :rtype: list[hal_py.Gate or hal_py.Net] + )" ) .def( "get_vertex_from_ptr", []( const cte::ClockTree &self, const void *ptr ) -> py::object { @@ -202,7 +233,14 @@ namespace hal return py::none(); }, py::arg( "ptr" ), - R"()" ) + R"( + Get the igraph vertex ID of a gate or net of the clock tree. + + :param ptr: The gate or net. + :type ptr: hal_py.Gate or hal_py.Net + :returns: The vertex ID on success, ``None`` otherwise. + :rtype: int or None + )" ) .def( "get_ptr_from_vertex", []( const cte::ClockTree &self, const igraph_integer_t vertex ) -> py::object { @@ -225,7 +263,13 @@ namespace hal }, py::arg( "vertex" ), borrowed(), - R"()" ) + R"( + Get the gate or net behind an igraph vertex ID of the clock tree. + + :param int vertex: The vertex ID. + :returns: The gate or net on success, ``None`` otherwise. + :rtype: hal_py.Gate or hal_py.Net or None + )" ) .def( "get_vertices_from_ptrs", []( const cte::ClockTree &self, const std::vector &ptrs ) -> py::list { @@ -238,7 +282,13 @@ namespace hal return py::none(); }, py::arg( "ptrs" ), - R"()" ) + R"( + Get the igraph vertex IDs of gates and nets of the clock tree. + + :param list[hal_py.Gate or hal_py.Net] ptrs: The gates and nets. + :returns: The vertex IDs on success, ``None`` otherwise. + :rtype: list[int] or None + )" ) .def( "get_ptrs_from_vertices", []( const cte::ClockTree &self, const std::vector &vertices ) -> py::list { @@ -268,7 +318,14 @@ namespace hal return py::none(); }, py::arg( "vertices" ), - R"()" ) + borrowed(), + R"( + Get the gates and nets behind igraph vertex IDs of the clock tree. + + :param list[int] vertices: The vertex IDs. + :returns: The gates and nets on success, ``None`` otherwise. + :rtype: list[hal_py.Gate or hal_py.Net] or None + )" ) .def( "get_parents", []( const cte::ClockTree &self, const void *ptr ) -> py::list { @@ -299,7 +356,14 @@ namespace hal }, py::arg( "ptr" ), borrowed(), - R"()" ) + R"( + Get the gates and nets directly upstream of a gate or net in the clock tree. + + :param ptr: The gate or net. + :type ptr: hal_py.Gate or hal_py.Net + :returns: The neighbors on success, ``None`` otherwise. + :rtype: list[hal_py.Gate or hal_py.Net] or None + )" ) .def( "get_childs", []( const cte::ClockTree &self, const void *ptr ) -> py::list { @@ -330,10 +394,32 @@ namespace hal }, py::arg( "ptr" ), borrowed(), - R"()" ) - .def( "get_gates", &cte::ClockTree::get_gates, borrowed(), R"()" ) - .def( "get_nets", &cte::ClockTree::get_nets, borrowed(), R"()" ) - .def( "get_netlist", &cte::ClockTree::get_netlist, borrowed(), R"()" ); + R"( + Get the gates and nets directly downstream of a gate or net in the clock tree. + + :param ptr: The gate or net. + :type ptr: hal_py.Gate or hal_py.Net + :returns: The neighbors on success, ``None`` otherwise. + :rtype: list[hal_py.Gate or hal_py.Net] or None + )" ) + .def( "get_gates", &cte::ClockTree::get_gates, borrowed(), R"( + Get all gates of the clock tree. + + :returns: The gates. + :rtype: list[hal_py.Gate] + )" ) + .def( "get_nets", &cte::ClockTree::get_nets, borrowed(), R"( + Get all nets of the clock tree. + + :returns: The nets. + :rtype: list[hal_py.Net] + )" ) + .def( "get_netlist", &cte::ClockTree::get_netlist, borrowed(), R"( + Get the netlist the clock tree was extracted from. + + :returns: The netlist. + :rtype: hal_py.Netlist + )" ); #ifndef PYBIND11_MODULE return m.ptr(); diff --git a/plugins/clock_tree_extractor/src/clock_tree.cpp b/plugins/clock_tree_extractor/src/clock_tree.cpp index 182846ea55d..227779bd4c6 100644 --- a/plugins/clock_tree_extractor/src/clock_tree.cpp +++ b/plugins/clock_tree_extractor/src/clock_tree.cpp @@ -224,8 +224,10 @@ namespace hal } else if( clk->is_global_input_net() ) { + // the flip-flop is clocked straight from the outside: the net is the root and the flip-flop its only child vertices.insert( (void *) clk ); ptrs_to_type[(void *) clk] = PtrType::NET; + edges.insert( { (void *) clk, (void *) ff } ); continue; } else if( clk->get_num_of_sources() == 0 ) diff --git a/plugins/clock_tree_extractor/test/CMakeLists.txt b/plugins/clock_tree_extractor/test/CMakeLists.txt new file mode 100644 index 00000000000..f8d7bd9edab --- /dev/null +++ b/plugins/clock_tree_extractor/test/CMakeLists.txt @@ -0,0 +1,13 @@ +if(BUILD_TESTS) + include_directories(${gtest_SOURCE_DIR}/include ${gtest_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/include ${CMAKE_SOURCE_DIR}/tests ${CMAKE_SOURCE_DIR}/plugins/clock_tree_extractor/include ${CMAKE_SOURCE_DIR}/plugins/graph_algorithm/include) + + add_executable(runTest-clock_tree_extractor clock_tree_extractor.cpp) + + target_link_libraries(runTest-clock_tree_extractor clock_tree_extractor graph_algorithm pthread gtest hal::core hal::netlist test_utils) + + add_test(runTest-clock_tree_extractor ${CMAKE_BINARY_DIR}/bin/hal_plugins/runTest-clock_tree_extractor --gtest_output=xml:${CMAKE_BINARY_DIR}/gtestresults-runBasicTests.xml) + + if(${CMAKE_BUILD_TYPE} STREQUAL "Debug") + add_sanitizers(runTest-clock_tree_extractor) + endif() +endif() diff --git a/plugins/clock_tree_extractor/test/clock_tree_extractor.cpp b/plugins/clock_tree_extractor/test/clock_tree_extractor.cpp new file mode 100644 index 00000000000..546aed7dc37 --- /dev/null +++ b/plugins/clock_tree_extractor/test/clock_tree_extractor.cpp @@ -0,0 +1,100 @@ +#include "clock_tree_extractor/clock_tree.h" +#include "hal_core/netlist/gate.h" +#include "hal_core/netlist/net.h" +#include "hal_core/netlist/netlist.h" +#include "netlist_test_utils.h" + +#include "gtest/gtest.h" + +namespace hal +{ + class ClockTreeExtractorTest : public ::testing::Test + { + protected: + virtual void SetUp() + { + test_utils::init_log_channels(); + } + + virtual void TearDown() + { + } + + static std::vector upstream_of(const cte::ClockTree& tree, const Gate* gate) + { + auto res = tree.get_neighbors(gate, IGRAPH_IN); + EXPECT_TRUE(res.is_ok()); + std::vector ptrs; + for (const auto& [ptr, type] : res.get()) + { + ptrs.push_back(ptr); + } + return ptrs; + } + }; + + /** + * A flip-flop whose clock pin is driven by a buffer behind a global input net: the tree runs from the net through the + * buffer to the flip-flop. + * + * Functions: from_netlist, get_neighbors + */ + TEST_F(ClockTreeExtractorTest, check_buffered_clock) + { + TEST_START + { + std::unique_ptr nl = test_utils::create_empty_netlist(); + ASSERT_NE(nl, nullptr); + const GateLibrary* gl = nl->get_gate_library(); + + Gate* buf = nl->create_gate(gl->get_gate_type_by_name("BUF"), "buf"); + Gate* ff = nl->create_gate(gl->get_gate_type_by_name("DFF"), "ff"); + + Net* clk = nl->create_net("clk"); + clk->mark_global_input_net(); + clk->add_destination(buf, "I"); + test_utils::connect(nl.get(), buf, "O", ff, "CLK"); + + auto res = cte::ClockTree::from_netlist(nl.get()); + ASSERT_TRUE(res.is_ok()); + std::unique_ptr tree = res.get(); + + EXPECT_EQ(tree->get_gates().size(), 2); + EXPECT_EQ(tree->get_nets().size(), 1); + EXPECT_EQ(upstream_of(*tree, ff), std::vector{buf}); + EXPECT_EQ(upstream_of(*tree, buf), std::vector{clk}); + } + TEST_END + } + + /** + * A flip-flop clocked straight from a global input net, without any gate in between: the net is the root of the tree and + * the flip-flop hangs below it. + * + * Functions: from_netlist, get_neighbors + */ + TEST_F(ClockTreeExtractorTest, check_direct_clock) + { + TEST_START + { + std::unique_ptr nl = test_utils::create_empty_netlist(); + ASSERT_NE(nl, nullptr); + const GateLibrary* gl = nl->get_gate_library(); + + Gate* ff = nl->create_gate(gl->get_gate_type_by_name("DFF"), "ff"); + + Net* clk = nl->create_net("clk"); + clk->mark_global_input_net(); + clk->add_destination(ff, "CLK"); + + auto res = cte::ClockTree::from_netlist(nl.get()); + ASSERT_TRUE(res.is_ok()); + std::unique_ptr tree = res.get(); + + EXPECT_EQ(tree->get_gates().size(), 1); + EXPECT_EQ(tree->get_nets().size(), 1); + EXPECT_EQ(upstream_of(*tree, ff), std::vector{clk}); + } + TEST_END + } +} // namespace hal From 877cb552e6503adce620ac0b51959648a8b6fd0d Mon Sep 17 00:00:00 2001 From: "julian.speith" Date: Fri, 11 Sep 2026 10:01:11 +0200 Subject: [PATCH 5/8] Wire the Graphviz memory discipline check through and drop DOT viewer leftovers The CMake check for Agdisc_t::mem set HAS_AGDISC_MEM but never passed it to the compiler, and the two #ifdefs spelled it HAS_AGDISK_MEM, so the in-memory reader was dead code on every Graphviz version; the plain reader always ran, which is why it worked. The define is now passed when the check succeeds and the guards use its name. The colour-style toggle no longer prints to stderr, its menu text no longer says "attibute", and -march=native is gone from the build as nothing needs it. Co-Authored-By: Claude Fable 5.1 --- plugins/dot_viewer/CMakeLists.txt | 1 - plugins/dot_viewer/deps/QGVCore/CMakeLists.txt | 5 +++++ plugins/dot_viewer/deps/QGVCore/QGVScene.cpp | 2 +- plugins/dot_viewer/deps/QGVCore/private/QGVCore.h | 2 +- plugins/dot_viewer/src/dot_viewer.cpp | 3 +-- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/plugins/dot_viewer/CMakeLists.txt b/plugins/dot_viewer/CMakeLists.txt index de4035cd525..e9369a17012 100644 --- a/plugins/dot_viewer/CMakeLists.txt +++ b/plugins/dot_viewer/CMakeLists.txt @@ -21,7 +21,6 @@ if(PL_DOT_VIEWER OR BUILD_ALL_PLUGINS) SHARED HEADER ${DOT_VIEWER_INC} SOURCES ${DOT_VIEWER_SRC} ${DOT_VIEWER_PYTHON_SRC} ${MOC_HDR} - COMPILE_OPTIONS "-march=native" PYDOC SPHINX_DOC_INDEX_FILE ${CMAKE_CURRENT_SOURCE_DIR}/documentation/dot_viewer.rst ) diff --git a/plugins/dot_viewer/deps/QGVCore/CMakeLists.txt b/plugins/dot_viewer/deps/QGVCore/CMakeLists.txt index fe53d6e370a..f1ef17ca121 100644 --- a/plugins/dot_viewer/deps/QGVCore/CMakeLists.txt +++ b/plugins/dot_viewer/deps/QGVCore/CMakeLists.txt @@ -50,6 +50,11 @@ ADD_DEFINITIONS(-DQGVCORE_LIB -D_PACKAGE_ast -D_dll_import -D_BLD_cdt -D_DLL_BLD ADD_DEFINITIONS(-DQVGCORE_LIB) +# Graphviz up to 13 lets a reader supply its own memory discipline, later versions dropped it +if(HAS_AGDISC_MEM) + ADD_DEFINITIONS(-DHAS_AGDISC_MEM) +endif() + QT6_WRAP_UI(QGVLIB_UI_H ${qgvlib_UI}) QT6_ADD_RESOURCES(qgvlib_RCCS ${qgvlib_QRC}) diff --git a/plugins/dot_viewer/deps/QGVCore/QGVScene.cpp b/plugins/dot_viewer/deps/QGVCore/QGVScene.cpp index 085892031d9..79172305a5e 100644 --- a/plugins/dot_viewer/deps/QGVCore/QGVScene.cpp +++ b/plugins/dot_viewer/deps/QGVCore/QGVScene.cpp @@ -70,7 +70,7 @@ void QGVInteraction::disableHandler() QGVScene::QGVScene(QObject *parent) : QGraphicsScene(parent), _drawGrid(true) { -#ifdef HAS_AGDISK_MEM +#ifdef HAS_AGDISC_MEM aaglex_destroy(); #endif _context = new QGVGvcPrivate(gvContext()); diff --git a/plugins/dot_viewer/deps/QGVCore/private/QGVCore.h b/plugins/dot_viewer/deps/QGVCore/private/QGVCore.h index f82d0624f6f..b3d27b371da 100644 --- a/plugins/dot_viewer/deps/QGVCore/private/QGVCore.h +++ b/plugins/dot_viewer/deps/QGVCore/private/QGVCore.h @@ -85,7 +85,7 @@ class QGVCore static Agraph_t *agmemread2(const char *cp) { -#ifdef HAS_AGDISK_MEM +#ifdef HAS_AGDISC_MEM Agraph_t* g; rdr_t rdr; Agdisc_t disc; diff --git a/plugins/dot_viewer/src/dot_viewer.cpp b/plugins/dot_viewer/src/dot_viewer.cpp index 6ed347cfd34..14eff18a62d 100644 --- a/plugins/dot_viewer/src/dot_viewer.cpp +++ b/plugins/dot_viewer/src/dot_viewer.cpp @@ -184,11 +184,10 @@ namespace hal const char* target[] = {"node", "edge", nullptr }; for (int i=0; target[i]; i++) { - QAction* act = menu.addAction(QString("HAL color style overwrites %1 attibute").arg(target[i])); + QAction* act = menu.addAction(QString("HAL color style overwrites %1 attribute").arg(target[i])); act->setCheckable(true); act->setChecked(QGVStyle::instance()->getStyle((QGVStyle::StyleTarget)i) != QGVStyle::Graphviz); connect (act, &QAction::triggered, this, [target,i,this](bool checked){ - std::cerr << QString("HAL color style overwrites %1 attibute => %2").arg(target[i]).arg(checked?"true":"false").toStdString() << std::endl; if (checked) QGVStyle::instance()->setStyle((QGVStyle::StyleTarget)i, (QGVStyle::StyleType) MainWindow::sSettingStyle->value().toInt()); else From db43fb9800551a832ecf4e5a53544863ee537515 Mon Sep 17 00:00:00 2001 From: "julian.speith" Date: Fri, 11 Sep 2026 10:01:11 +0200 Subject: [PATCH 6/8] Expand the Homebrew prefix the dependency script writes into the shell config install_dependencies.sh appended export PATH="$BREW_PREFIX/..." lines to .zshrc and .bash_profile in single quotes, so the variable was never expanded and every such line prepended a path that does not exist. The lines are now written and checked with the prefix expanded. Also: the macOS workflow passed -DQt5_DIR at a Qt 6 install, which CMake ignored, and now passes Qt6_DIR; the Brewfile listed graphviz twice; and the pin group context menu said "Autmatically rename pins". Co-Authored-By: Claude Fable 5.1 --- .github/workflows/macOS.yml | 2 +- Brewfile | 1 - install_dependencies.sh | 32 +++++++++---------- .../module_pins_tree.cpp | 2 +- 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/.github/workflows/macOS.yml b/.github/workflows/macOS.yml index 6734c13339b..ff5b8678477 100644 --- a/.github/workflows/macOS.yml +++ b/.github/workflows/macOS.yml @@ -92,7 +92,7 @@ jobs: cd build export PATH="$(brew --prefix qt)/bin:$PATH" export LDFLAGS="-L$(brew --prefix qt)/lib -Wl,-rpath,$(brew --prefix llvm)/lib" - cmake -G Ninja .. -DQt5_DIR="$(brew --prefix qt)/lib/cmake" -DCMAKE_BUILD_TYPE=Debug -DBUILD_ALL_PLUGINS=ON -DBUILD_TESTS=ON -DPL_GUI=ON + cmake -G Ninja .. -DQt6_DIR="$(brew --prefix qt)/lib/cmake/Qt6" -DCMAKE_BUILD_TYPE=Debug -DBUILD_ALL_PLUGINS=ON -DBUILD_TESTS=ON -DPL_GUI=ON env: HAL_BASE_PATH: ${{runner.workspace}}/hal/build CCACHE_DIR: ${{runner.workspace}}/.ccache diff --git a/Brewfile b/Brewfile index e36983c06d3..7afddac434f 100644 --- a/Brewfile +++ b/Brewfile @@ -18,4 +18,3 @@ brew "z3" brew "boost" brew "readline" brew "verilator" -brew "graphviz" diff --git a/install_dependencies.sh b/install_dependencies.sh index a08329261b9..4f7219e5687 100755 --- a/install_dependencies.sh +++ b/install_dependencies.sh @@ -23,45 +23,45 @@ if [[ "$platform" == 'macOS' ]]; then pip3 install -r requirements.txt BREW_PREFIX=$(brew --prefix) if [ -n "$($SHELL -c 'echo $ZSH_VERSION')" ]; then - grep -Fxq 'export PATH="$BREW_PREFIX/opt/qt/bin:$PATH"' ~/.zshrc + grep -Fxq "export PATH=\"$BREW_PREFIX/opt/qt/bin:\$PATH\"" ~/.zshrc if ! [[ $? -eq 0 ]]; then - echo 'export PATH="$BREW_PREFIX/opt/qt/bin:$PATH"' >> ~/.zshrc + echo "export PATH=\"$BREW_PREFIX/opt/qt/bin:\$PATH\"" >> ~/.zshrc fi - grep -Fxq 'export PATH="$BREW_PREFIX/opt/llvm@14/bin:$PATH"' ~/.zshrc + grep -Fxq "export PATH=\"$BREW_PREFIX/opt/llvm@14/bin:\$PATH\"" ~/.zshrc if ! [[ $? -eq 0 ]]; then - echo 'export PATH="$BREW_PREFIX/opt/llvm@14/bin:$PATH"' >> ~/.zshrc + echo "export PATH=\"$BREW_PREFIX/opt/llvm@14/bin:\$PATH\"" >> ~/.zshrc fi - grep -Fxq 'export PATH="$BREW_PREFIX/opt/flex/bin:$PATH"' ~/.zshrc + grep -Fxq "export PATH=\"$BREW_PREFIX/opt/flex/bin:\$PATH\"" ~/.zshrc if ! [[ $? -eq 0 ]]; then - echo 'export PATH="$BREW_PREFIX/opt/flex/bin:$PATH"' >> ~/.zshrc + echo "export PATH=\"$BREW_PREFIX/opt/flex/bin:\$PATH\"" >> ~/.zshrc fi - grep -Fxq 'export PATH="$BREW_PREFIX/opt/bison/bin:$PATH"' ~/.zshrc + grep -Fxq "export PATH=\"$BREW_PREFIX/opt/bison/bin:\$PATH\"" ~/.zshrc if ! [[ $? -eq 0 ]]; then - echo 'export PATH="$BREW_PREFIX/opt/bison/bin:$PATH"' >> ~/.zshrc + echo "export PATH=\"$BREW_PREFIX/opt/bison/bin:\$PATH\"" >> ~/.zshrc fi source ~/.zshrc elif [ -n "$($SHELL -c 'echo $BASH_VERSION')" ]; then - grep -Fxq 'export PATH="$BREW_PREFIX/opt/qt/bin:$PATH"' ~/.bash_profile + grep -Fxq "export PATH=\"$BREW_PREFIX/opt/qt/bin:\$PATH\"" ~/.bash_profile if ! [[ $? -eq 0 ]]; then - echo 'export PATH="$BREW_PREFIX/opt/qt/bin:$PATH"' >> ~/.bash_profile + echo "export PATH=\"$BREW_PREFIX/opt/qt/bin:\$PATH\"" >> ~/.bash_profile fi - grep -Fxq 'export PATH="$BREW_PREFIX/opt/llvm@14/bin:$PATH"' ~/.bash_profile + grep -Fxq "export PATH=\"$BREW_PREFIX/opt/llvm@14/bin:\$PATH\"" ~/.bash_profile if ! [[ $? -eq 0 ]]; then - echo 'export PATH="$BREW_PREFIX/opt/llvm@14/bin:$PATH"' >> ~/.bash_profile + echo "export PATH=\"$BREW_PREFIX/opt/llvm@14/bin:\$PATH\"" >> ~/.bash_profile fi - grep -Fxq 'export PATH="$BREW_PREFIX/opt/flex/bin:$PATH"' ~/.bash_profile + grep -Fxq "export PATH=\"$BREW_PREFIX/opt/flex/bin:\$PATH\"" ~/.bash_profile if ! [[ $? -eq 0 ]]; then - echo 'export PATH="$BREW_PREFIX/opt/flex/bin:$PATH"' >> ~/.bash_profile + echo "export PATH=\"$BREW_PREFIX/opt/flex/bin:\$PATH\"" >> ~/.bash_profile fi - grep -Fxq 'export PATH="$BREW_PREFIX/opt/bison/bin:$PATH"' ~/.bash_profile + grep -Fxq "export PATH=\"$BREW_PREFIX/opt/bison/bin:\$PATH\"" ~/.bash_profile if ! [[ $? -eq 0 ]]; then - echo 'export PATH="$BREW_PREFIX/opt/bison/bin:$PATH"' >> ~/.bash_profile + echo "export PATH=\"$BREW_PREFIX/opt/bison/bin:\$PATH\"" >> ~/.bash_profile fi source ~/.bash_profile else diff --git a/plugins/gui/src/selection_details_widget/module_details_widget/module_pins_tree.cpp b/plugins/gui/src/selection_details_widget/module_details_widget/module_pins_tree.cpp index b198ee2686f..b66e013d7ba 100644 --- a/plugins/gui/src/selection_details_widget/module_details_widget/module_pins_tree.cpp +++ b/plugins/gui/src/selection_details_widget/module_details_widget/module_pins_tree.cpp @@ -243,7 +243,7 @@ namespace hal ActionPingroup* act = ActionPingroup::toggleAscendingGroup(mod, itemId); if (act) act->exec(); }); - menu.addAction("Autmatically rename pins", [itemId, mod](){ + menu.addAction("Automatically rename pins", [itemId, mod](){ ActionPingroup* act = ActionPingroup::automaticallyRenamePins(mod, itemId); if (act) act->exec(); }); From bcba68796aa338be28ad611a13fec5103c498e76 Mon Sep 17 00:00:00 2001 From: "julian.speith" Date: Fri, 11 Sep 2026 14:03:21 +0200 Subject: [PATCH 7/8] removed direct call to dot viewer from DANA --- .../include/dataflow_analysis/api/result.h | 7 ------ plugins/dataflow_analysis/src/api/result.cpp | 24 ------------------- .../dataflow_analysis/src/plugin_dataflow.cpp | 11 --------- plugins/solve_fsm/python/python_bindings.cpp | 1 - 4 files changed, 43 deletions(-) diff --git a/plugins/dataflow_analysis/include/dataflow_analysis/api/result.h b/plugins/dataflow_analysis/include/dataflow_analysis/api/result.h index 5e344fe7c64..8d347b74d28 100644 --- a/plugins/dataflow_analysis/include/dataflow_analysis/api/result.h +++ b/plugins/dataflow_analysis/include/dataflow_analysis/api/result.h @@ -148,13 +148,6 @@ namespace hal */ hal::Result> get_gate_predecessors(const Gate* gate) const; - /** - * @brief Open DOT graph in dot viewer if appropriate plugin was loaded. - * - * @param[in] out_path - The output path. - */ - void open_dot_in_viewer(const std::filesystem::path& out_path) const; - /** * @brief Write the dataflow graph as a DOT graph to the specified location. * diff --git a/plugins/dataflow_analysis/src/api/result.cpp b/plugins/dataflow_analysis/src/api/result.cpp index 0d3f339ffca..b02b1f3c7e9 100644 --- a/plugins/dataflow_analysis/src/api/result.cpp +++ b/plugins/dataflow_analysis/src/api/result.cpp @@ -4,8 +4,6 @@ #include "hal_core/netlist/gate_library/gate_type.h" #include "hal_core/netlist/module.h" #include "hal_core/netlist/netlist.h" -#include "hal_core/plugin_system/gui_extension_interface.h" -#include "hal_core/plugin_system/plugin_manager.h" #include "hal_core/utilities/log.h" #include "hal_core/utilities/utils.h" @@ -247,28 +245,6 @@ namespace hal return OK({}); } - void dataflow::Result::open_dot_in_viewer(const std::filesystem::path& out_path) const - { - BasePluginInterface* bpif = plugin_manager::get_plugin_instance("dot_viewer"); - if (!bpif) - { - log_info("dataflow", "Cannot find 'dot_viewer' plugin, dot graph not displayed."); - return; - } - GuiExtensionInterface* geif = bpif->get_first_extension(); - if (!geif) - { - log_info("dataflow", "Cannot find dot_viewer GUI interface, dot graph not displayed."); - return; - } - std::vector params; - params.push_back(PluginParameter(PluginParameter::ExistingFile, "filename", "", out_path.string())); - params.push_back(PluginParameter(PluginParameter::String, "plugin", "", "dataflow")); - params.push_back(PluginParameter(PluginParameter::PushButton, "exec", "", "clicked")); - geif->set_parameter(params); - log_info("dataflow", "Request to display graph '{}' send to dot viewer.", out_path.string()); - } - hal::Result dataflow::Result::write_dot(const std::filesystem::path& out_path, const std::unordered_set& group_ids) const { auto write_path = out_path; diff --git a/plugins/dataflow_analysis/src/plugin_dataflow.cpp b/plugins/dataflow_analysis/src/plugin_dataflow.cpp index 65fb038ef33..2a27c0babf1 100644 --- a/plugins/dataflow_analysis/src/plugin_dataflow.cpp +++ b/plugins/dataflow_analysis/src/plugin_dataflow.cpp @@ -212,7 +212,6 @@ namespace hal return; } auto grouping = grouping_res.get(); - std::filesystem::path dot_graph_written_to_path; if (m_write_dot) { @@ -220,10 +219,6 @@ namespace hal { log_error("dataflow", "could not write .dot file:\n{}", res.get_error().get()); } - else - { - dot_graph_written_to_path = res.get(); - } } if (m_write_txt) @@ -241,12 +236,6 @@ namespace hal log_error("dataflow", "could not create modules:\n{}", res.get_error().get()); } } - - // open in dot viewer must be called after modules got created - if (!dot_graph_written_to_path.empty()) - { - grouping.open_dot_in_viewer(dot_graph_written_to_path); - } } diff --git a/plugins/solve_fsm/python/python_bindings.cpp b/plugins/solve_fsm/python/python_bindings.cpp index 29e2538179e..4d555266bc9 100644 --- a/plugins/solve_fsm/python/python_bindings.cpp +++ b/plugins/solve_fsm/python/python_bindings.cpp @@ -264,7 +264,6 @@ namespace hal Each state becomes a node labeled with its value and, if outputs were computed, with the value of every output in that state. Each transition becomes an edge labeled with its condition. - If the ``dot_viewer`` plugin is available, the written graph is additionally offered to it for display. :param pathlib.Path graph_path: The file path at which to store the graph. No file is written if the path is left empty. Defaults to an empty path. :param int max_condition_length: The maximum number of characters printed for a Boolean function. Defaults to 128. From 3dea4d05ae0e99ae898c4ad58d35513d036abf18 Mon Sep 17 00:00:00 2001 From: "julian.speith" Date: Fri, 11 Sep 2026 14:03:29 +0200 Subject: [PATCH 8/8] changelog cleanup --- CHANGELOG.md | 348 ++++++++++++++++++++++++++------------------------- 1 file changed, 178 insertions(+), 170 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bce7109b0e1..160c6f2d809 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,205 +3,213 @@ All notable changes to this project will be documented in this file. ## [Unreleased] * Core - * fixed crash when passing a `nullptr` pin to `Net::remove_source` or `Net::remove_destination`, which is also reachable from Python - * changed `Net` and `Gate` to identify a pin by pointer identity instead of by value when looking up an endpoint - * sped up deleting gates, nets and modules, which searched the object vectors of the netlist and of the owning module linearly for the element to remove; every such vector now keeps the position of its elements in a map (`utils::indexed_vector_push_back` and `indexed_vector_erase`) and removal is a constant-time swap with the last element - * sped up `Module::is_parent_module_of`, which walked the whole subtree of the module, for a top-level module the entire hierarchy, once per endpoint whenever module nets are recomputed; it now walks up the parent chain of the queried module, bounded by the depth of the hierarchy. Together with the constant-time removal, parsing the 216 MB OpenTitan Earl Grey Verilog netlist of 25,906 modules and 2.3 million endpoints went from 709 s to 31 s - * fixed `utils::split` reading the last character of an empty string, which is undefined behaviour - * progress and layout reporting - * added `ProgressScope` and `LayoutLocker` to the core, which report progress and suppress layout updates through the user interface plugin looked up at runtime, replacing the copies of `GuiLayoutLocker` in the dataflow analysis and module identification plugins - * added `UIPluginInterface::set_progress` and `plugin_manager::get_ui_plugin`, so that a plugin no longer needs to provide a `GuiExtensionInterface` and register a callback just to report its progress - * moved the progress bar of the dataflow analysis into the core as `user_feedback::ProgressPrinter`, which reports to the terminal and to the user interface at once and brackets the operation like a `ProgressScope` does, so that a plugin reports its progress once and reaches whoever is watching - * fixed the progress overlay of the graph view staying up until it is clicked away after a dataflow analysis that was started from a script instead of from the plugin dialog, only the dialog reported the analysis as finished although both report its progress - * fixed the progress overlay of the graph view never being dismissed after a module identification run, the call reporting the analysis as finished was commented out - * fixed the progress overlay of the graph view being dismissed while the layout updates deferred during a dataflow analysis were still being applied, which left the graph view showing its spinner - * fixed the GUI freezing while a dataflow analysis started from its dialog reported its progress: the worker threads of the analysis updated the progress overlay directly, called `processEvents` from a thread that is not the GUI thread, and did so while holding the mutex that guards the results. Progress reported from another thread is now posted to the GUI thread, and the overlay names the phase of the analysis instead of a generic message - * program options - * added `ProgramOptions::add_flags` that takes the flags and parameters as vectors so that they can be assembled at runtime - * netlist traversal - * deprecated `netlist_utils::get_nets_at_pins` without a relocation: it is a per-pin lookup that `Gate::get_fan_in_net` and `get_fan_out_net` already provide, and it has no callers - * moved `get_common_inputs` from `netlist_utils` onto `NetlistTraversalDecorator` and deprecated the original - * added `NetlistTraversalDecorator::make_traversal_cache` and `get_gates` overloads that share results across calls through a `TraversalCache`. The traversal a cache answers for is sealed in at creation -- direction, match condition, stop rule and endpoint filters -- so a cache can never be consulted by a walk asking a different question, and everything that would make a cached answer depend on how a net was reached is excluded by construction - * fixed `get_next_sequential_gates`, `get_combinational_cone` and `get_next_sequential_gates_map` returning results with gates missing through a shared cache when the netlist contains a combinational cycle. Cache entries were written while a net was still being explored, and a cycle that led the walk back to such a net baked the partial answer into the entries of the nets in flight, so a later call reaching one of them through a side path was silently wrong -- which is how the Boolean influence plugin produced a wrong flip-flop dependency matrix on such netlists. An entry is now published only once its net, and any cycle it belongs to, is fully explored - * deprecated the four `netlist_utils::get_path` overloads, which despite the name return every gate of a cone rather than a path, in favour of `NetlistTraversalDecorator::get_gates` with a negated condition and `TraversalStop::at_mismatch` - * added `NetlistTraversalDecorator::get_gates`, the traversal that the other traversals of the decorator are special cases of. What separated them from one another was never what they collect but where they stop relative to it, which is now said out loud by a `TraversalStop` of `at_match`, `at_mismatch` or `never` rather than implied by a pair of booleans named one syllable apart. Direction is a `TraversalDirection` rather than a bare `bool successors` - * deprecated the three `netlist_utils::get_shortest_path` overloads in favour of `NetlistTraversalDecorator::get_shortest_path`, and `netlist_utils::get_ff_dependency_matrix` in favour of the one in the Boolean influence plugin, which also reports how strongly each flip-flop depends on another rather than only whether it does - * moved `get_gate_chain` and `get_complex_gate_chain` from `netlist_utils` onto `NetlistTraversalDecorator`, where the rest of the traversal lives and where a binding can keep the netlist alive for as long as Python refers to the gates it returns - * renamed `NetlistTraversalDecorator::get_next_combinational_gates` to `get_combinational_cone`, which is what it returns -- every combinational gate up to the sequential boundary, not a next layer of anything -- and removed the raw result-map cache parameter from it and from `get_next_sequential_gates`, whose reuse contract nothing enforced. Repeated traversals share results through a sealed `TraversalCache` instead. The Python bindings of both now also default `forbidden_pins` to an empty set like the C++ side always did - * added `NetlistTraversalDecorator::get_shortest_path` overloads that end at any gate of a module and that connect two modules. The former existed only as a free function in `netlist_utils` before, the latter is new - * gate library - * fixed reloading a gate library destroying the library a netlist was built against, which silently replaced every gate type of that netlist. Gate libraries are now owned through a `shared_ptr` and outlive both the netlists and the Python handles that refer to them - * added the `GateTypeProperty::delay` gate type property for dedicated delay cells, such as those of a clock distribution network, which HGL files read and write as `delay` + * netlist + * changed `Net` and `Gate` to identify a pin by pointer identity instead of by value when looking up an endpoint + * sped up deleting gates, nets and modules, which searched the object vectors of the netlist and of the owning module linearly; every such vector now tracks the positions of its elements and removal is a constant-time swap + * sped up `Module::is_parent_module_of`, which walked the whole subtree of the module once per endpoint whenever module nets are recomputed; it now walks up the parent chain of the queried module. Together with the constant-time removal, parsing the OpenTitan Earl Grey netlist of 25,906 modules went from 709 s to 31 s + * fixed `utils::split` reading the last character of an empty string + * fixed crash when passing a `nullptr` pin to `Net::remove_source` or `Net::remove_destination` * module pins - * sped up assigning gates to a module and removing them from it, `Module::get_pin_by_net` scanned every pin of the module and runs for every net of every gate that enters or leaves it; the pins are now indexed by net - * added `PinChangedBulkScope`, which collects the pin events of a bulk operation and sends a single `PinEvent::PinsReload` per affected module in their place, telling a listener to re-read the pins of that module. Assigning gates to modules opens one, which took creating and deleting 24 modules on a 7144 gate netlist from 43040 `pin_changed` events to 48; each event triggered from a Python script cost a round trip to the GUI thread, which was 91% of the script's runtime. Interactive pin changes keep their fine-grained events, and the pins tree of the GUI keeps its expanded groups and its selection across the reload - * fixed `Module::set_pin_group_direction` sending a `GroupTypeChange` pin event instead of `GroupDirChange`, so the pins tree of the GUI did not show the new direction of the group - * fixed the pin events collected while several pins are assigned to a group being delivered in pin ID order rather than in the order the rows come into being, so the pins tree of the GUI could insert a pin at a row its group item did not have yet and crash or show the pins out of order; an out-of-range row is now clamped as well + * sped up assigning gates to a module and removing them from it, `Module::get_pin_by_net` scanned every pin of the module for every net of every gate that enters or leaves it; the pins are now indexed by net + * added `PinChangedBulkScope`, which collects the pin events of a bulk operation and sends a single `PinEvent::PinsReload` per affected module instead. Assigning gates to modules opens one, while interactive pin changes keep their fine-grained events * changed the default pin order of both `Module::create_pin_group` overloads from ascending to descending, in C++ and in Python, matching the order the GUI, the dataflow analysis and the module identification produce. In Python, `start_index` now defaults to the index of the last pin for a descending group and to 0 for an ascending one -* Boolean functions - * added `to_string` to `SMT::QueryConfig`, `SMT::Model` and `SMT::SolverResult`, so that all four SMT types offer it the way `SMT::Constraint` already did instead of only an `operator<<` - * fixed the printed form of an `SMT::Model` starting with a stray comma, `{, A:5}` instead of `{A:5}` - * sped up `BooleanFunction::compute_truth_table` by evaluating 64 rows of the table at once instead of running a symbolic execution per row, which walks and simplifies the entire node list every single time. Applies to single-bit functions of bitwise operations whose variables are all part of the truth table, everything else keeps using the previous implementation - * raised the limit on the number of variables a truth table may be computed for from 10 to 20, see `BooleanFunction::MAX_TRUTH_TABLE_VARIABLES` - * sped up the evaluation of Boolean functions, `BooleanFunction::operator<` compared two functions by building and comparing their reverse polish notation strings, which the symbolic state hit on every variable lookup - * fixed silent truncation of additions and subtractions, results of operands wider than 32 bit lost their upper bits - * fixed `Eq` reporting a definite inequality when an undefined bit could have made the two values equal, it now reports an undefined result like the other comparisons do - * added constant folding for the `Sdiv`, `Udiv`, `Srem` and `Urem` operations, which were not implemented and made evaluation of any function containing them fail, following the SMT-LIB definitions these operations are translated to - * sped up evaluation with constant inputs by about 3x by folding the values directly instead of building a Boolean function per operation, which dominates the runtime of `compute_truth_table()` and thereby of the HAWKEYE S-box identification - * fixed `SMT::Solver::has_local_solver_for` testing `SolverCall::Binary` in both of its branches, so the branch for `SolverCall::Library` was unreachable and library availability always reported `false` - * fixed `SMT::SymbolicState::set` using `emplace`, which leaves an existing binding untouched, so setting a variable a second time did nothing and a loop stepping a symbolic state forward silently kept the value it started with - * added simplification rules for the word level operations, which the single-bit simplification through ABC cannot reach: extensions to the width the value already has, nested extensions and slices, slices that fall into one half of a concatenation or into either part of an extension, unsigned comparisons against zero and the maximum, equality of a value with its own negation, and single bit equalities and selections - * fixed `BooleanFunction::to_string` reading past the end of its digit table when a bit-vector that contains an `X` or `Z` is converted to an octal or hexadecimal string, the undefined bit was folded into the table index before the digit was replaced by `X` - * fixed `BooleanFunction::from_string` rejecting Liberty expressions that combine Liberty-only syntax such as `A'` or `+` with spaces around the operators, which the Liberty grammar reads as additional AND operations; if neither grammar accepts an expression, it is parsed once more with every space removed, added as `BooleanFunctionParser::ParserType::LibertyNoSpace` - * added `SMT::SymbolicState::get_bindings`, which returns the variables bound in a symbolic state indexed by their name, so that a caller resolving many variables of one state no longer builds a Boolean function as the lookup key for each -* Python bindings - * fixed the four `boolean_influence` functions that return influences per net handing out the nets without keeping the netlist alive: they return dicts keyed by net, and nothing protected a borrowed object sitting in a dict key - * added a warning, once per function and process, when a deprecated `NetlistUtils` function is called from Python, naming its replacement. `[[deprecated]]` warns whoever compiles, and a script has no compiler - * fixed the deprecated `NetlistUtils` bindings handing out gates and nets without keeping the netlist alive for as long as Python refers to them, which they keep doing until they are removed - * fixed `netlist_preprocessing.create_multi_bit_gate_modules` and `create_nets_at_unconnected_pins` handing out modules and nets without keeping the netlist alive: the `hal::borrowed()` call policy ties each returned object to the netlist that owns it, which works on a module-level function as well, as the owner is found through the wrapper the caller necessarily passed in - * fixed the Python bindings handing out gates, nets, modules, endpoints and pins without tying them to the netlist that owns them, so that dropping the netlist left them pointing into freed memory. Reading 500 gates and 500 nets of a dropped netlist returned the wrong name and ID for 184 and 230 of them respectively, silently rather than by crashing - * fixed the decorators storing a reference to the netlist or net they were constructed from without keeping it alive - * fixed `NetlistGraph` never being freed by Python: its factories hand over ownership but it was bound with a non-owning holder, so every graph built from a netlist leaked, more than a gigabyte over 1500 graphs on a 3458 gate netlist - * fixed `GateLibrary::get_path` and the `path` property returning the name of the library instead of its path - * fixed `GuiApi::getSelectedModules` and `getSelectedItems` not tying the returned modules to the netlist - * fixed `GuiApi::selectGate`, `selectNet` and `selectModule` with `clear_current_selection` set, which is the default, keeping the previous selection: the pending selection was cleared but the current one was united back into it right afterwards - * added Python bindings for `NetlistGraph::from_gates`, `is_shadow_vertex`, and `get_all_vertices_from_gate` - * added Python bindings for `ProgramOptions`, `ProgramArguments`, and `FacExtensionInterface` - * added Python bindings for the remaining functions of `plugin_manager` and exposed the `initialize` and `silent` parameters of `get_plugin_instance` - * fixed `GateLibraryManager.get_gate_libraries` handing each library to Python as a newly constructed `shared_ptr` over a pointer it had only borrowed, which opened a second ownership group over a library the manager already owned and freed it twice - * fixed `Module.pins`, `Module.pin_groups` and `GateType.components` raising a `TypeError` whenever they were read, as each was bound to a method whose only parameter has a default in C++, which pybind11 exposes as a required argument that a property cannot pass - * added a test that calls every no-argument binding reachable from a small netlist and imports every plugin module, so that a binding which compiles and only fails when called is caught - * changed every binding that hands out a borrowed object to keep its **owner** alive rather than the object it was read from, through the new `hal::borrowed()` call policy that replaces `py::return_value_policy::reference_internal` at 241 places. The policy was only applied while a wrapper was being created, so whether an object was protected depended on which binding happened to hand it over first, and a module read from a gate was tied to that gate although the netlist is what owns it - * fixed `DataContainer`, `ProjectDirectory`, `hawkeye.DetectionConfiguration`, `hawkeye.SBoxDatabase` and `dataflow.Configuration` leaking every instance created from Python, as each was bound with a holder that never frees. `SBoxDatabase.from_file` leaked 25 KB per call, and `ProjectManager.get_project_directory` leaked a copy on every call, as pybind11 copies a returned reference by default - * fixed `SMT.SymbolicExecution.evaluate` raising a `TypeError` on every call: both overloads were bound directly, so they returned an unregistered `Result`, where every other binding in that file unwraps it - * fixed the `hal::borrowed()` call policy having no effect on any of the 55 properties it was given to, so those still handed out a borrowed object without keeping its owner alive. `def_property_readonly` builds the getter itself before it forwards the attributes that follow, so a call policy given to a property never reaches the function that performs the call - * fixed a Python interpreter that loaded the HAL plugins segfaulting on the way out unless it unloaded them again by hand, as the plugin libraries were closed while the parser and writer registries still held a factory function out of each of them - * added Python bindings for `SMT.SolverCall` and `SMT.Solver.to_smt2`, and the missing `Bitwuzla` value of `SMT.SolverType`. Without `SolverCall`, neither `QueryConfig.with_call` nor `Solver.has_local_solver_for` could be called at all although both were bound - * fixed three enum values that were bound to a different value of their own enum, which made them indistinguishable from Python: `GateTypeProperty.fifo` was bound to `ram`, `module_identification.CandidateType.addition_offset` to `addition`, and `gui_extension_demo.ParameterType.Module` to `Gate` - * added `to_string` and `__str__` to `SMT.QueryConfig`, `SMT.Constraint`, `SMT.Model` and `SMT.SolverResult`, printing any of them showed an object address before - * fixed every binding that carries the `hal::borrowed()` call policy segfaulting when it is called with arguments that its first overload does not accept, which happens with pybind11 3.1 and newer: the post-call hook is now also run for an overload whose arguments did not load and is handed a sentinel instead of an object. `Netlist.create_module` with a name, a parent, and a list of gates was the first such call in the binding smoke test, which is why the macOS CI, whose Homebrew pybind11 moved to 3.1.0 in September 2026, failed while the Ubuntu jobs on older pybind11 did not - * added `log_trace`, `log_debug`, `log_info`, `log_warning`, `log_error` and `log_critical` to `hal_py`, which take the channel first like the C++ macros and prefix every severity but `info` with the file and line of the calling Python code - * changed `BooleanFunction.nodes` from a method to a read-only property, which is what its documentation always claimed - * fixed `GateLibrary.get_gate_location_data_identifiers` being unreachable from Python, as it was registered under the name of `get_gate_location_data_category` - * fixed keyword arguments that did not match the documentation: `GateType.add_boolean_function(name)` was `pin_name`, `Module.contains_module(recursive)` was spelled `recusive`, `NetlistFactory.load_netlist_from_string(netlist_string)` was `hdl_string`, and `GateType.has_property(property)` had no keyword at all - * added the missing `delay` value of `GateTypeProperty`, which the enum gained in C++ but the binding never listed + * fixed `Module.pins` and `Module.pin_groups` raising a `TypeError` whenever they were read from Python + * fixed `Module::set_pin_group_direction` sending a `GroupTypeChange` pin event instead of `GroupDirChange` + * fixed the pin events of assigning several pins to a group being delivered in pin ID order rather than in the order the rows come were created, which could crash the pins tree of the GUI + * gate library + * added the `GateTypeProperty::delay` gate type property for dedicated delay cells, such as those of a clock distribution network, which HGL files read and write as `delay` + * fixed reloading a gate library destroying the library a netlist was built against, which silently replaced every gate type of that netlist. Gate libraries are now owned through a `shared_ptr` and outlive both the netlists and the Python handles that refer to them + * fixed `GateLibraryManager.get_gate_libraries` handing each library to Python as a newly constructed `shared_ptr` over a pointer it had only borrowed, which freed the library twice + * fixed `GateLibrary::get_path` and the `path` property returning the name of the library instead of its path + * fixed `GateLibrary.get_gate_location_data_identifiers` being registered under the name of `get_gate_location_data_category` in Python + * fixed `GateTypeProperty.fifo` being bound to `ram` in Python + * fixed `GateType.components` raising a `TypeError` whenever it was read from Python + * Boolean functions + * sped up `BooleanFunction::compute_truth_table` by evaluating 64 rows of the table at once instead of running a symbolic execution per row. Applies to single-bit functions of bitwise operations whose variables are all part of the truth table + * raised the limit on the number of variables a truth table may be computed for from 10 to 20, see `BooleanFunction::MAX_TRUTH_TABLE_VARIABLES` + * sped up the evaluation of Boolean functions, `BooleanFunction::operator<` compared two functions by building and comparing their reverse polish notation strings + * sped up evaluation with constant inputs by about 3x by folding the values directly instead of building a Boolean function per operation + * added constant folding for the `Sdiv`, `Udiv`, `Srem` and `Urem` operations + * added simplification rules for the word level operations: extensions to the width the value already has, nested extensions and slices, slices that fall into one half of a concatenation or into either part of an extension, unsigned comparisons against zero and the maximum, equality of a value with its own negation, and single bit equalities and selections + * fixed the constant folding of `Add` and `Sub` masking the result to 32 bit before cutting it to the operand width, so for operands of 33 to 64 bit the upper bits of the result were always zero + * fixed `Eq` reporting a definite inequality when an undefined bit could have made the two values equal, it now reports an undefined result like the other comparisons do + * fixed `BooleanFunction::to_string` reading past the end of its digit table when a bit-vector that contains an `X` or `Z` is converted to an octal or hexadecimal string + * fixed `BooleanFunction::from_string` rejecting Liberty expressions that combine Liberty-only syntax such as `A'` or `+` with spaces around the operators; if neither grammar accepts an expression, it is parsed once more with every space removed, added as `BooleanFunctionParser::ParserType::LibertyNoSpace` + * changed `BooleanFunction.nodes` from a method to a read-only property in Python, which is what its documentation always claimed + * SMT + * added `to_string` to `SMT::QueryConfig`, `SMT::Model` and `SMT::SolverResult`, so that all four SMT types offer it the way `SMT::Constraint` already did, and `__str__` to all four in Python + * added `SMT::SymbolicState::get_bindings`, which returns the variables bound in a symbolic state indexed by their name + * added Python bindings for `SMT.SolverCall` and `SMT.Solver.to_smt2`, and the missing `Bitwuzla` value of `SMT.SolverType`. Without `SolverCall`, neither `QueryConfig.with_call` nor `Solver.has_local_solver_for` could be called from Python + * fixed the printed form of an `SMT::Model` starting with a stray comma, `{, A:5}` instead of `{A:5}` + * fixed `SMT::Solver::has_local_solver_for` testing `SolverCall::Binary` in both of its branches, so library availability always reported `false` + * fixed `SMT::SymbolicState::set` using `emplace`, which leaves an existing binding untouched, so setting a variable a second time did nothing + * fixed `SMT.SymbolicExecution.evaluate` raising a `TypeError` on every call from Python, as both overloads returned an unregistered `Result` instead of unwrapping it + * netlist traversal + * added `NetlistTraversalDecorator::get_gates`, the traversal that the other traversals of the decorator are special cases of: where a traversal stops is now said by a `TraversalStop` of `at_match`, `at_mismatch` or `never`, and its direction by a `TraversalDirection`, instead of a pair of booleans + * added `NetlistTraversalDecorator::make_traversal_cache` and `get_gates` overloads that share results across calls through a `TraversalCache`. The specific traversal a cache answers for is set at creation, so a cache can never be consulted by a walk with different parameters + * added `NetlistTraversalDecorator::get_shortest_path` overloads that end at any gate of a module and that connect two modules. The former existed only as a free function in `netlist_utils` before, the latter is new + * renamed `NetlistTraversalDecorator::get_next_combinational_gates` to `get_combinational_cone`, which is what it returns, and removed the raw result-map cache parameter from it and from `get_next_sequential_gates` in favour of a `TraversalCache`. The Python bindings of both now default `forbidden_pins` to an empty set like the C++ side always did + * moved `get_common_inputs`, `get_gate_chain` and `get_complex_gate_chain` from `netlist_utils` onto `NetlistTraversalDecorator` and deprecated the originals + * deprecated the four `netlist_utils::get_path` overloads, which despite the name return every gate of a cone rather than a path, in favour of `NetlistTraversalDecorator::get_gates` with a negated condition and `TraversalStop::at_mismatch` + * deprecated the three `netlist_utils::get_shortest_path` overloads in favour of `NetlistTraversalDecorator::get_shortest_path`, and `netlist_utils::get_ff_dependency_matrix` in favour of the one in the Boolean influence plugin + * deprecated `netlist_utils::get_nets_at_pins` without a relocation: `Gate::get_fan_in_net` and `get_fan_out_net` already provide the lookup, and it has no callers + * added a warning, once per function and process, when a deprecated `NetlistUtils` function is called from Python, naming its replacement + * fixed `get_next_sequential_gates`, `get_combinational_cone` and `get_next_sequential_gates_map` returning results with gates missing through a shared cache when the netlist contains a combinational cycle, which is how the Boolean influence plugin produced a wrong flip-flop dependency matrix on such netlists. A cache entry is now published only once its net, and any cycle it belongs to, is fully explored + * fixed the deprecated `NetlistUtils` bindings handing out gates and nets without keeping the netlist alive for as long as Python refers to them + * progress and layout reporting + * added `user_feedback::ProgressScope`, `LayoutLocker` and `ProgressPrinter` to the core: a scope brackets an operation in the progress display of the user interface, a locker suppresses layout updates while it exists, and a printer reports progress to the terminal and the user interface at once. All three do nothing when no user interface is loaded, so a plugin no longer needs a `GuiExtensionInterface` and a callback of its own to report progress, and the copies of `GuiLayoutLocker` in the dataflow analysis and the module identification are gone + * added `UIPluginInterface::set_progress` next to the existing `set_layout_locker`, and `plugin_manager::get_ui_plugin` to look the user interface up at runtime + * fixed the progress overlay of the graph view never being dismissed after a module identification run, and staying up after a dataflow analysis started from a script, which only reported the analysis as finished from its plugin dialog + * fixed the progress overlay being dismissed while the layout updates deferred during a dataflow analysis were still being applied + * fixed the GUI freezing while a dataflow analysis reported its progress from its worker threads; progress reported from another thread is now posted to the GUI thread + * plugin system + * added `ProgramOptions::add_flags` that takes the flags and parameters as vectors so that they can be assembled at runtime + * added Python bindings for `ProgramOptions`, `ProgramArguments` and `FacExtensionInterface`, and for the remaining functions of `plugin_manager`, exposing the `initialize` and `silent` parameters of `get_plugin_instance` + * added `log_trace`, `log_debug`, `log_info`, `log_warning`, `log_error` and `log_critical` to `hal_py`, which take the channel first like the C++ macros and prefix every severity but `info` with the file and line of the calling Python code + * fixed a Python interpreter that loaded the HAL plugins segfaulting on the way out unless it unloaded them again by hand, as the plugin libraries were closed while the parser and writer registries still held a factory function out of each of them + * Python bindings + * added a test that calls every no-argument binding reachable from a small netlist and imports every plugin module, so that a binding which compiles and only fails when called is caught + * fixed every binding that hands out a borrowed object to keep its **owner** alive rather than the object it was read from, through the new `hal::borrowed()` call policy that replaces `py::return_value_policy::reference_internal`. Before, dropping a netlist left gates, nets, modules, endpoints and pins read from it pointing into freed memory, and a module read from a gate was tied to that gate although the netlist owns it + * fixed the decorators storing a reference to the netlist or net they were constructed from without keeping it alive + * fixed `DataContainer` and `ProjectDirectory` leaking every instance created from Python, as each was bound with a holder that never frees; `ProjectManager.get_project_directory` leaked a copy on every call + * Plugins - * Boolean influence - * fixed `get_ff_dependency_matrix` dereferencing an uninitialized pointer on every call, which segfaulted before it returned anything. The cache it passes on was never initialized, and a pointer that is not null passed the callee's check for one - * HAWKEYE - * replaced `RegisterCandidate`, `RoundCandidate` and the free S-box functions of HAWKEYE with a single `CipherCandidate` that analyzes a candidate in place instead of copying it into a netlist of its own, so its gates and nets are the ones of the netlist under analysis and no longer have to be mapped back - * added `CipherCandidate::identify_sboxes` that identifies every S-box of a candidate at once and annotates it with the outcome, grouping the variants the search produces of one and the same S-box and leaving a group as soon as one of them matches - * added `CipherCandidate::create_modules` that writes a candidate back into the netlist as a module hierarchy of the candidate, its state register, and one submodule per identified S-box - * fixed the candidates of HAWKEYE being ordered by the addresses of their gates, which made the result of `detect_candidates` depend on where the gates of the netlist happened to be allocated and hence differ between runs of the same binary. Two candidates sharing size and input register could also compare equal and silently discard one another, which cost an entire candidate and the S-box identification that depended on it - * fixed `RegisterCandidate::operator==` never reporting a round-based candidate as equal to itself - * fixed the S-box search of HAWKEYE calling `std::includes` on the unsorted result of `get_unique_predecessors`, which decided by an order that is not guaranteed which inverters it drops from an S-box. This made the number of located S-boxes differ between runs of the same binary, 476 to 1508 across three runs of a netlist that holds 16 - * fixed the round function of HAWKEYE walking every path from each flip-flop of the register rather than every gate, which is exponential in a cone of logic that reconverges. Computing it for a 514 flip-flop candidate took 179 seconds and now takes 0.21 seconds with an unchanged result - * fixed the linear independence check of HAWKEYE shifting by more than the width of its type for S-boxes of more than 6 bits, which is undefined and made the check operate on garbage for 7-bit and 8-bit S-boxes - * sped up the S-box identification of HAWKEYE by tabulating each output over the state and the control inputs together and reading the assignment of the control inputs out of that one table, instead of substituting the control values and tabulating anew for each of up to 256 assignments - * changed the round function of a HAWKEYE candidate to determine which flip-flops each of its gates depends on only when the S-box search asks for it, as that is the most expensive part of analyzing a candidate and nothing else reads the result - * fixed `SBoxDatabase::lookup` never terminating for an 8-bit S-box that is not contained in the database, as it counted the constant it adds to the outputs in a `u8`, which never reaches 256 - * fixed `SBoxDatabase::store` reporting a failure although it had written the database, and made it report one if the file cannot be opened - * added a limit to the canonical form search behind an S-box lookup, which finishes quickly for a real S-box but does not terminate in reasonable time for a table that is close to linear, such as two 4-bit S-boxes glued into an 8-bit one by the surrounding logic - * added the HAWKEYE S-box database to the build directory so that it is found at runtime, and clarified that `identify_sbox` returning an empty string means no match rather than an error - * graph algorithm - * added `NetlistGraph::from_gates` that builds a graph from a subset of the gates of a netlist, optionally representing a gate by a primary and a shadow vertex so that feedback through it does not close a cycle - * fixed `NetlistGraph` destroying an igraph graph that was never created when `from_netlist` or `copy` fail before creating it, and made `NetlistGraph` non-copyable, as the implicitly generated copy shared the igraph internals between two objects that both freed them * clock tree extractor - * added the `clock_tree_extractor` plugin, which recovers the clock distribution network of a gate-level netlist. Starting at the clock pin of every flip-flop it walks against the signal direction through buffers, inverters, delay gates, clock gates and toggle-flip-flop clock dividers up to the global input nets that drive them, and returns the result as a `ClockTree`, a directed igraph graph whose vertices are gates and nets of the netlist. Reconvergent clock networks, i.e. meshes, are extracted as well. Available from Python as the `clock_tree_extractor` module - * added `ClockTree::from_netlist` that extracts the clock tree of a netlist, leaving out latches, power and ground nets, and the control inputs of clock gates, and reporting every unrouted or multi-driven clock net it skips as a warning - * added `ClockTree::export_dot` that writes the tree as a DOT graph, in which every gate carries its instance name, gate type and, where the netlist provides them, its `X` and `Y` coordinates as node attributes, is shaped by kind (buffer, inverter, delay gate, flip-flop, clock gate), and every edge downstream of an inverter switches colour, so that the polarity of the clock at each gate can be read off the graph - * added `ClockTree::get_subtree` that returns the clock tree below a gate or net as a `ClockTree` of its own, optionally starting one level up at the parent of the given object if it has exactly one + * added the `clock_tree_extractor` plugin, which recovers the clock distribution network of a gate-level netlist. Starting at the clock pin of every flip-flop it walks against the signal direction through buffers, inverters, delay gates, clock gates and toggle-flip-flop clock dividers up to the global input nets that drive them, and returns the result as a `ClockTree`. Reconvergent clock networks, i.e. meshes, are extracted as well. + * added `ClockTree::from_netlist` that extracts the clock tree of a netlist + * added `ClockTree::export_dot` that writes the tree as a DOT graph + * added `ClockTree::get_subtree` that returns the clock tree below a gate or net as a `ClockTree` of its own * added `ClockTree::get_neighbors`, bound to Python as `get_parents` and `get_childs`, that returns the gates and nets directly upstream or downstream of an object in the clock tree * added `ClockTree::get_gates`, `get_nets`, `get_all` and `get_netlist` that list the gates and nets of a clock tree and hand back the netlist it was built from - * added `ClockTree::get_vertex_from_ptr`, `get_ptr_from_vertex`, `get_vertices_from_ptrs` and `get_ptrs_from_vertices` that translate between gates or nets and their igraph vertex IDs, and `ClockTree::get_igraph` that exposes the underlying `igraph_t` to C++ callers, so that any igraph algorithm can be run on the clock tree + * added `ClockTree::get_vertex_from_ptr`, `get_ptr_from_vertex`, `get_vertices_from_ptrs` and `get_ptrs_from_vertices` that translate between gates or nets and their igraph vertex IDs, and `ClockTree::get_igraph` that exposes the underlying `igraph_t` to C++ callers + * dot viewer + * added the `dot_viewer` plugin, which renders a Graphviz `.dot` file inside the GUI and, for a file written by another HAL plugin, ties the graph to the netlist + * added Python function `load_dot_file()` to open a `.dot` file in the viewer + * HAWKEYE + * restructured HAWKEYE for improved usability + * replaced `RegisterCandidate`, `RoundCandidate` and the free S-box functions of HAWKEYE with a single `CipherCandidate` that analyzes a candidate in place instead of copying it into a netlist of its own + * added `CipherCandidate::identify_sboxes` that identifies every S-box of a candidate at once and annotates it with the outcome + * added `CipherCandidate::create_modules` that writes a candidate back into the netlist as a module hierarchy of the candidate, its state register, and one submodule per identified S-box + * added the HAWKEYE S-box database to the build directory so that it is found at runtime + * improved speed of HAWKEYE candidate search and identification + * fixed the round function of HAWKEYE walking every path from each flip-flop of the register rather than every gate, which is exponential in a cone of logic that reconverges + * sped up the S-box identification of HAWKEYE by tabulating each output over the state and the control inputs together and reading the assignment of the control inputs out of that one table, instead of substituting the control values and tabulating anew for each of up to 256 assignments + * changed the round function of a HAWKEYE candidate to determine which flip-flops each of its gates depends on only when the S-box search asks for it + * added a limit to the canonical form search behind an S-box lookup, which finishes quickly for a real S-box but does not terminate in reasonable time for a table that is close to linear + * bugfixes + * fixed the candidates of HAWKEYE being ordered by the addresses of their gates, which made the result of `detect_candidates` differ between runs of the same binary + * fixed `RegisterCandidate::operator==` never reporting a round-based candidate as equal to itself + * fixed the S-box search of HAWKEYE calling `std::includes` on the unsorted result of `get_unique_predecessors`, which made the number of located S-boxes differ between runs of the same binary + * fixed the linear independence check of HAWKEYE shifting by more than the width of its type for S-boxes of more than 6 bits, which is undefined and made the check operate on garbage for 7-bit and 8-bit S-boxes + * fixed `SBoxDatabase::lookup` never terminating for an 8-bit S-box that is not contained in the database, as it counted the constant it adds to the outputs in a `u8`, which never reaches 256 + * fixed `SBoxDatabase::store` reporting a failure although it had written the database, and made it report one if the file cannot be opened + * fixed `hawkeye.DetectionConfiguration` and `hawkeye.SBoxDatabase` leaking every instance created from Python, `SBoxDatabase.from_file` leaked 25 KB per call + * simulation + * added selecting the nets of the waveforms selected in the waveform viewer in the graph view as well + * simulation wizard + * added `Load data from file` to the manual input page, which fills the input table from a SALEAE directory, a VCD or a CSV file so that the data can be edited before simulating + * added a `Display values as hex numbers` switch to the input table, and changed the table to accept values in the `0x`, `0o`, `0b` and the Verilog `'h`, `'o`, `'b`, `'d` notations as well as `"` for the code of a character, each checked against the width of the pin group + * changed the input columns of the simulation wizard to list the nets of a descending pin group from the highest index down, so that the bits of an entered value land on the nets in the order the group declares + * changed the window title from `Empty Wizard` to `Simulation Wizard` and greyed out the input method that is not selected + * fixed editing a time in the middle of the input table replacing it with the previous time plus 1000 as if it were the last row; it is now kept if it lies between its neighbours and replaced by their midpoint otherwise + * fixed the wizard starting with the wave data and net groups of the previous run, its SALEAE directory and the controller are now reset when the wizard opens + * bugfixes + * fixed the debug dump of the SALEAE directory that was printed to the console whenever waveforms were loaded + * fixed the waveform viewer keeping the tab of a simulation controller that was deleted, e.g., from Python, and dereferencing the deleted controller from it + * fixed the value of a waveform group being computed with the first net as LSB in `WaveDataGroup::recalcData`, `WaveDataProviderGroup` and `WaveGroupValue` while the rest of the viewer takes the first net as MSB, so the same group showed different values depending on where it was evaluated + * fixed the waveform tree losing the expanded or collapsed state of its groups whenever its items are reordered, e.g., after a drag and drop + * fixed dropping a waveform onto a group in the waveform tree always inserting it as the first entry of the group regardless of the drop position * dataflow analysis - * fixed running the dataflow analysis from the command line with `--dataflow`, which failed with "no gate types specified" as the CLI path configured neither the gate types nor the control pin types that the plugin dialog sets, and wrote no result. It now analyzes flip-flops with clock, enable, reset and set as control pins and writes `graph.dot` and `groups.txt` to the directory given by `--path`. `write_dot` and `write_txt` also no longer report "replacing invalid file extension" for a path whose extension is already correct + * fixed running the dataflow analysis from the command line with `--dataflow`, which failed with "no gate types specified" * fixed the dataflow analysis blocking a thread when it wrote its results, progress was reported while the lock that guards the results was held - * changed `create_modules` to create the pin groups of a DANA module in descending order, listing the highest index first as the GUI does by default, instead of renaming the single-pin group that the module already carried. The pin indices are unchanged - * added `Result::open_dot_in_viewer`, which the plugin dialog calls after writing the `.dot` file so that the dataflow graph opens in the DOT viewer if that plugin is loaded. `Result::write_dot` now returns the path it wrote to, as it replaces a wrong extension - * module identification - * changed the pin groups of an identified module to be descending, listing the highest index first as the GUI does by default. The pin indices are unchanged, and the `CTRL` group is now of type `control` instead of `enable` + * changed `create_modules` to create the pin groups of a DANA module in descending order + * `Result::write_dot` now returns the path it wrote to, as it replaces a wrong extension + * changed the plugin dialog to no longer open the written `.dot` graph in the DOT viewer unprompted + * fixed `dataflow.Configuration` leaking every instance created from Python + * graph algorithm + * added `NetlistGraph::from_gates` that builds a graph from a subset of the gates of a netlist, optionally representing a gate by a primary and a shadow vertex so that feedback through it does not close a cycle + * fixed `NetlistGraph` destroying an igraph graph that was never created when `from_netlist` or `copy` fail before creating it + * made `NetlistGraph` non-copyable, as the implicitly generated copy shared the igraph internals between two objects that both freed them + * fixed `NetlistGraph` never being freed by Python, as its factories hand over ownership but it was bound with a non-owning holder; every graph built from a netlist leaked + * added Python bindings for `NetlistGraph::from_gates`, `is_shadow_vertex` and `get_all_vertices_from_gate` * FSM solver - * reworked the API of `solve_fsm` into a single function that is set up through a `Configuration` object, which also selects between the SMT and the brute force approach + * changed the API of `solve_fsm` into a single function that is set up through a `Configuration` object, which also selects between the SMT and the brute force approach * changed `solve_fsm` to report the value of each configured output of the FSM in each state, annotating the states of the DOT graph with it * changed `solve_fsm` to no longer write a file on its own, the DOT graph is now rendered by calling `generate_dot_graph` on the returned state transition graph - * changed `solve_fsm` to no longer open the graph in the dot viewer behind the user's back, use `dot_viewer.load_dot_file` to display it + * changed `solve_fsm` to no longer open the graph in the dot viewer unprompted, use `dot_viewer.load_dot_file` to display it * added `to_string` and `write_txt` to the state transition graph, printing the full conditions of all transitions together with a legend that maps the state bits, the outputs, and every net variable of a Boolean function back to the netlist - * split the `solve_fsm` API into one header per struct, mirroring the layout of the dataflow analysis plugin + * added tests * removed the debug output that `solve_fsm` printed to stdout on every run - * fixed `solve_fsm` interpreting a user-provided initial state with the wrong bit order, which made the exploration start from a different state than the one requested - * added tests for the `solve_fsm` plugin, which had none so far - * Liberty parser - * fixed the Liberty parser rejecting a `type` group that declares `bit_to`, the value was left in the token stream after the attribute was read and parsing then failed on what it took to be the end of the statement. The value is implied by `bit_from`, `bit_width` and `downto` and is still ignored + * fixed `solve_fsm` interpreting a user-provided initial state with the wrong bit order * netlist preprocessing - * fixed `remove_redundant_gates` treating two flip-flops as duplicates although they start out at different values, as the fingerprint it groups them by covers the gate type and the fan-in but not the initial value, and flip-flops are merged on that fingerprint alone without the equivalence check that combinational gates get. This affects 11 of the 13 flip-flop types of the Xilinx UNISIM library, all of which carry an `INIT` value - * added an optional gate scope to the preprocessing functions of `netlist_preprocessing` and `xilinx_toolbox`, restricting which gates may be modified or deleted and defaulting to the entire netlist + * added an optional gate scope to the preprocessing functions, restricting which gates may be modified or deleted and defaulting to the entire netlist * changed `split_shift_registers` and `unify_ff_outputs` to assign newly created gates to the module of the gate they replace instead of always to the top module * fixed `simplify_lut_inits` crashing on a LUT whose output pin is unconnected * fixed `remove_unconnected_gates` looping forever if a gate could not be deleted + * fixed `remove_redundant_gates` treating two flip-flops as duplicates although they have different initialization values + * fixed `create_multi_bit_gate_modules` and `create_nets_at_unconnected_pins` handing out modules and nets to Python without keeping the netlist alive * Xilinx toolbox - * fixed `split_luts` crashing on a `LUT6_2` that only uses one of its two output pins, which is the common case the function is meant to handle + * added an optional gate scope to the preprocessing functions, restricting which gates may be modified or deleted and defaulting to the entire netlist + * changed the members of `xilinx_toolbox::LOC` to be default-initialised, `loc_type` to `PIN` and both coordinates to `0` + * added tests + * fixed `split_luts` crashing on a Xilinx `LUT6_2` that only uses one of its two output pins * fixed the documentation of `split_shift_registers`, which claimed that only `SRL16E` is supported although `SRLC32E` is handled as well - * added tests for the `xilinx_toolbox` plugin, which had none so far - * changed the members of `xilinx_toolbox::LOC` to be default-initialised, `loc_type` to `PIN` and both coordinates to `0`, so that the LOC of a package pin, for which the XDC parser sets no coordinates, no longer carries uninitialised values * bit-order propagation - * changed the interface to speak in a `BitOrder`, which is the order of one module pin group, and a `BitOrderResult`, which is what a propagation reports, in place of a map from pairs of module and pin group to a map from net to index. A bit order is now an object rather than a container, so Python can be given one without losing track of the netlist it belongs to, and a result iterates by module and pin group ID rather than by the addresses they happen to sit at - * added tests for the plugin, which had none + * changed the interface to speak in a `BitOrder`, which is the order of one module pin group, and a `BitOrderResult`, which is what a propagation reports, instead of maps + * added tests * fixed bug in the bitorder propagation algorithm that would assign a wrong propagation order if pingroups with direction none were given as parameters - * simulation - * added selecting the nets of the waveforms selected in the waveform viewer in the graph view as well, and removed the debug dump of the SALEAE directory that was printed to the console whenever waveforms were loaded - * fixed the waveform viewer keeping the tab of a simulation controller that was deleted, e.g. from Python, and dereferencing the deleted controller from it - * fixed the value of a waveform group being computed with the first net as LSB in `WaveDataGroup::recalcData`, `WaveDataProviderGroup` and `WaveGroupValue` while the rest of the viewer takes the first net as MSB since 4.5.0, so the same group showed different values depending on where it was evaluated - * changed the input columns of the simulation wizard to list the nets of a descending pin group from the highest index down, so that the bits of an entered value land on the nets in the order the group declares - * fixed the waveform tree losing the expanded or collapsed state of its groups whenever its items are reordered, e.g. after a drag and drop - * fixed dropping a waveform onto a group in the waveform tree always inserting it as the first entry of the group regardless of the drop position - * simulation wizard - * added `Load data from file` to the manual input page, which fills the input table from a SALEAE directory, a VCD or a CSV file so that the data can be edited before simulating - * added a `Display values as hex numbers` switch to the input table, and changed the table to accept values in the `0x`, `0o`, `0b` and the Verilog `'h`, `'o`, `'b`, `'d` notations as well as `"` for the code of a character, each checked against the width of the pin group; before, a multi-bit column only took a bare hexadecimal number - * fixed editing a time in the middle of the input table replacing it with the previous time plus 1000 as if it were the last row; it is now kept if it lies between its neighbours and replaced by their midpoint otherwise - * fixed the wizard starting with the wave data and net groups of the previous run, its SALEAE directory and the controller are now reset when the wizard opens - * changed the window title from `Empty Wizard` to `Simulation Wizard` and greyed out the input method that is not selected - * fixed the documentation of `NetlistSimulatorController::initialize`, which described the behaviour of the legacy `NetlistSimulator`: it claimed that no gates or clocks may be added afterwards and that `simulate` calls it automatically, neither of which holds since its body became empty - * dot viewer - * added the `dot_viewer` plugin, which renders a Graphviz `.dot` file inside the GUI and, for a file written by another HAL plugin, ties the graph to the netlist. Its Python module is `dot_viewer` and its one function `load_dot_file(path, creator_plugin="")` returns whether the file was displayed. The viewer is opened from the plugin dialog or from `load_dot_file`, zooms with the mouse wheel and the zoom shortcuts of the graph view, pans by dragging with Shift held or with the middle mouse button if the graph view's middle-button panning is enabled, highlights the node under the mouse together with its edges, offers a grid toggle and a toolbar menu that chooses per node and edge whether the colors come from the DOT file or from the dark or light style of HAL, and honours the `\n` line breaks of a node label. The plugin that wrote the file is taken from the `creator_plugin` argument, else from a `created by HAL plugin` comment in the file, else asked for in a dialog. For a dataflow analysis graph, selecting a node selects the module in HAL and the other way round, a node follows its module when the module is renamed, and the context menu of an edge isolates the shortest path between its two modules in a new view; for an FSM solver graph, selecting a transition puts every net into a `0 state`, `1 state` or `x state` grouping according to the value the transition condition requires of it, the context menu of a transition lists those nets, and selecting a net in HAL highlights it in every transition it appears in - * renamed `DotViewerCallFromTread` to `DotViewerCallFromThread` + * Boolean influence + * fixed `get_ff_dependency_matrix` dereferencing an uninitialized pointer on every call + * fixed the four functions that return influences per net handing out the nets to Python without keeping the netlist alive, as nothing protected a borrowed object sitting in a dict key + * module identification + * changed the pin groups of an identified module to be descending. The pin indices are unchanged, and the `CTRL` group is now of type `control` instead of `enable` + * fixed `CandidateType.addition_offset` being bound to `addition` in Python, which made the two indistinguishable + * Liberty parser + * fixed the Liberty parser rejecting a `type` group that declares `bit_to` + * GUI extension demo + * fixed `ParameterType.Module` being bound to `Gate` in Python, which made the two indistinguishable + * GUI - * fixed the GUI hanging for minutes when a module with many gates is selected, `ModuleModel` emitted a row insert signal per item while the model was already being reset, which made the attached filter proxy remap its rows once per item - * fixed the GUI stalling when a large module is unfolded, the tree views measured every row individually and shaped the text of each gate name just to learn how tall the row is - * changed the module elements tree to not rebuild itself twice per selection change - * fixed the Python editor and the comment editor hanging when the search string is empty or the regular expression matches an empty string, `find` returned the same zero-length match forever; an empty search string now clears the highlighting - * added restoring the layout of the previous session on start: for every widget, plugin widgets included, its dock area, position, visibility, size and, if it was detached, the position of its window are written to the user settings file together with the splitter sizes when a netlist is closed and applied when the next one is opened - * added `Set focus to pin` to the context menu of a pin in the pin tree of a gate and of a module, which moves the sub-focus of the graph view to that pin - * changed the plugin manager of the GUI to unload a plugin, and the dependencies it pulled in, right after loading it to read its description, so that only plugins requested by the user or required by such a plugin stay loaded - * fixed the GUI dropping an unrelated gate from the selection instead of the net itself when a selected net is deleted - * fixed the GUI re-laying out its graph views once per gate while a preprocessing function invoked from a context menu deletes or replaces many of them - * added context menu entries to the GUI for `remove_buffers`, `unify_ff_outputs`, `split_luts`, and `split_shift_registers`, each applicable to the current selection or to the entire netlist - * fixed the GUI crashing when the selection is to be brought into view before the graph view has rendered its layout, e.g. by `GuiApi::selectGate` with `navigate_to_selection` set right after opening a netlist, the scene had no item for the selected element yet - * fixed unloading the waveform viewer or the DOT viewer plugin leaving its dock button behind, the widget was deleted without being removed from its anchor - * changed duplicating a view to place the modules and gates of the copy where they are in the original instead of laying the copy out anew - * fixed a module that reuses the ID of a deleted module being shown with the colour icon of the deleted one in the selection details, the icon cache was never told about removed modules - * fixed the GUI at times not registering the first change to a netlist after start, and so not offering to save it on close, the flag that guards the notification was never initialized - * module pin groups - * fixed the GUI crashing when a pin group that still holds pins is deleted: the pins tree removed the group item while the pin items still hung below it, and the events that followed for those pins looked them up through the detached group - * fixed the GUI crashing when `Delete pin group` is chosen for a group that holds a single pin named like the group: there is nothing to do for such a group and the action built for it was a null pointer that was then dereferenced; the entry now does nothing for such a group + * module and gate pins * changed the pin tree of a module to show the number of pins of each group and its order, ↑ for ascending and ↓ for descending, in a `Size/Index` column right after the name, which also holds the index of each pin; the pin tree of a gate shows the number of pins and the order of each group in its `Index` column - * fixed toggling a pin group between ascending and descending losing the direction and the type of the group, and deleting a pin group creating the single-pin groups for its pins without direction and type * added `Automatically rename pins` to the context menu of a pin group, which renames every pin of the group to `()` + * added `Set focus to pin` to the context menu of a pin in the pin tree of a gate and of a module, which moves the sub-focus of the graph view to that pin + * misc + * added restoring the GUI layout of the previous session on start + * changed the plugin manager of the GUI to unload a plugin, and the dependencies it pulled in, right after loading it to read its description, so that only plugins requested by the user or required by such a plugin stay loaded + * added context menu entries to the GUI for `remove_buffers`, `unify_ff_outputs`, `split_luts`, and `split_shift_registers`, each applicable to the current selection or to the entire netlist + * changed duplicating a view to place the modules and gates of the copy where they are in the original instead of laying the copy out anew + * bugfixes + * fixed the GUI crashing when a pin group that still holds pins is deleted + * fixed the GUI crashing when `Delete pin group` is chosen for a group that holds a single pin named like the group + * fixed toggling a pin group between ascending and descending losing the direction and the type of the group, and deleting a pin group creating the single-pin groups for its pins without direction and type + * fixed a module that reuses the ID of a deleted module being shown with the colour icon of the deleted one in the selection details + * fixed the GUI at times not registering the first change to a netlist after start, and so not offering to save it on close + * fixed unloading the waveform viewer or the DOT viewer plugin leaving its dock button behind + * fixed the GUI crashing when the selection is to be brought into view before the graph view has rendered its layout, e.g. by `GuiApi::selectGate` with `navigate_to_selection` set right after opening a netlist, the scene had no item for the selected element yet + * fixed the GUI dropping an unrelated gate from the selection instead of the net itself when a selected net is deleted + * fixed the GUI re-laying out its graph views once per gate while a preprocessing function invoked from a context menu deletes or replaces many of them + * fixed the GUI hanging for minutes when a module with many gates is selected + * fixed the GUI stalling when a large module is unfolded within a list of modules + * fixed the Python editor and the comment editor hanging when the search string is empty or the regular expression matches an empty string + * fixed the module tree to not rebuild itself twice per selection change + * fixed `GuiApi.selectGate`, `selectNet` and `selectModule` with `clear_current_selection` set, which is the default, keeping the previous selection + * fixed `GuiApi.getSelectedModules` and `getSelectedItems` not tying the returned modules to the netlist * Build and dependencies - * changed the GUI from Qt 5 to Qt 6, which is now required to build the GUI + * changed the GUI from Qt 5 to Qt 6 * updated the vendored QuaZip from 1.3 to 1.5 as part of the move to Qt 6 - * added the Graphviz development libraries to the build dependencies, `libgraphviz-dev` on Ubuntu, which the DOT viewer plugin links against and which is built by default; pass `-DPL_DOT_VIEWER=OFF` to build without them + * added the Graphviz development libraries used by the DOT viewer to the build dependencies * added support for Ubuntu 26.04, which is now built and tested in CI next to 22.04, 24.04 and macOS - * fixed `install_dependencies.sh` on macOS writing the Homebrew prefix into the shell configuration as the unexpanded text `$BREW_PREFIX`, so the `PATH` entries it added for Qt, LLVM, flex and bison pointed nowhere; a line that was added this way is left in place and can be deleted - * fixed the documentation build: Sphinx is now run through the Python interpreter that `hal_py` is linked against, as an unrelated `sphinx-build` crashed on importing it, and `hal_py` is imported before autodoc touches a plugin module, without which the `boolean_influence`, `dataflow` and `module_identification` pages were empty. Doxygen warnings went from 295 to 2 and Sphinx warnings from 15 to 0, and every namespace, class and struct now carries a description - * added a test that checks the Python bindings never hand out a borrowed pointer without keeping its owner alive, and never give a class bound with a non-owning holder to a factory that returns a `unique_ptr`. It covers plugins kept in a repository of their own as well, and holds free, static and submodule-level functions to the same rule as methods, which `hal::borrowed()` made fixable - * updated the vendored igraph dependency from 0.10.12 to 1.0.1 and ported the graph algorithm and HAWKEYE plugins to the igraph 1.0 API; building against a system igraph (`USE_VENDORED_IGRAPH=OFF`) now requires igraph 1.0, and the vendored igraph is no longer built with warnings as errors, which is what broke the macOS build once Apple clang 18 added `-Wuninitialized-const-pointer` - * removed the tests below `tests/python_binding`, which were neither referenced by the build nor by any workflow and called API that no longer exists + * fixed `install_dependencies.sh` on macOS writing the Homebrew prefix into the shell configuration as the unexpanded text + * fixed the documentation build: Sphinx is now run through the Python interpreter that `hal_py` is linked against + * updated the vendored igraph dependency from 0.10.12 to 1.0.1 and ported the graph algorithm and HAWKEYE plugins to the igraph 1.0 API ## [4.5.0](v4.5.0) - 2025-09-23 12:00:00+02:00 (urgency: medium) * plugins