From 2f276d072309fb16675565beb6546135881d7650 Mon Sep 17 00:00:00 2001 From: tondapusili Date: Wed, 25 Feb 2026 12:15:46 -0800 Subject: [PATCH 01/44] Added log flushes after each negopt pass for clearer logging --- passes/silimate/negopt.cc | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/passes/silimate/negopt.cc b/passes/silimate/negopt.cc index 334348987..e3066b9b7 100644 --- a/passes/silimate/negopt.cc +++ b/passes/silimate/negopt.cc @@ -81,33 +81,47 @@ struct NegoptPass : public Pass { run_post = true; } + constexpr int max_iterations = 100; + for (auto module : design->selected_modules()) { if (run_pre) { did_something = true; - while (did_something) { + for (int iter = 0; iter < max_iterations && did_something; iter++) { did_something = false; peepopt_pm pm(module); pm.setup(module->selected_cells()); pm.run_manual2sub(); // Reduce manual 2's complement to subtraction first + log_flush(); pm.run_sub2neg(); + log_flush(); pm.run_negexpand(); + log_flush(); pm.run_negneg(); + log_flush(); pm.run_negmux(); + log_flush(); } + if (did_something) + log_warning("NEGOPT pre reached max iterations (%d) in module %s without convergence.\n", max_iterations, log_id(module)); } if (run_post) { did_something = true; - while (did_something) { + for (int iter = 0; iter < max_iterations && did_something; iter++) { did_something = false; peepopt_pm pm(module); pm.setup(module->selected_cells()); pm.run_negrebuild(); + log_flush(); pm.run_muxneg(); + log_flush(); pm.run_neg2sub(); + log_flush(); } + if (did_something) + log_warning("NEGOPT post reached max iterations (%d) in module %s without convergence.\n", max_iterations, log_id(module)); } } } From f46b8d2a4468e0aa6c11cee7e9b60511a089e937 Mon Sep 17 00:00:00 2001 From: tondapusili Date: Fri, 27 Feb 2026 09:13:39 -0800 Subject: [PATCH 02/44] silimate: add opt_timing_balance pass and tests --- .github/actions/setup-build-env/action.yml | 4 +- .github/workflows/test-build.yml | 2 +- passes/silimate/Makefile.inc | 1 + passes/silimate/opt_timing_balance.cc | 1466 ++++++++++++++++++++ tests/silimate/opt_timing_balance.ys | 511 +++++++ 5 files changed, 1981 insertions(+), 3 deletions(-) create mode 100644 passes/silimate/opt_timing_balance.cc create mode 100644 tests/silimate/opt_timing_balance.ys diff --git a/.github/actions/setup-build-env/action.yml b/.github/actions/setup-build-env/action.yml index ff2574d31..c6ea64e2e 100644 --- a/.github/actions/setup-build-env/action.yml +++ b/.github/actions/setup-build-env/action.yml @@ -35,14 +35,14 @@ runs: if: runner.os == 'Linux' uses: awalsh128/cache-apt-pkgs-action@v1.6.0 with: - packages: gawk git make python3 bison clang flex libffi-dev libfl-dev libreadline-dev pkg-config tcl-dev zlib1g-dev libnsl-dev libdwarf-dev libelf-dev elfutils libdw-dev ccache + packages: gawk git make python3 bison clang flex libffi-dev libfl-dev libreadline-dev pkg-config tcl-dev zlib1g-dev libnsl-dev libdwarf-dev libelf-dev elfutils libdw-dev ccache version: ${{ inputs.runs-on }}-commonys - name: Linux build dependencies if: runner.os == 'Linux' && inputs.get-build-deps == 'true' uses: awalsh128/cache-apt-pkgs-action@v1.6.0 with: - packages: bison clang flex libffi-dev libfl-dev libreadline-dev pkg-config tcl-dev zlib1g-dev libgtest-dev + packages: gawk git make python3 bison clang flex libffi-dev libfl-dev libreadline-dev pkg-config tcl-dev zlib1g-dev libnsl-dev libdwarf-dev libelf-dev elfutils libdw-dev ccache libgtest-dev version: ${{ inputs.runs-on }}-buildys - name: Linux docs dependencies diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 372e3424c..331c062d5 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -116,7 +116,7 @@ jobs: uses: ./.github/actions/setup-build-env with: runs-on: ${{ matrix.os }} - get-test-deps: true + get-build-deps: true get-iverilog: true - name: Download build artifact diff --git a/passes/silimate/Makefile.inc b/passes/silimate/Makefile.inc index 60e03c5a8..4f480ba05 100644 --- a/passes/silimate/Makefile.inc +++ b/passes/silimate/Makefile.inc @@ -13,6 +13,7 @@ OBJS += passes/silimate/reg_rename.o OBJS += passes/silimate/splitfanout.o OBJS += passes/silimate/splitlarge.o OBJS += passes/silimate/splitnetlist.o +OBJS += passes/silimate/opt_timing_balance.o OBJS += passes/silimate/opt_expand.o GENFILES += passes/silimate/peepopt_expand.h diff --git a/passes/silimate/opt_timing_balance.cc b/passes/silimate/opt_timing_balance.cc new file mode 100644 index 000000000..f10283015 --- /dev/null +++ b/passes/silimate/opt_timing_balance.cc @@ -0,0 +1,1466 @@ +/* + * yosys -- Yosys Open SYnthesis Suite + * + * Copyright (C) 2012 Claire Xenia Wolf + * 2026 Abhinav Tondapu + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + */ + +#include "kernel/yosys.h" +#include "kernel/sigtools.h" +#include "kernel/celltypes.h" +#include "kernel/utils.h" +#include +#include +#include +#include +#include +#include +#include +#include + +USING_YOSYS_NAMESPACE +PRIVATE_NAMESPACE_BEGIN + +/* Invariants: + * - Operates on internal word cells ($add/$and/$or/$xor) pre-techmap + * - Connectivity and timing keys use sigmap-mapped signals + * - Rewiring uses original head Y bits to avoid alias drift + * - Disjoint clusters are rewritten per sweep, clean/rebuild happens per iteration +*/ + +// ----------------------------------------------------------------------------- +// Shared constants, helpers, and traits +// ----------------------------------------------------------------------------- + +static constexpr double kDelayDefault = 1.0; +static constexpr double kDelayLogic = 0.5; +static constexpr double kMinIterationDelta = 1e-3; +static constexpr int kMaxPassIterations = 10; +static constexpr int kTraversalStackReserve = 256; + +static const IdString kAttrTimingBalanceGenerated = "\\timing_balance_generated"; + +static IdString make_id(Cell *anchor, const char *suffix) +{ + // NEW_ID2_SUFFIX relies on a local variable named `cell` + Cell *cell = anchor; + return NEW_ID2_SUFFIX(suffix); +} + +static inline double log2p1_int(int n) { return std::log2(static_cast(n) + 1.0); } + +static int cell_y_width(const Cell *cell) +{ + log_assert(cell != nullptr); + if (cell->hasParam(ID::Y_WIDTH)) + return std::max(1, cell->getParam(ID::Y_WIDTH).as_int()); + if (cell->hasPort(ID::Y)) + return std::max(1, GetSize(cell->getPort(ID::Y))); + + // TimingOracle can query non-target drivers, fall back to widest output port + int width = 0; + for (const auto &[port_id, sig] : cell->connections()) + if (cell->output(port_id)) + width = std::max(width, GetSize(sig)); + return std::max(1, width); +} + +enum class BalanceCategory { + Logic, + Arith +}; + +enum class WidthRule { + MaxInput, + AddCarry +}; + +enum class DelayHeuristicKind { + Fixed, + AddLike +}; + +enum class TraversalState : int { + Unseen = 0, + Active = 1, + Done = 2 +}; + +// Per-cell balancing traits and delay heuristic policy +struct SupportedCellSpec +{ + IdString type; + BalanceCategory category; + bool requires_strict_width_match = false; + bool requires_matching_signedness = false; + WidthRule width_rule = WidthRule::MaxInput; + DelayHeuristicKind delay_kind = DelayHeuristicKind::Fixed; + double fixed_delay = 0.0; +}; + +// Registry for balance targets and their delay/width behavior +// Adding a new associative target should only require editing this table +static const std::vector &supported_cell_registry() +{ + static const std::vector specs = { + {ID($and), BalanceCategory::Logic, false, false, WidthRule::MaxInput, DelayHeuristicKind::Fixed, kDelayLogic}, + {ID($or), BalanceCategory::Logic, false, false, WidthRule::MaxInput, DelayHeuristicKind::Fixed, kDelayLogic}, + {ID($xor), BalanceCategory::Logic, false, false, WidthRule::MaxInput, DelayHeuristicKind::Fixed, kDelayDefault}, + {ID($add), BalanceCategory::Arith, true, true, WidthRule::AddCarry, DelayHeuristicKind::AddLike, 0.0}, + }; + return specs; +} + +static const dict &supported_cell_registry_map() +{ + static const dict by_type = []() { + dict m; + for (const auto &spec : supported_cell_registry()) + m[spec.type] = &spec; + return m; + }(); + return by_type; +} + +static const SupportedCellSpec *get_supported_cell_spec(IdString type) +{ + const auto &by_type = supported_cell_registry_map(); + auto it = by_type.find(type); + if (it == by_type.end()) + return nullptr; + return it->second; +} + +static std::vector collect_target_cell_ids(bool enable_logic, bool enable_arith) +{ + std::vector ids; + for (const auto &spec : supported_cell_registry()) + { + bool enabled_category = (spec.category == BalanceCategory::Logic) ? enable_logic : enable_arith; + if (!enabled_category) + continue; + ids.push_back(spec.type); + } + return ids; +} + +static bool less_sigbit_key(const SigBit &a, const SigBit &b) +{ + bool a_const = a.wire == nullptr; + bool b_const = b.wire == nullptr; + if (a_const != b_const) + return a_const; + + if (a_const) { + int ad = static_cast(a.data); + int bd = static_cast(b.data); + return ad < bd; + } + + if (a.wire->name != b.wire->name) + return std::strcmp(a.wire->name.c_str(), b.wire->name.c_str()) < 0; + return a.offset < b.offset; +} + +static bool less_sigspec_key(const SigSpec &a, const SigSpec &b) +{ + if (GetSize(a) != GetSize(b)) + return GetSize(a) < GetSize(b); + + int n = GetSize(a); + for (int i = 0; i < n; i++) { + const SigBit &ab = a[i]; + const SigBit &bb = b[i]; + if (ab == bb) + continue; + return less_sigbit_key(ab, bb); + } + return false; +} + +// For supported ops here, result signedness is true only when both inputs are signed +static constexpr bool yosys_binary_result_signed(bool a_signed, bool b_signed) { return a_signed && b_signed; } + +static const dict &fixed_delay_table() +{ + static const auto table = dict{ + {ID($not), 0.0}, + {ID($pos), 0.0}, + {ID($logic_not), 0.0}, + {ID($and), kDelayLogic}, + {ID($or), kDelayLogic}, + {ID($xor), kDelayDefault}, + {ID($xnor), kDelayDefault}, + {ID($logic_and), kDelayLogic}, + {ID($logic_or), kDelayLogic}, + {ID($mux), kDelayDefault}, + }; + return table; +} + +static bool is_timing_boundary_cell(Cell *cell, const CellTypes &cell_types) +{ + if (cell == nullptr) + return true; + + // Explicit user attributes + if (cell->get_bool_attribute(ID::keep) || cell->get_bool_attribute(ID::blackbox)) + return true; + + // Flip-flops + if (cell->is_builtin_ff()) + return true; + + // Latches, memories, and formal/simulation cells + if (cell->type.in( + ID($dlatch), ID($adlatch), ID($dlatchsr), + ID($mem), ID($mem_v2), ID($memrd), ID($memrd_v2), ID($memwr), ID($memwr_v2), ID($meminit), ID($meminit_v2), + ID($anyconst), ID($anyseq), ID($allconst), ID($allseq), ID($equiv), + ID($assert), ID($assume), ID($cover), ID($check), ID($print) + )) + return true; + + // Macro or unknown cell + return !cell_types.cell_known(cell->type); +} + +static double estimate_cell_delay(const Cell *cell, int out_width) +{ + if (cell == nullptr) + return kDelayDefault; + + IdString type = cell->type; + int width = out_width; + + const auto &by_type = supported_cell_registry_map(); + auto reg_it = by_type.find(type); + if (reg_it != by_type.end()) { + const SupportedCellSpec *spec = reg_it->second; + switch (spec->delay_kind) + { + case DelayHeuristicKind::Fixed: + return spec->fixed_delay; + case DelayHeuristicKind::AddLike: + return log2p1_int(width); + } + } + + if (type == ID($pmux)) { + int s_width = 1; + if (cell->hasParam(ID::S_WIDTH)) + s_width = cell->getParam(ID::S_WIDTH).as_int(); + return log2p1_int(s_width); + } + if (type.in(ID($add), ID($sub), ID($neg), ID($alu))) + return log2p1_int(width); + if (type.in(ID($mul), ID($div), ID($mod))) + return width; + if (type.in(ID($shl), ID($shr), ID($sshl), ID($sshr))) + return log2p1_int(width); + + const auto &fixed = fixed_delay_table(); + auto it = fixed.find(type); + if (it != fixed.end()) + return it->second; + return kDelayDefault; +} + +// ----------------------------------------------------------------------------- +// Analysis: connectivity and timing oracle +// ----------------------------------------------------------------------------- + +struct ConnectivitySnapshot +{ + // One-sweep structural connectivity view + dict unique_driver_by_bit; + SigSet sinks_by_bit; + pool output_port_bits; + + ConnectivitySnapshot() = default; + ConnectivitySnapshot(Module *module, SigMap &sigmap) { build(module, sigmap); } + + void build(Module *module, SigMap &sigmap) + { + unique_driver_by_bit.clear(); + sinks_by_bit.clear(); + output_port_bits.clear(); + + // Full-module view keeps fanout checks selection-safe + for (Cell *cell : module->cells()) { + for (const auto &[port_id, sig] : cell->connections()) { + SigSpec mapped = sigmap(sig); + if (cell->output(port_id)) { + for (auto bit : mapped) { + if (!bit.wire) + continue; + auto [it, inserted] = unique_driver_by_bit.emplace(bit, cell); + if (!inserted && it->second != cell) + it->second = nullptr; + } + } + if (cell->input(port_id)) + sinks_by_bit.insert(mapped, cell); + } + } + // Output ports mark head boundaries. Input boundaries are handled in TimingOracle + for (auto wire : module->wires()) { + if (wire->port_output) { + for (auto bit : sigmap(wire)) + output_port_bits.insert(bit); + } + } + } + + Cell *get_unique_driver_mapped(const SigSpec &sig) const + { + // Caller passes sigmap-mapped signal slices + Cell *driver = nullptr; + for (auto bit : sig) + { + if (!bit.wire) + return nullptr; + auto it = unique_driver_by_bit.find(bit); + if (it == unique_driver_by_bit.end() || it->second == nullptr) + return nullptr; + if (driver == nullptr) + driver = it->second; + else if (driver != it->second) + return nullptr; + } + return driver; + } + + void collect_sinks_mapped(const SigSpec &mapped_sig, pool &sinks) + { + // SigSet::find() is non-const in current Yosys API + sinks.clear(); + sinks_by_bit.find(mapped_sig, sinks); + } + +}; + +struct TimingOracle +{ + // Lazy backward arrival estimator over the current connectivity snapshot + // Unknown or boundary drivers return 0.0, combinational cycles return +inf + const CellTypes &cell_types; + SigMap &sigmap; + const dict *driver_map; + dict arrival_cache; + dict visit_state; + struct StackEntry { + SigBit bit; + // false: expand dependencies, true: finalize after children + bool finalize_phase = false; + }; + bool cycle_detected = false; + + TimingOracle(const CellTypes &cell_types, SigMap &sigmap, + const dict &driver_map) : + cell_types(cell_types), sigmap(sigmap), driver_map(&driver_map) { } + + void clear_timing_cache() + { + arrival_cache.clear(); + visit_state.clear(); + cycle_detected = false; + } + + void rebind_driver_map(const dict &new_driver_map) + { + driver_map = &new_driver_map; + clear_timing_cache(); + } + + void cache_final_value(SigBit bit, double arrival) + { + if (!bit.wire) + return; + bit = sigmap(bit); + arrival_cache[bit] = arrival; + visit_state[bit] = TraversalState::Done; + } + + TraversalState get_visit_state(SigBit bit) const + { + if (auto it = visit_state.find(bit); it != visit_state.end()) + return it->second; + return TraversalState::Unseen; + } + + void set_visit_state(SigBit bit, TraversalState state) + { + visit_state[bit] = state; + } + + double get_arrival(const SigSpec &sig) + { + cycle_detected = false; + double t = 0.0; + for (auto bit : sigmap(sig)) + t = std::max(t, get_arrival_noguard(bit)); + return t; + } + +private: + /* + * Two-phase DFS avoids recursion, + * finalize_phase = false expands inputs, true computes and caches node arrival + * Active marks the current path, unresolved inputs during finalize are treated as cycles with +inf + */ + double get_arrival_noguard(SigBit bit) + { + SigBit start = sigmap(bit); + if (!start.wire) + return 0.0; + if (auto it = arrival_cache.find(start); it != arrival_cache.end()) + return it->second; + + // Local stack keeps traversal state scoped to one query + std::vector eval_stack; + eval_stack.reserve(kTraversalStackReserve); + eval_stack.push_back({start, false}); + + while (!eval_stack.empty()) + { + StackEntry e = std::move(eval_stack.back()); + eval_stack.pop_back(); + SigBit curr = e.bit; + if (!curr.wire) + continue; + if (arrival_cache.count(curr)) + continue; + + if (curr.wire->port_input) { + cache_final_value(curr, 0.0); + continue; + } + + Cell *driver = nullptr; + if (auto it_drv = driver_map->find(curr); it_drv != driver_map->end()) + driver = it_drv->second; + if (driver == nullptr || is_timing_boundary_cell(driver, cell_types)) { + cache_final_value(curr, 0.0); + continue; + } + + TraversalState state = get_visit_state(curr); + + if (!e.finalize_phase) + { + if (state == TraversalState::Done) + continue; + if (state == TraversalState::Active) { + // Node already on current path, skip duplicate expansion + continue; + } + + set_visit_state(curr, TraversalState::Active); + eval_stack.push_back({curr, true}); + for (const auto &[port_id, sig] : driver->connections()) { + if (!driver->input(port_id)) + continue; + for (auto in_bit : sigmap(sig)) { + if (!in_bit.wire || arrival_cache.count(in_bit)) + continue; + if (get_visit_state(in_bit) == TraversalState::Active) { + cycle_detected = true; + continue; + } + eval_stack.push_back({in_bit, false}); + } + } + continue; + } + + double max_input = 0.0; + for (const auto &[port_id, sig] : driver->connections()) { + if (!driver->input(port_id)) + continue; + for (auto in_bit : sigmap(sig)) { + double in_arrival = 0.0; + if (in_bit.wire) { + auto it = arrival_cache.find(in_bit); + if (it != arrival_cache.end()) + in_arrival = it->second; + else { + // Missing child arrival at finalize implies combinational cycle + cycle_detected = true; + in_arrival = std::numeric_limits::infinity(); + } + } + max_input = std::max(max_input, in_arrival); + } + } + + double cell_delay = estimate_cell_delay(driver, cell_y_width(driver)); + double t = max_input + cell_delay; + cache_final_value(curr, t); + } + + auto it = arrival_cache.find(start); + return it != arrival_cache.end() ? it->second : 0.0; + } +}; + +// ----------------------------------------------------------------------------- +// Rewrite planning and emission +// ----------------------------------------------------------------------------- + +static int natural_output_width(WidthRule width_rule, int a_width, int b_width) +{ + switch (width_rule) + { + case WidthRule::AddCarry: + return std::max(a_width, b_width) + 1; + case WidthRule::MaxInput: + default: + return std::max(a_width, b_width); + } +} + +static int minimum_y_width_for_reassociation(WidthRule width_rule, int a_width, int b_width) +{ + if (width_rule == WidthRule::AddCarry) + // Validation-only relaxation for modulo 2^N add reassociation + return std::max(a_width, b_width); + return natural_output_width(width_rule, a_width, b_width); +} + +struct TreeLeaf +{ + SigSpec signal; + double arrival_time = 0.0; + int width = 0; + bool is_signed = false; + int stable_id = 0; +}; + +struct MergeShape +{ + int out_width = 1; + bool a_signed = false; + bool b_signed = false; + bool out_signed = false; +}; + +struct PlannedMerge +{ + int lhs_node = -1; + int rhs_node = -1; + MergeShape shape; +}; + +// Immutable plan produced by HuffmanPlanner and consumed by TreeEmitter +struct TreePlan +{ + // Node ids are dense: + // - [0, leaves) are leaf nodes + // - [leaves, leaves+merges) are merge nodes in emission order + std::vector leaves; + std::vector merges; + int root_node = -1; + double output_arrival = 0.0; + + bool valid() const { return root_node >= 0; } + + int node_count() const { return GetSize(leaves) + GetSize(merges); } +}; + +// Computes merge order and expected arrival, does not mutate RTLIL +struct HuffmanPlanner +{ + struct PlanNode + { + int node_id = -1; + double arrival_time = 0.0; + int width = 0; + bool is_signed = false; + int stable_id = 0; + }; + + struct PlanNodeCmp + { + bool operator()(const PlanNode &a, const PlanNode &b) const + { + // Use a min-heap by inverting comparator for std::priority_queue + return std::tie(a.arrival_time, a.width, a.stable_id) > + std::tie(b.arrival_time, b.width, b.stable_id); + } + }; + + MergeShape compute_merge_shape(const TreeLeaf &a, const TreeLeaf &b, + const SupportedCellSpec &spec, int target_out_width, bool force_root_width) const + { + int out_width = std::max(1, target_out_width); + if (!force_root_width && spec.width_rule == WidthRule::AddCarry) + out_width = std::min(out_width, natural_output_width(spec.width_rule, a.width, b.width)); + bool a_signed = a.is_signed; + bool b_signed = b.is_signed; + bool out_signed = yosys_binary_result_signed(a_signed, b_signed); + return {out_width, a_signed, b_signed, out_signed}; + } + + double compute_merge_arrival(double a_arrival, double b_arrival, int out_width, const Cell *delay_ref_cell) const + { + return std::max(a_arrival, b_arrival) + estimate_cell_delay(delay_ref_cell, out_width); + } + + TreePlan plan(const std::vector &leaves, IdString cell_type, Cell *reference_cell) const + { + // Deterministic leaf ordering is provided by build_tree_leaves() + TreePlan plan; + if (leaves.empty()) + return plan; + plan.leaves = leaves; + if (GetSize(leaves) == 1) { + plan.root_node = 0; + plan.output_arrival = leaves.front().arrival_time; + return plan; + } + + const SupportedCellSpec *spec = get_supported_cell_spec(cell_type); + if (spec == nullptr) + return {}; + + int target_out_width = std::max(1, cell_y_width(reference_cell)); + + std::priority_queue, PlanNodeCmp> pq; + for (int i = 0; i < GetSize(leaves); i++) { + const auto &leaf = leaves[i]; + pq.push({i, leaf.arrival_time, leaf.width, leaf.is_signed, leaf.stable_id}); + } + + int next_internal_id = GetSize(leaves); + int next_stable_id = GetSize(leaves); + /* Greedy Huffman merge always pops the two best nodes first, + * stable_id makes tie breaks deterministic for equal arrival and width, + * root merge forces target width to preserve the head output contract + */ + while (GetSize(pq) > 1) + { + PlanNode a = pq.top(); pq.pop(); + PlanNode b = pq.top(); pq.pop(); + + bool force_root_width = pq.empty(); + TreeLeaf a_leaf = {SigSpec(), a.arrival_time, a.width, a.is_signed, a.stable_id}; + TreeLeaf b_leaf = {SigSpec(), b.arrival_time, b.width, b.is_signed, b.stable_id}; + MergeShape shape = compute_merge_shape(a_leaf, b_leaf, *spec, target_out_width, force_root_width); + int out_width = shape.out_width; + double new_arrival = compute_merge_arrival(a.arrival_time, b.arrival_time, out_width, reference_cell); + + int node_id = next_internal_id++; + plan.merges.push_back({a.node_id, b.node_id, shape}); + pq.push({node_id, new_arrival, out_width, shape.out_signed, next_stable_id++}); + } + + log_assert(!pq.empty()); + plan.root_node = pq.top().node_id; + plan.output_arrival = pq.top().arrival_time; + return plan; + } +}; + +// TreeEmitter materializes a precomputed plan into RTLIL cells and wires +struct TreeEmitter +{ + Module *module; + dict &cell_count; + + TreeEmitter(Module *module, dict &cell_count) : + module(module), cell_count(cell_count) { } + + SigSpec apply(const TreePlan &plan, IdString cell_type, Cell *reference_cell) + { + if (!plan.valid() || plan.leaves.empty()) + return {}; + if (GetSize(plan.leaves) == 1) + return plan.leaves.front().signal; + + int total_nodes = plan.node_count(); + std::vector node_signals(total_nodes); + for (int i = 0; i < GetSize(plan.leaves); i++) + node_signals[i] = plan.leaves[i].signal; + + for (int merge_idx = 0; merge_idx < GetSize(plan.merges); merge_idx++) + { + const PlannedMerge &m = plan.merges[merge_idx]; + log_assert(m.lhs_node >= 0 && m.lhs_node < total_nodes); + log_assert(m.rhs_node >= 0 && m.rhs_node < total_nodes); + + SigSpec a_sig = node_signals[m.lhs_node]; + SigSpec b_sig = node_signals[m.rhs_node]; + log_assert(GetSize(a_sig) > 0 && GetSize(b_sig) > 0); + + IdString new_cell_name = make_id(reference_cell, "timing_balance"); + Cell *new_cell = module->addCell(new_cell_name, cell_type); + new_cell->set_bool_attribute(kAttrTimingBalanceGenerated); + new_cell->set_src_attribute(reference_cell->get_src_attribute()); + IdString out_wire_name = make_id(reference_cell, "timing_balance_y"); + Wire *out_wire = module->addWire(out_wire_name, m.shape.out_width); + + new_cell->setPort(ID::A, a_sig); + new_cell->setPort(ID::B, b_sig); + new_cell->setPort(ID::Y, out_wire); + if (new_cell->hasParam(ID::A_SIGNED)) + new_cell->setParam(ID::A_SIGNED, m.shape.a_signed); + if (new_cell->hasParam(ID::B_SIGNED)) + new_cell->setParam(ID::B_SIGNED, m.shape.b_signed); + if (new_cell->hasParam(ID::A_WIDTH)) + new_cell->setParam(ID::A_WIDTH, GetSize(a_sig)); + if (new_cell->hasParam(ID::B_WIDTH)) + new_cell->setParam(ID::B_WIDTH, GetSize(b_sig)); + if (new_cell->hasParam(ID::Y_WIDTH)) + new_cell->setParam(ID::Y_WIDTH, m.shape.out_width); + new_cell->fixup_parameters(); + + int node_id = GetSize(plan.leaves) + merge_idx; + node_signals[node_id] = SigSpec(out_wire); + cell_count[cell_type]++; + } + + log_assert(plan.root_node >= 0 && plan.root_node < total_nodes); + return node_signals[plan.root_node]; + } +}; + +// ----------------------------------------------------------------------------- +// Rewrite engine: cluster harvest, evaluation, and commit loop +// ----------------------------------------------------------------------------- + +// Harvested cluster plus external source multiset for one candidate head +struct ClusterHarvest +{ + // Track source multiplicity by signedness to preserve per-use semantics + dict signed_source_uses; + dict unsigned_source_uses; + pool cluster_cells; +}; + +// Worker contract: +// Finds heads for each target type, harvests and evaluates clusters, commits +// beneficial disjoint rewrites in-sweep, and rebuilds views between iterations +struct OptTimingBalanceWorker +{ + struct RewriteStats + { + int candidates = 0; + int trees = 0; + int rewrites = 0; + }; + + struct RewriteDecision + { + SigSpec head_output; + TreePlan plan; + }; + + struct ObjectiveScore + { + double sum_arrival = 0.0; + }; + + struct SweepContext + { + pool candidate_cells; + pool consumed_cells; + RewriteStats stats; + dict target_cache; + dict y_cache; + }; + + Design *design; + Module *module; + SigMap sigmap; + CellTypes cell_types; + std::vector target_cell_ids; + dict cell_count; + HuffmanPlanner planner; + TreeEmitter emitter; + dict warned_contract_issues; + static constexpr int warnRequiredPortsErrCode = 1; + static constexpr int warnRequiredWidthParamsErrCode = 2; + + OptTimingBalanceWorker(Design *design, Module *module, const std::vector &target_cell_ids) : + design(design), module(module), sigmap(module), cell_types(design), target_cell_ids(target_cell_ids), + planner(), emitter(module, cell_count) + { } + + // View lifecycle + void rebuild_views(ConnectivitySnapshot &graph, TimingOracle &timer) + { + sigmap = SigMap(module); + graph.build(module, sigmap); + timer.rebind_driver_map(graph.unique_driver_by_bit); + } + + // Warnings and objective gate + void warn_contract_once(IdString cell_type, int err_code) + { + int &mask = warned_contract_issues[cell_type]; + if (mask & err_code) + return; + mask |= err_code; + if (err_code == warnRequiredPortsErrCode) { + log_warning("opt_timing_balance: skipping %s cells without A/B/Y ports in module %s.\n", + log_id(cell_type), log_id(module)); + } else { + log_warning("opt_timing_balance: skipping %s cells without width parameters in module %s. " + "Pass expects word-level RTL cells (run before gate-level techmapping).\n", + log_id(cell_type), log_id(module)); + } + } + + bool objective_improved(const ObjectiveScore &objective_before, const ObjectiveScore &objective_after) const + { + if (!std::isfinite(objective_after.sum_arrival)) + return false; + if (!std::isfinite(objective_before.sum_arrival)) + return true; + // Sum-only gating can regress the worst single path, but may unlock deferred global gains in later iterations + return objective_after.sum_arrival < objective_before.sum_arrival - kMinIterationDelta; + } + + // Candidate and head predicates + bool is_target_cell_type(Cell *cell, IdString cell_type, bool exclude_generated) + { + if (cell == nullptr || cell->type != cell_type) + return false; + if (exclude_generated && cell->get_bool_attribute(kAttrTimingBalanceGenerated)) + return false; + const SupportedCellSpec *spec = get_supported_cell_spec(cell_type); + if (spec == nullptr) + return false; + if (!cell->hasPort(ID::A) || !cell->hasPort(ID::B) || !cell->hasPort(ID::Y)) { + warn_contract_once(cell_type, warnRequiredPortsErrCode); + return false; + } + if (!cell->hasParam(ID::Y_WIDTH) || !cell->hasParam(ID::A_WIDTH) || !cell->hasParam(ID::B_WIDTH)) { + warn_contract_once(cell_type, warnRequiredWidthParamsErrCode); + return false; + } + + int y_width = cell->getParam(ID::Y_WIDTH).as_int(); + int a_width = cell->getParam(ID::A_WIDTH).as_int(); + int b_width = cell->getParam(ID::B_WIDTH).as_int(); + if (y_width <= 0 || a_width <= 0 || b_width <= 0) + return false; + if (GetSize(cell->getPort(ID::A)) != a_width) + return false; + if (GetSize(cell->getPort(ID::B)) != b_width) + return false; + if (GetSize(cell->getPort(ID::Y)) != y_width) + return false; + + if (spec->requires_matching_signedness) { + if (!cell->hasParam(ID::A_SIGNED) || !cell->hasParam(ID::B_SIGNED)) + return false; + } + + int required_width = minimum_y_width_for_reassociation(spec->width_rule, a_width, b_width); + return y_width >= required_width; + } + + bool is_target_cell_type_cached(Cell *cell, IdString cell_type, + bool exclude_generated, dict &target_cache) + { + if (cell == nullptr) + return false; + auto it = target_cache.find(cell); + if (it != target_cache.end()) + return it->second; + bool is_target = is_target_cell_type(cell, cell_type, exclude_generated); + target_cache[cell] = is_target; + return is_target; + } + + const SigSpec &mapped_y(Cell *cell, dict &y_cache) + { + auto it = y_cache.find(cell); + if (it != y_cache.end()) + return it->second; + y_cache[cell] = sigmap(cell->getPort(ID::Y)); + return y_cache[cell]; + } + + // Backward cluster extraction + bool is_head_cell(Cell *cell, IdString cell_type, bool exclude_generated, + ConnectivitySnapshot &graph, dict &target_cache, dict &y_cache) + { + if (cell == nullptr) + return false; + const SigSpec &y = mapped_y(cell, y_cache); + // Output-port drivers are always heads + for (auto bit : y) + if (graph.output_port_bits.count(bit)) + return true; + + pool sinks; + graph.collect_sinks_mapped(y, sinks); + // Leaf drivers are heads + if (sinks.empty()) + return true; + + // Any non-target consumer terminates same-type chain growth + for (Cell *sink : sinks) { + if (!is_target_cell_type_cached(sink, cell_type, exclude_generated, target_cache)) + return true; + } + return false; + } + + /* + * BFS over same-type unique drivers from head_cell, + * merge only when driver Y exactly matches consumed mapped bits to avoid semantic drift, + * when merge stops, record source use count with per-port signedness + */ + bool collect_cluster(IdString cell_type, Cell *head_cell, const pool &candidate_cells, + ConnectivitySnapshot &graph, dict &target_cache, dict &y_cache, + ClusterHarvest &harvest) + { + const SupportedCellSpec *spec = get_supported_cell_spec(cell_type); + if (spec == nullptr || head_cell == nullptr) + return false; + + bool enforce_strict_width_match = spec->requires_strict_width_match; + int target_width = 0; + if (enforce_strict_width_match) { + // Strict width preserves truncation points + target_width = cell_y_width(head_cell); + } + + bool enforce_matching_signedness = spec->requires_matching_signedness; + bool target_add_signed = false; + if (enforce_matching_signedness) { + if (!head_cell->hasParam(ID::A_SIGNED) || !head_cell->hasParam(ID::B_SIGNED)) + return false; + bool head_a_signed = head_cell->getParam(ID::A_SIGNED).as_bool(); + bool head_b_signed = head_cell->getParam(ID::B_SIGNED).as_bool(); + if (head_a_signed != head_b_signed) + return false; + target_add_signed = head_a_signed; + } + + harvest = ClusterHarvest(); + harvest.cluster_cells.insert(head_cell); + std::deque queue = {head_cell}; + + while (!queue.empty()) + { + Cell *cell = queue.front(); + queue.pop_front(); + + for (IdString port : {ID::A, ID::B}) { + SigSpec sig = sigmap(cell->getPort(port)); + Cell *driver = graph.get_unique_driver_mapped(sig); + + bool can_merge = true; + if (driver == nullptr || driver == cell || !candidate_cells.count(driver)) + can_merge = false; + if (can_merge && !is_target_cell_type_cached(driver, cell_type, true, target_cache)) + can_merge = false; + + if (can_merge) { + const SigSpec &drv_y = mapped_y(driver, y_cache); + // Require exact Y coverage for safe reassociation + if (GetSize(drv_y) != GetSize(sig) || drv_y != sig) + can_merge = false; + } + if (can_merge && enforce_strict_width_match && + cell_y_width(driver) != target_width) + can_merge = false; + if (can_merge && enforce_matching_signedness) { + if (!driver->hasParam(ID::A_SIGNED) || !driver->hasParam(ID::B_SIGNED)) + can_merge = false; + else { + bool a_signed = driver->getParam(ID::A_SIGNED).as_bool(); + bool b_signed = driver->getParam(ID::B_SIGNED).as_bool(); + if (a_signed != b_signed || a_signed != target_add_signed) + can_merge = false; + } + } + + if (can_merge) { + if (!harvest.cluster_cells.count(driver)) { + harvest.cluster_cells.insert(driver); + queue.push_back(driver); + } + continue; + } + + IdString signed_param = port == ID::A ? ID::A_SIGNED : ID::B_SIGNED; + bool signed_port = cell->hasParam(signed_param) && cell->getParam(signed_param).as_bool(); + if (signed_port) + harvest.signed_source_uses[sig]++; + else + harvest.unsigned_source_uses[sig]++; + } + } + + // Single-cell cluster is a no-op + return GetSize(harvest.cluster_cells) > 1; + } + + std::vector collect_candidates(IdString cell_type, bool exclude_generated, dict &target_cache) + { + std::vector cells; + for (Cell *cell : module->selected_cells()) + if (is_target_cell_type_cached(cell, cell_type, exclude_generated, target_cache)) + cells.push_back(cell); + // Sort lexically for cross-run deterministic candidate order + std::sort(cells.begin(), cells.end(), [](Cell *a, Cell *b) { + return std::strcmp(a->name.c_str(), b->name.c_str()) < 0; + }); + return cells; + } + + // Rewrite evaluation and commit + void rewrite_one_head(IdString cell_type, Cell *head, SweepContext &sweep, + ConnectivitySnapshot &graph, TimingOracle &timer) + { + // No per-head rebuild in this sweep, defer heads that read already consumed drivers + auto source_uses_consumed_driver = [&](const dict &uses) -> bool { + // Stale snapshot guard: skip heads fed by already rewritten clusters + for (const auto &[sig, use_count] : uses) { + if (use_count <= 0) + continue; + for (auto bit : sig) { + if (!bit.wire) + continue; + auto drv_it = graph.unique_driver_by_bit.find(bit); + if (drv_it == graph.unique_driver_by_bit.end()) + continue; + Cell *driver = drv_it->second; + if (driver != nullptr && sweep.consumed_cells.count(driver)) + return true; + } + } + return false; + }; + + if (sweep.consumed_cells.count(head)) + return; + if (!is_head_cell(head, cell_type, true, graph, sweep.target_cache, sweep.y_cache)) + return; + + ClusterHarvest harvest; + if (!collect_cluster(cell_type, head, sweep.candidate_cells, graph, sweep.target_cache, sweep.y_cache, harvest)) + return; + + // Batch only disjoint clusters in one sweep + for (Cell *cell : harvest.cluster_cells) + if (cell != nullptr && sweep.consumed_cells.count(cell)) + return; + + // Defer heads that depend on already rewritten snapshot drivers + if (source_uses_consumed_driver(harvest.signed_source_uses) || + source_uses_consumed_driver(harvest.unsigned_source_uses)) + return; + + RewriteDecision decision; + if (!evaluate_rewrite(cell_type, head, harvest, timer, decision)) + return; + if (!commit_rewrite(cell_type, head, decision)) + return; + + for (Cell *cell : harvest.cluster_cells) + if (cell != nullptr) + sweep.consumed_cells.insert(cell); + sweep.stats.rewrites++; + + // No per-head rebuild, invalidate rewritten Y-cache entries only + for (Cell *cell : harvest.cluster_cells) + if (cell != nullptr) + sweep.y_cache.erase(cell); + sweep.y_cache.erase(head); + } + + std::vector order_heads_by_dependency(const std::vector &heads, ConnectivitySnapshot &graph, bool &saw_cycle) + { + saw_cycle = false; + if (heads.empty()) + return {}; + + /* + * Backward DFS over driver links, + * postorder emits upstream-first head order, + * cycles fall back to conservative skip in this sweep + */ + pool head_cells; + for (auto head : heads) + head_cells.insert(head); + + dict state; + std::vector postorder_heads; + struct DfsEntry { + Cell *cell; + bool postorder; + }; + std::vector stack; + stack.reserve(kTraversalStackReserve); + + for (auto root : heads) + { + if (root == nullptr) + continue; + + stack.clear(); + stack.push_back({root, false}); + while (!stack.empty()) + { + DfsEntry e = stack.back(); + stack.pop_back(); + Cell *cell = e.cell; + if (cell == nullptr || is_timing_boundary_cell(cell, cell_types)) + continue; + + TraversalState st = TraversalState::Unseen; + if (auto it = state.find(cell); it != state.end()) + st = it->second; + + if (e.postorder) { + if (st != TraversalState::Done) { + state[cell] = TraversalState::Done; + if (head_cells.count(cell)) + postorder_heads.push_back(cell); + } + continue; + } + + if (st == TraversalState::Done) + continue; + if (st == TraversalState::Active) { + saw_cycle = true; + continue; + } + + state[cell] = TraversalState::Active; + stack.push_back({cell, true}); + + for (const auto &[port_id, sig] : cell->connections()) { + if (!cell->input(port_id)) + continue; + for (auto bit : sigmap(sig)) { + if (!bit.wire) + continue; + auto drv_it = graph.unique_driver_by_bit.find(bit); + if (drv_it == graph.unique_driver_by_bit.end()) + continue; + Cell *driver = drv_it->second; + if (driver == nullptr || driver == cell) + continue; + stack.push_back({driver, false}); + } + } + } + } + + if (saw_cycle) + log_warning("opt_timing_balance: cycle detected in head ordering in module %s, using conservative order.\n", + log_id(module)); + + // Preserve deterministic order for disconnected heads + pool seen_heads; + std::vector ordered_heads; + ordered_heads.reserve(GetSize(heads)); + for (auto head : postorder_heads) { + if (!seen_heads.count(head)) { + seen_heads.insert(head); + ordered_heads.push_back(head); + } + } + for (auto head : heads) { + if (!seen_heads.count(head)) + ordered_heads.push_back(head); + } + return ordered_heads; + } + + bool build_tree_leaves(const ClusterHarvest &harvest, TimingOracle &timer, std::vector &leaves) + { + struct SourceUse { + SigSpec sig; + bool is_signed; + int count; + }; + + leaves.clear(); + int stable_id = 0; + + // Deterministic source-use ordering for stable tree shape + std::vector uses; + uses.reserve(GetSize(harvest.signed_source_uses) + GetSize(harvest.unsigned_source_uses)); + for (const auto &[sig, count] : harvest.signed_source_uses) + uses.push_back({sig, true, count}); + for (const auto &[sig, count] : harvest.unsigned_source_uses) + uses.push_back({sig, false, count}); + std::sort(uses.begin(), uses.end(), [](const SourceUse &a, const SourceUse &b) { + if (a.sig != b.sig) + return less_sigspec_key(a.sig, b.sig); + if (a.is_signed != b.is_signed) + return a.is_signed > b.is_signed; + return a.count < b.count; + }); + + for (const auto &use : uses) + { + if (use.count <= 0) + continue; + double src_arrival = timer.get_arrival(use.sig); + if (!std::isfinite(src_arrival)) + return false; + + for (int i = 0; i < use.count; i++) + leaves.push_back({use.sig, src_arrival, GetSize(use.sig), use.is_signed, stable_id++}); + } + + return !leaves.empty() && !timer.cycle_detected; + } + + bool evaluate_rewrite(IdString cell_type, Cell *head_cell, const ClusterHarvest &harvest, + TimingOracle &timer, RewriteDecision &decision) + { + decision = RewriteDecision(); + // Keep exact head output bits. Mapping here can rewire the wrong alias + decision.head_output = head_cell->getPort(ID::Y); + + std::vector leaves; + if (!build_tree_leaves(harvest, timer, leaves)) + return false; + + double old_arrival = timer.get_arrival(decision.head_output); + if (timer.cycle_detected || !std::isfinite(old_arrival)) + return false; + + decision.plan = planner.plan(leaves, cell_type, head_cell); + if (!decision.plan.valid()) + return false; + + double estimated_new_arrival = decision.plan.output_arrival; + if (!std::isfinite(estimated_new_arrival) || estimated_new_arrival >= old_arrival - kMinIterationDelta) + return false; + return true; + } + + bool commit_rewrite(IdString cell_type, Cell *head_cell, + const RewriteDecision &decision) + { + SigSpec head_output = decision.head_output; + SigSpec tree_output = emitter.apply(decision.plan, cell_type, head_cell); + if (GetSize(head_output) <= 0 || GetSize(tree_output) <= 0) + return false; + if (GetSize(head_output) != GetSize(tree_output)) + return false; + + // Detach old driver first to avoid transient multi-driver aliasing + IdString detached_name = make_id(head_cell, "timing_balance_detach"); + Wire *detached = module->addWire(detached_name, std::max(1, GetSize(head_output))); + head_cell->setPort(ID::Y, SigSpec(detached)); + if (head_cell->hasParam(ID::Y_WIDTH)) + head_cell->setParam(ID::Y_WIDTH, GetSize(head_output)); + head_cell->fixup_parameters(); + + module->connect(head_output, tree_output); + return true; + } + + // Objective and per-type sweep + ObjectiveScore compute_delay_objective(const std::vector &target_cell_ids, ConnectivitySnapshot &graph, TimingOracle &timer) + { + ObjectiveScore objective; + for (auto cell_type : target_cell_ids) + { + dict target_cache; + dict y_cache; + std::vector candidates = collect_candidates(cell_type, false, target_cache); + std::vector heads; + for (Cell *cell : candidates) { + if (is_head_cell(cell, cell_type, false, graph, target_cache, y_cache)) + heads.push_back(cell); + } + + for (Cell *cell : heads) { + double arrival = timer.get_arrival(cell->getPort(ID::Y)); + if (timer.cycle_detected || !std::isfinite(arrival)) + return {std::numeric_limits::infinity()}; + objective.sum_arrival += arrival; + } + } + return objective; + } + + RewriteStats process_cell_type_once(IdString cell_type, ConnectivitySnapshot &graph, TimingOracle &timer) + { + SweepContext sweep; + std::vector candidates = collect_candidates(cell_type, true, sweep.target_cache); + for (Cell *cell : candidates) + sweep.candidate_cells.insert(cell); + sweep.stats.candidates = GetSize(candidates); + + std::vector heads; + for (Cell *cell : candidates) + if (is_head_cell(cell, cell_type, true, graph, sweep.target_cache, sweep.y_cache)) + heads.push_back(cell); + sweep.stats.trees = GetSize(heads); + + bool saw_cycle = false; + std::vector ordered_heads = order_heads_by_dependency(heads, graph, saw_cycle); + if (saw_cycle) { + // Cyclic cones are rejected conservatively for this sweep + return sweep.stats; + } + + for (Cell *head : ordered_heads) + rewrite_one_head(cell_type, head, sweep, graph, timer); + return sweep.stats; + } + + // Top-level worker loop + void run() + { + if (target_cell_ids.empty()) + return; + + ConnectivitySnapshot graph(module, sigmap); + TimingOracle timer(cell_types, sigmap, graph.unique_driver_by_bit); + + ObjectiveScore objective_before = compute_delay_objective(target_cell_ids, graph, timer); + bool stopped_early = false; + log(" processing module %s\n", log_id(module)); + log_flush(); + + for (int iter = 0; iter < kMaxPassIterations; iter++) { + ObjectiveScore iter_before = objective_before; + ObjectiveScore iter_after = iter_before; + bool improved = false; + int generated_before = 0; + for (IdString cell_type : target_cell_ids) + generated_before += cell_count[cell_type]; + + log(" iteration %d/%d begin\n", iter + 1, kMaxPassIterations); + int total_rewrites = 0; + for (IdString cell_type : target_cell_ids) { + RewriteStats stats = process_cell_type_once(cell_type, graph, timer); + total_rewrites += stats.rewrites; + log(" %s trees=%d candidates=%d rewrites=%d\n", + log_id(cell_type), stats.trees, stats.candidates, stats.rewrites); + } + + int generated_after = 0; + for (IdString cell_type : target_cell_ids) + generated_after += cell_count[cell_type]; + int generated_delta = generated_after - generated_before; + log(" rewrote_trees=%d generated_cells=%d\n", total_rewrites, generated_delta); + + if (total_rewrites > 0) { + log(" clean -purge begin\n"); + Pass::call_on_module(design, module, "clean -purge"); + log(" clean -purge end\n"); + rebuild_views(graph, timer); + iter_after = compute_delay_objective(target_cell_ids, graph, timer); + improved = objective_improved(iter_before, iter_after); + } + + log(" before = %.3f after = %.3f, %s\n", + iter_before.sum_arrival, iter_after.sum_arrival, + improved ? "timing estimation improved, continuing" : "timing estimation did not improve, stopping"); + log(" iteration %d/%d end\n", iter + 1, kMaxPassIterations); + log_flush(); + + if (!improved) { + stopped_early = true; + break; + } + objective_before = iter_after; + } + + if (!stopped_early) { + log(" reached iteration cap %d stopping\n", kMaxPassIterations); + log_flush(); + } + } +}; + +// ----------------------------------------------------------------------------- +// Pass wrapper +// ----------------------------------------------------------------------------- + +struct OptTimingBalancePass : public Pass +{ + OptTimingBalancePass() : Pass("opt_timing_balance", "timing-aware balancing of associative trees") { } + + void help() override + { + log("\n"); + log(" opt_timing_balance [options] [selection]\n"); + log("\n"); + log("Iterative timing-aware balancing for cascaded associative cells.\n"); + log("Uses lazy backward arrival estimation plus DAG-ordered Huffman rebuilding.\n"); + log("\n"); + log(" -arith\n"); + log(" only convert arithmetic cells ($add).\n"); + log("\n"); + log(" -logic\n"); + log(" only convert logic cells ($and/$or/$xor).\n"); + log("\n"); + } + + void execute(std::vector args, RTLIL::Design *design) override + { + log_header(design, "Executing OPT_TIMING_BALANCE pass (iterative timing-aware tree rewrite).\n"); + + size_t argidx; + bool saw_type_flag = false; + bool enable_arith = false; + bool enable_logic = false; + for (argidx = 1; argidx < (size_t)GetSize(args); argidx++) { + if (args[argidx] == "-arith") { + saw_type_flag = true; + enable_arith = true; + continue; + } + if (args[argidx] == "-logic") { + saw_type_flag = true; + enable_logic = true; + continue; + } + // Remaining args are selection filters + break; + } + extra_args(args, argidx, design); + + if (!saw_type_flag) { + enable_arith = true; + enable_logic = true; + } + + std::vector target_cell_ids = collect_target_cell_ids(enable_logic, enable_arith); + + dict cell_count; + for (auto module : design->selected_modules()) { + OptTimingBalanceWorker worker(design, module, target_cell_ids); + worker.run(); + for (const auto &[type, count] : worker.cell_count) + cell_count[type] += count; + } + + for (auto cell_type : target_cell_ids) { + log(" Converted %d %s cells into timing-balanced trees.\n", cell_count[cell_type], log_id(cell_type)); + } + } +} OptTimingBalancePass; + +PRIVATE_NAMESPACE_END diff --git a/tests/silimate/opt_timing_balance.ys b/tests/silimate/opt_timing_balance.ys new file mode 100644 index 000000000..4c5b118b4 --- /dev/null +++ b/tests/silimate/opt_timing_balance.ys @@ -0,0 +1,511 @@ +# +# opt_timing_balance regression coverage +# + +# --------------------------------------------------------------------------- +# Case: XOR chain with late leaf should be rewritten +# --------------------------------------------------------------------------- +log -header "opt_timing_balance: xor late leaf rewrite" +log -push +design -reset +read_verilog < Date: Fri, 27 Feb 2026 10:12:43 -0800 Subject: [PATCH 03/44] working implementation that i will improvee further --- kernel/fstdata.cc | 106 +++++++++++++++++++++++++++++++++++++++++----- kernel/fstdata.h | 3 ++ 2 files changed, 98 insertions(+), 11 deletions(-) diff --git a/kernel/fstdata.cc b/kernel/fstdata.cc index f0f00181c..3e3e8d127 100644 --- a/kernel/fstdata.cc +++ b/kernel/fstdata.cc @@ -114,16 +114,46 @@ void FstData::extractVarNames() struct fstHier *h; std::string fst_scope_name; + // Track current fork scope + std::string current_fork_scope; + fstHandle next_virtual_handle = (fstHandle)-1; // forks get negative handle values for better identification + while ((h = fstReaderIterateHier(ctx))) { switch (h->htyp) { - case FST_HT_SCOPE: { - fst_scope_name = fstReaderPushScope(ctx, h->u.scope.name, NULL); - break; + case FST_HT_SCOPE: { + fst_scope_name = fstReaderPushScope(ctx, h->u.scope.name, NULL); + + // Fork scopes are identified by FST_ST_VCD_FORK + if (h->u.scope.typ == FST_ST_VCD_FORK) { + current_fork_scope = fst_scope_name; + // Create new vector that contains struct members + fork_scopes[current_fork_scope] = std::vector(); } - case FST_HT_UPSCOPE: { - fst_scope_name = fstReaderPopScope(ctx); - break; + break; + } + case FST_HT_UPSCOPE: { + if (!current_fork_scope.empty() && current_fork_scope == fst_scope_name) { + + // Create a virtual handle + fstHandle virtual_handle = next_virtual_handle--; + + // Map the scope to the virtual handle + name_to_handle[current_fork_scope] = virtual_handle; + virtual_to_fork[virtual_handle] = current_fork_scope; + + // Calculate total width of the struct and map it to the virtual handle + int total_width = 0; + for (fstHandle member : fork_scopes[current_fork_scope]) { + if (handle_to_var.find(member) != handle_to_var.end()) { + total_width += handle_to_var[member].width; + } + } + virtual_handle_widths[virtual_handle] = total_width; + current_fork_scope.clear(); } + fst_scope_name = fstReaderPopScope(ctx); + break; + } case FST_HT_VAR: { FstVar var; var.id = h->u.var.handle; @@ -133,10 +163,13 @@ void FstData::extractVarNames() var.scope = fst_scope_name; normalize_brackets(var.scope); var.width = h->u.var.length; - vars.push_back(var); - if (!var.is_alias) - handle_to_var[h->u.var.handle] = var; - std::string clean_name; + vars.push_back(var); + if (!var.is_alias) + handle_to_var[h->u.var.handle] = var; + if (!current_fork_scope.empty()) { + fork_scopes[current_fork_scope].push_back(h->u.var.handle); + } + std::string clean_name; bool has_space = false; for(size_t i=0;iu.var.name);i++) { @@ -275,8 +308,59 @@ void FstData::reconstructAllAtTimes(std::vector &signal, uint64_t sta std::string FstData::valueOf(fstHandle signal) { + // Condition to check if the signal comes from a fork scope + if ((int)signal < 0 && virtual_to_fork.find(signal) != virtual_to_fork.end()) { + std::string fork_scope = virtual_to_fork[signal]; + if (fork_scopes.find(fork_scope) != fork_scopes.end()) { + + // Empty string to store the full struct values + std::string result; + + // Get the members of the fork scope + const std::vector& members = fork_scopes[fork_scope]; + for (auto it = members.rbegin(); it != members.rend(); ++it) { + fstHandle member = *it; + std::string member_val; + int expected_width = 0; + + // Get the width of the member + if (handle_to_var.find(member) != handle_to_var.end()) { + expected_width = handle_to_var[member].width; + } + + // Check if the member has a value + if (past_data.find(member) != past_data.end()) { + member_val = past_data[member]; + + // If the member value is shorter than the expected width, pad with zeros + if (expected_width > 0 && (int)member_val.length() < expected_width) { + member_val = std::string(expected_width - member_val.length(), '0') + member_val; + } + } else if (expected_width > 0) { + // If the member has no value, pad with x's + member_val = std::string(expected_width, 'x'); + } else { + member_val = "x"; + } + + // Add the member value to the result + result += member_val; + } + return result; + } + + // If the fork scope is not found, pad with x's based on the width + if (virtual_handle_widths.find(signal) != virtual_handle_widths.end()) { + return std::string(virtual_handle_widths[signal], 'x'); + } + return "x"; + } + if (past_data.find(signal) == past_data.end()) { - return std::string(handle_to_var[signal].width, 'x'); + if (handle_to_var.find(signal) != handle_to_var.end()) { + return std::string(handle_to_var[signal].width, 'x'); + } + return "x"; } return past_data[signal]; } diff --git a/kernel/fstdata.h b/kernel/fstdata.h index a8ae40301..b65681b00 100644 --- a/kernel/fstdata.h +++ b/kernel/fstdata.h @@ -69,6 +69,9 @@ private: uint64_t last_time; std::map past_data; uint64_t past_time; + std::map> fork_scopes; + std::map virtual_to_fork; + std::map virtual_handle_widths; double timescale; std::string timescale_str; uint64_t start_time; From 0aaca679cea2884feeab3e04d11824322368b7e6 Mon Sep 17 00:00:00 2001 From: Stan Lee Date: Fri, 27 Feb 2026 10:22:06 -0800 Subject: [PATCH 04/44] better but not ideal --- kernel/fstdata.cc | 103 +++++++++++++++++++--------------------------- kernel/fstdata.h | 4 +- 2 files changed, 43 insertions(+), 64 deletions(-) diff --git a/kernel/fstdata.cc b/kernel/fstdata.cc index 3e3e8d127..1ecf1f7e9 100644 --- a/kernel/fstdata.cc +++ b/kernel/fstdata.cc @@ -116,7 +116,8 @@ void FstData::extractVarNames() // Track current fork scope std::string current_fork_scope; - fstHandle next_virtual_handle = (fstHandle)-1; // forks get negative handle values for better identification + fstHandle next_fork_handle = 0x80000000; + std::map> fork_scopes; while ((h = fstReaderIterateHier(ctx))) { switch (h->htyp) { @@ -133,22 +134,13 @@ void FstData::extractVarNames() } case FST_HT_UPSCOPE: { if (!current_fork_scope.empty() && current_fork_scope == fst_scope_name) { - - // Create a virtual handle - fstHandle virtual_handle = next_virtual_handle--; - - // Map the scope to the virtual handle - name_to_handle[current_fork_scope] = virtual_handle; - virtual_to_fork[virtual_handle] = current_fork_scope; - - // Calculate total width of the struct and map it to the virtual handle - int total_width = 0; - for (fstHandle member : fork_scopes[current_fork_scope]) { - if (handle_to_var.find(member) != handle_to_var.end()) { - total_width += handle_to_var[member].width; - } - } - virtual_handle_widths[virtual_handle] = total_width; + fstHandle fork_handle = next_fork_handle++; + std::string normalized_fork_scope = current_fork_scope; + normalize_brackets(normalized_fork_scope); + + name_to_handle[normalized_fork_scope] = fork_handle; + fork_scope_members[fork_handle] = fork_scopes[current_fork_scope]; + current_fork_scope.clear(); } fst_scope_name = fstReaderPopScope(ctx); @@ -308,54 +300,43 @@ void FstData::reconstructAllAtTimes(std::vector &signal, uint64_t sta std::string FstData::valueOf(fstHandle signal) { - // Condition to check if the signal comes from a fork scope - if ((int)signal < 0 && virtual_to_fork.find(signal) != virtual_to_fork.end()) { - std::string fork_scope = virtual_to_fork[signal]; - if (fork_scopes.find(fork_scope) != fork_scopes.end()) { - - // Empty string to store the full struct values - std::string result; - - // Get the members of the fork scope - const std::vector& members = fork_scopes[fork_scope]; - for (auto it = members.rbegin(); it != members.rend(); ++it) { - fstHandle member = *it; - std::string member_val; - int expected_width = 0; - - // Get the width of the member - if (handle_to_var.find(member) != handle_to_var.end()) { - expected_width = handle_to_var[member].width; - } - - // Check if the member has a value - if (past_data.find(member) != past_data.end()) { - member_val = past_data[member]; - - // If the member value is shorter than the expected width, pad with zeros - if (expected_width > 0 && (int)member_val.length() < expected_width) { - member_val = std::string(expected_width - member_val.length(), '0') + member_val; - } - } else if (expected_width > 0) { - // If the member has no value, pad with x's - member_val = std::string(expected_width, 'x'); - } else { - member_val = "x"; - } - - // Add the member value to the result - result += member_val; + // Check if this is a fork scope (struct) + auto it = fork_scope_members.find(signal); + if (it != fork_scope_members.end()) { + std::string result; + const std::vector& members = it->second; + + // Iterate in REVERSE: first declared member is MSB in SystemVerilog packed structs + for (auto m = members.rbegin(); m != members.rend(); ++m) { + fstHandle member = *m; + std::string member_val; + int expected_width = 0; + + // Get the declared width of this member + if (handle_to_var.find(member) != handle_to_var.end()) { + expected_width = handle_to_var[member].width; } - return result; + + // Get the current value + if (past_data.find(member) != past_data.end()) { + member_val = past_data[member]; + // VCD drops leading zeros - must pad to full width + if (expected_width > 0 && (int)member_val.length() < expected_width) { + member_val = std::string(expected_width - member_val.length(), '0') + member_val; + } + } else if (expected_width > 0) { + // No value yet, use x's + member_val = std::string(expected_width, 'x'); + } else { + member_val = "x"; + } + + result += member_val; } - - // If the fork scope is not found, pad with x's based on the width - if (virtual_handle_widths.find(signal) != virtual_handle_widths.end()) { - return std::string(virtual_handle_widths[signal], 'x'); - } - return "x"; + return result; } + // Normal signal handling if (past_data.find(signal) == past_data.end()) { if (handle_to_var.find(signal) != handle_to_var.end()) { return std::string(handle_to_var[signal].width, 'x'); diff --git a/kernel/fstdata.h b/kernel/fstdata.h index b65681b00..ee421f9ac 100644 --- a/kernel/fstdata.h +++ b/kernel/fstdata.h @@ -69,9 +69,7 @@ private: uint64_t last_time; std::map past_data; uint64_t past_time; - std::map> fork_scopes; - std::map virtual_to_fork; - std::map virtual_handle_widths; + std::map> fork_scope_members; double timescale; std::string timescale_str; uint64_t start_time; From 48894488f1227d2aff1a08e6a0d9cd1531a7c5da Mon Sep 17 00:00:00 2001 From: Stan Lee Date: Fri, 27 Feb 2026 11:02:46 -0800 Subject: [PATCH 05/44] better method for assigning fsthandles --- kernel/fstdata.cc | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/kernel/fstdata.cc b/kernel/fstdata.cc index 1ecf1f7e9..ec61e80d4 100644 --- a/kernel/fstdata.cc +++ b/kernel/fstdata.cc @@ -116,7 +116,9 @@ void FstData::extractVarNames() // Track current fork scope std::string current_fork_scope; - fstHandle next_fork_handle = 0x80000000; + + // Start fork handles after the maximum real handle from FST file to avoid collisions + fstHandle next_fork_handle = fstReaderGetMaxHandle(ctx) + 1; std::map> fork_scopes; while ((h = fstReaderIterateHier(ctx))) { @@ -134,13 +136,16 @@ void FstData::extractVarNames() } case FST_HT_UPSCOPE: { if (!current_fork_scope.empty() && current_fork_scope == fst_scope_name) { + // Assign a unique handle to this fork scope fstHandle fork_handle = next_fork_handle++; + + // Map normalized scope name to the handle for future lookups via getHandle() std::string normalized_fork_scope = current_fork_scope; normalize_brackets(normalized_fork_scope); - name_to_handle[normalized_fork_scope] = fork_handle; fork_scope_members[fork_handle] = fork_scopes[current_fork_scope]; - + + // Clear current fork scope for the next scope current_fork_scope.clear(); } fst_scope_name = fstReaderPopScope(ctx); @@ -158,9 +163,12 @@ void FstData::extractVarNames() vars.push_back(var); if (!var.is_alias) handle_to_var[h->u.var.handle] = var; + + // Add variable to fork scope members if we are currently in a fork scope if (!current_fork_scope.empty()) { fork_scopes[current_fork_scope].push_back(h->u.var.handle); } + std::string clean_name; bool has_space = false; for(size_t i=0;iu.var.name);i++) From ae3b9b74e2a6d6c9deb5e77e84e6f44f7fd5932f Mon Sep 17 00:00:00 2001 From: Stan Lee Date: Fri, 27 Feb 2026 11:11:05 -0800 Subject: [PATCH 06/44] ready --- kernel/fstdata.cc | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/kernel/fstdata.cc b/kernel/fstdata.cc index ec61e80d4..a4c7d7e65 100644 --- a/kernel/fstdata.cc +++ b/kernel/fstdata.cc @@ -129,7 +129,7 @@ void FstData::extractVarNames() // Fork scopes are identified by FST_ST_VCD_FORK if (h->u.scope.typ == FST_ST_VCD_FORK) { current_fork_scope = fst_scope_name; - // Create new vector that contains struct members + // Create new vector that contains struct members copied during upscope fork_scopes[current_fork_scope] = std::vector(); } break; @@ -143,6 +143,9 @@ void FstData::extractVarNames() std::string normalized_fork_scope = current_fork_scope; normalize_brackets(normalized_fork_scope); name_to_handle[normalized_fork_scope] = fork_handle; + + // Copy the extracted members of the fork scope to the fork scope members map + // for value lookups in valueOf() fork_scope_members[fork_handle] = fork_scopes[current_fork_scope]; // Clear current fork scope for the next scope @@ -313,32 +316,32 @@ std::string FstData::valueOf(fstHandle signal) if (it != fork_scope_members.end()) { std::string result; const std::vector& members = it->second; - - // Iterate in REVERSE: first declared member is MSB in SystemVerilog packed structs + + // Iterate over members of the struct to get concatenated value. + // The first declared member is MSB in SystemVerilog packed structs for (auto m = members.rbegin(); m != members.rend(); ++m) { fstHandle member = *m; std::string member_val; int expected_width = 0; - + // Get the declared width of this member if (handle_to_var.find(member) != handle_to_var.end()) { expected_width = handle_to_var[member].width; } - - // Get the current value + // Get the current value of the member if (past_data.find(member) != past_data.end()) { member_val = past_data[member]; - // VCD drops leading zeros - must pad to full width + // Pad with zeros to the expected width of the member if (expected_width > 0 && (int)member_val.length() < expected_width) { member_val = std::string(expected_width - member_val.length(), '0') + member_val; } } else if (expected_width > 0) { - // No value yet, use x's + // No value yet, use X to pad member_val = std::string(expected_width, 'x'); - } else { + } else { // fallback to X member_val = "x"; } - + // Concatenate the member value to the overall struct value result += member_val; } return result; From fa1267e0cbd8be508345cfcbea8d62532249f51a Mon Sep 17 00:00:00 2001 From: Stan Lee Date: Fri, 27 Feb 2026 11:27:37 -0800 Subject: [PATCH 07/44] fix indents --- kernel/fstdata.cc | 71 +++++++++++++++++++++++------------------------ 1 file changed, 34 insertions(+), 37 deletions(-) diff --git a/kernel/fstdata.cc b/kernel/fstdata.cc index a4c7d7e65..6d45a8750 100644 --- a/kernel/fstdata.cc +++ b/kernel/fstdata.cc @@ -123,37 +123,37 @@ void FstData::extractVarNames() while ((h = fstReaderIterateHier(ctx))) { switch (h->htyp) { - case FST_HT_SCOPE: { - fst_scope_name = fstReaderPushScope(ctx, h->u.scope.name, NULL); + case FST_HT_SCOPE: { + fst_scope_name = fstReaderPushScope(ctx, h->u.scope.name, NULL); - // Fork scopes are identified by FST_ST_VCD_FORK - if (h->u.scope.typ == FST_ST_VCD_FORK) { - current_fork_scope = fst_scope_name; - // Create new vector that contains struct members copied during upscope - fork_scopes[current_fork_scope] = std::vector(); + // Fork scopes are identified by FST_ST_VCD_FORK + if (h->u.scope.typ == FST_ST_VCD_FORK) { + current_fork_scope = fst_scope_name; + // Create new vector that contains struct members copied during upscope + fork_scopes[current_fork_scope] = std::vector(); + } + break; } - break; - } - case FST_HT_UPSCOPE: { - if (!current_fork_scope.empty() && current_fork_scope == fst_scope_name) { - // Assign a unique handle to this fork scope - fstHandle fork_handle = next_fork_handle++; + case FST_HT_UPSCOPE: { + if (!current_fork_scope.empty() && current_fork_scope == fst_scope_name) { + // Assign a unique handle to this fork scope + fstHandle fork_handle = next_fork_handle++; - // Map normalized scope name to the handle for future lookups via getHandle() - std::string normalized_fork_scope = current_fork_scope; - normalize_brackets(normalized_fork_scope); - name_to_handle[normalized_fork_scope] = fork_handle; + // Map normalized scope name to the handle for future lookups via getHandle() + std::string normalized_fork_scope = current_fork_scope; + normalize_brackets(normalized_fork_scope); + name_to_handle[normalized_fork_scope] = fork_handle; - // Copy the extracted members of the fork scope to the fork scope members map - // for value lookups in valueOf() - fork_scope_members[fork_handle] = fork_scopes[current_fork_scope]; + // Copy the extracted members of the fork scope to the fork scope members map + // for value lookups in valueOf() + fork_scope_members[fork_handle] = fork_scopes[current_fork_scope]; - // Clear current fork scope for the next scope - current_fork_scope.clear(); + // Clear current fork scope for the next scope + current_fork_scope.clear(); + } + fst_scope_name = fstReaderPopScope(ctx); + break; } - fst_scope_name = fstReaderPopScope(ctx); - break; - } case FST_HT_VAR: { FstVar var; var.id = h->u.var.handle; @@ -163,14 +163,14 @@ void FstData::extractVarNames() var.scope = fst_scope_name; normalize_brackets(var.scope); var.width = h->u.var.length; - vars.push_back(var); - if (!var.is_alias) - handle_to_var[h->u.var.handle] = var; + vars.push_back(var); + if (!var.is_alias) + handle_to_var[h->u.var.handle] = var; - // Add variable to fork scope members if we are currently in a fork scope - if (!current_fork_scope.empty()) { - fork_scopes[current_fork_scope].push_back(h->u.var.handle); - } + // Add variable to fork scope members if we are currently in a fork scope + if (!current_fork_scope.empty()) { + fork_scopes[current_fork_scope].push_back(h->u.var.handle); + } std::string clean_name; bool has_space = false; @@ -348,11 +348,8 @@ std::string FstData::valueOf(fstHandle signal) } // Normal signal handling - if (past_data.find(signal) == past_data.end()) { - if (handle_to_var.find(signal) != handle_to_var.end()) { - return std::string(handle_to_var[signal].width, 'x'); - } - return "x"; + if (handle_to_var.find(signal) != handle_to_var.end()) { + return std::string(handle_to_var[signal].width, 'x'); } return past_data[signal]; } From 03ce300b49d2b516201aff748680fdb45a4b8490 Mon Sep 17 00:00:00 2001 From: Stan Lee Date: Fri, 27 Feb 2026 11:29:31 -0800 Subject: [PATCH 08/44] another indent --- kernel/fstdata.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel/fstdata.cc b/kernel/fstdata.cc index 6d45a8750..395bab66f 100644 --- a/kernel/fstdata.cc +++ b/kernel/fstdata.cc @@ -172,7 +172,7 @@ void FstData::extractVarNames() fork_scopes[current_fork_scope].push_back(h->u.var.handle); } - std::string clean_name; + std::string clean_name; bool has_space = false; for(size_t i=0;iu.var.name);i++) { From d36e2f7d17a8353afd22dd8a32840cbcf9d6d4ae Mon Sep 17 00:00:00 2001 From: Stan Lee Date: Fri, 27 Feb 2026 11:40:13 -0800 Subject: [PATCH 09/44] resolve accidental change --- kernel/fstdata.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel/fstdata.cc b/kernel/fstdata.cc index 395bab66f..fbcca6b82 100644 --- a/kernel/fstdata.cc +++ b/kernel/fstdata.cc @@ -348,7 +348,7 @@ std::string FstData::valueOf(fstHandle signal) } // Normal signal handling - if (handle_to_var.find(signal) != handle_to_var.end()) { + if (past_data.find(signal) == past_data.end()) { return std::string(handle_to_var[signal].width, 'x'); } return past_data[signal]; From c42d2c2d03dd814895fd1be18fe75f8d3b05b04b Mon Sep 17 00:00:00 2001 From: Stan Lee Date: Fri, 27 Feb 2026 11:54:43 -0800 Subject: [PATCH 10/44] support for nested structs --- kernel/fstdata.cc | 80 ++++++++++++++++++++++++++++------------------- 1 file changed, 47 insertions(+), 33 deletions(-) diff --git a/kernel/fstdata.cc b/kernel/fstdata.cc index fbcca6b82..0350be46b 100644 --- a/kernel/fstdata.cc +++ b/kernel/fstdata.cc @@ -114,8 +114,9 @@ void FstData::extractVarNames() struct fstHier *h; std::string fst_scope_name; - // Track current fork scope - std::string current_fork_scope; + // Track nested fork scopes using a stack to handle nested packed structs + // Begins with outmost scope and ends with innermost scope + std::vector fork_scope_stack; // Start fork handles after the maximum real handle from FST file to avoid collisions fstHandle next_fork_handle = fstReaderGetMaxHandle(ctx) + 1; @@ -126,30 +127,35 @@ void FstData::extractVarNames() case FST_HT_SCOPE: { fst_scope_name = fstReaderPushScope(ctx, h->u.scope.name, NULL); - // Fork scopes are identified by FST_ST_VCD_FORK + // Fork scopes are identified by FST_ST_VCD_FORK and are pushed onto the stack if (h->u.scope.typ == FST_ST_VCD_FORK) { - current_fork_scope = fst_scope_name; - // Create new vector that contains struct members copied during upscope - fork_scopes[current_fork_scope] = std::vector(); + fork_scope_stack.push_back(fst_scope_name); + // Create new vector that contains struct members + fork_scopes[fst_scope_name] = std::vector(); } break; } case FST_HT_UPSCOPE: { - if (!current_fork_scope.empty() && current_fork_scope == fst_scope_name) { - // Assign a unique handle to this fork scope + if (!fork_scope_stack.empty() && fork_scope_stack.back() == fst_scope_name) { + // Assign a unique handle to this fork scope and increment for future forks fstHandle fork_handle = next_fork_handle++; // Map normalized scope name to the handle for future lookups via getHandle() - std::string normalized_fork_scope = current_fork_scope; - normalize_brackets(normalized_fork_scope); - name_to_handle[normalized_fork_scope] = fork_handle; + normalize_brackets(fst_scope_name); + name_to_handle[fst_scope_name] = fork_handle; // Copy the extracted members of the fork scope to the fork scope members map // for value lookups in valueOf() - fork_scope_members[fork_handle] = fork_scopes[current_fork_scope]; + fork_scope_members[fork_handle] = fork_scopes[fst_scope_name]; - // Clear current fork scope for the next scope - current_fork_scope.clear(); + // If this is a nested fork scope, add its handle to the parent fork scope + if (fork_scope_stack.size() > 1) { + std::string parent_fork = fork_scope_stack[fork_scope_stack.size() - 2]; + fork_scopes[parent_fork].push_back(fork_handle); + } + + // Pop this fork scope from the stack + fork_scope_stack.pop_back(); } fst_scope_name = fstReaderPopScope(ctx); break; @@ -167,9 +173,9 @@ void FstData::extractVarNames() if (!var.is_alias) handle_to_var[h->u.var.handle] = var; - // Add variable to fork scope members if we are currently in a fork scope - if (!current_fork_scope.empty()) { - fork_scopes[current_fork_scope].push_back(h->u.var.handle); + // Add variable to the innermost fork scope in the fork scope stack + if (!fork_scope_stack.empty()) { + fork_scopes[fork_scope_stack.back()].push_back(h->u.var.handle); } std::string clean_name; @@ -322,24 +328,32 @@ std::string FstData::valueOf(fstHandle signal) for (auto m = members.rbegin(); m != members.rend(); ++m) { fstHandle member = *m; std::string member_val; - int expected_width = 0; + + // Check if this member is itself a nested fork scope (struct) + if (fork_scope_members.find(member) != fork_scope_members.end()) { + // Recursively get the value of the nested struct + member_val = valueOf(member); + } else { + // Regular variable - look up in past_data + int expected_width = 0; - // Get the declared width of this member - if (handle_to_var.find(member) != handle_to_var.end()) { - expected_width = handle_to_var[member].width; - } - // Get the current value of the member - if (past_data.find(member) != past_data.end()) { - member_val = past_data[member]; - // Pad with zeros to the expected width of the member - if (expected_width > 0 && (int)member_val.length() < expected_width) { - member_val = std::string(expected_width - member_val.length(), '0') + member_val; + // Get the declared width of this member + if (handle_to_var.find(member) != handle_to_var.end()) { + expected_width = handle_to_var[member].width; + } + // Get the current value of the member + if (past_data.find(member) != past_data.end()) { + member_val = past_data[member]; + // Pad with zeros to the expected width of the member + if (expected_width > 0 && (int)member_val.length() < expected_width) { + member_val = std::string(expected_width - member_val.length(), '0') + member_val; + } + } else if (expected_width > 0) { + // No value yet, use X to pad + member_val = std::string(expected_width, 'x'); + } else { // fallback to X + member_val = "x"; } - } else if (expected_width > 0) { - // No value yet, use X to pad - member_val = std::string(expected_width, 'x'); - } else { // fallback to X - member_val = "x"; } // Concatenate the member value to the overall struct value result += member_val; From 93af5a5232b8f64b3e2d8d54a046ae64e3ce48fd Mon Sep 17 00:00:00 2001 From: Stan Lee Date: Fri, 27 Feb 2026 12:17:43 -0800 Subject: [PATCH 11/44] in order --- kernel/fstdata.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel/fstdata.cc b/kernel/fstdata.cc index 0350be46b..44f53e8bd 100644 --- a/kernel/fstdata.cc +++ b/kernel/fstdata.cc @@ -325,7 +325,7 @@ std::string FstData::valueOf(fstHandle signal) // Iterate over members of the struct to get concatenated value. // The first declared member is MSB in SystemVerilog packed structs - for (auto m = members.rbegin(); m != members.rend(); ++m) { + for (auto m = members.begin(); m != members.end(); m++) { fstHandle member = *m; std::string member_val; From 8ee71ddc7f98884c6de90d2432a002fa8bdb4a34 Mon Sep 17 00:00:00 2001 From: Stan Lee Date: Fri, 27 Feb 2026 12:19:14 -0800 Subject: [PATCH 12/44] bugfix --- kernel/fstdata.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/kernel/fstdata.cc b/kernel/fstdata.cc index 44f53e8bd..f417048f2 100644 --- a/kernel/fstdata.cc +++ b/kernel/fstdata.cc @@ -131,6 +131,7 @@ void FstData::extractVarNames() if (h->u.scope.typ == FST_ST_VCD_FORK) { fork_scope_stack.push_back(fst_scope_name); // Create new vector that contains struct members + normalize_brackets(fst_scope_name); fork_scopes[fst_scope_name] = std::vector(); } break; From 18c3a0b907471a87f5de0a00dc7a90ad93dcb270 Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Fri, 27 Feb 2026 14:53:44 -0800 Subject: [PATCH 13/44] Remove old linefile loops stuff --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index 81e39cd43..ec93a0bb6 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,6 @@ ENABLE_LIBYOSYS_STATIC := 0 ENABLE_ZLIB := 1 ENABLE_HELP_SOURCE := 0 ENABLE_BACKTRACE := 1 -VERIFIC_LINEFILE_INCLUDES_LOOPS := 1 # python wrappers ENABLE_PYOSYS := 1 From fc4ff6ecd2f891d034db275a2399b4278f4f601e Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Fri, 27 Feb 2026 15:01:06 -0800 Subject: [PATCH 14/44] Add release workflow --- .github/workflows/release.yml | 116 ++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..d733c2d52 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,116 @@ +name: Release (anylinux amd64 wheel) + +on: + push: + branches: + - master + workflow_dispatch: + +permissions: + contents: write + +jobs: + build-and-release: + runs-on: ubuntu-latest + name: Build anylinux amd64 wheel + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: true + + - name: Build wheel in Alpine container + run: | + docker run --rm \ + -v "${{ github.workspace }}:/src" \ + -w /src \ + --platform linux/amd64 \ + alpine:3.20 sh -c ' + set -ex + apk add --no-cache \ + build-base bison flex gperf \ + tcl-dev readline-dev zlib-dev libffi-dev \ + python3 python3-dev py3-pip py3-setuptools py3-wheel \ + git pkgconf ccache + + git config --global --add safe.directory /src + git submodule foreach --recursive git config --global --add safe.directory \$toplevel/\$sm_path + + # Build Verific TCL main (needed before Yosys) + cd /src/verific/tclmain + make + cd /src + + # Build the wheel via setup.py + pip install --break-system-packages pybind11 cxxheaderparser + _PYOSYS_OVERRIDE_VER=$( + grep "^YOSYS_VER " Makefile | head -1 | sed "s/.*:= *//" | tr "+" "." + ) python3 setup.py bdist_wheel --dist-dir /src/dist + + # Also build the yosys binary + install tarball + make -j$(nproc) SMALL=1 ENABLE_PLUGINS=0 PREFIX=/usr/local install + make DESTDIR=/tmp/install PREFIX=/usr/local install + cd /tmp/install + tar czf /src/yosys-anylinux-amd64.tar.gz . + ' + + - name: Generate release notes + id: meta + run: | + SHORT_SHA=$(git rev-parse --short HEAD) + FULL_SHA=$(git rev-parse HEAD) + DATE=$(date -u +%Y-%m-%d) + echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT" + echo "date=$DATE" >> "$GITHUB_OUTPUT" + REPO_URL="${{ github.server_url }}/${{ github.repository }}" + WHEEL=$(ls dist/*.whl | head -1) + WHEEL_NAME=$(basename "$WHEEL") + echo "wheel_name=$WHEEL_NAME" >> "$GITHUB_OUTPUT" + printf '%s\n' \ + "Automated build from \`master\` @ [\`${SHORT_SHA}\`](${REPO_URL}/commit/${FULL_SHA})" \ + "" \ + "**Platform:** Linux amd64 (Alpine-based, portable across Linux distros)" \ + "**Built:** ${DATE}" \ + "" \ + "### Assets" \ + "- \`yosys-anylinux-amd64.tar.gz\` — standalone tarball" \ + "- \`${WHEEL_NAME}\` — Python wheel (pyosys)" \ + "" \ + "### Installation (tarball)" \ + "\`\`\`bash" \ + "sudo tar xzf yosys-anylinux-amd64.tar.gz -C /" \ + "\`\`\`" \ + "" \ + "### Installation (wheel)" \ + "\`\`\`bash" \ + "pip install ${WHEEL_NAME}" \ + "\`\`\`" \ + > release_notes.md + + - name: Create permanent release + run: | + TAG="build-${{ steps.meta.outputs.date }}-${{ steps.meta.outputs.short_sha }}" + gh release create "$TAG" \ + yosys-anylinux-amd64.tar.gz \ + dist/*.whl \ + --target "${{ github.sha }}" \ + --title "Build ${{ steps.meta.outputs.date }} (${{ steps.meta.outputs.short_sha }})" \ + --notes-file release_notes.md + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Update latest release + run: | + git tag -f latest HEAD + git push -f origin latest + gh release delete latest --yes 2>/dev/null || true + gh release create latest \ + yosys-anylinux-amd64.tar.gz \ + dist/*.whl \ + --target "${{ github.sha }}" \ + --title "Latest Build (${{ steps.meta.outputs.date }})" \ + --notes-file release_notes.md \ + --prerelease + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 2c1d16093013768dff6032dc2b8a016006ac169d Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Sat, 28 Feb 2026 12:03:52 -0800 Subject: [PATCH 15/44] fix: trigger release workflow on main branch, not master Made-with: Cursor --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d733c2d52..291738c40 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: Release (anylinux amd64 wheel) on: push: branches: - - master + - main workflow_dispatch: permissions: @@ -68,7 +68,7 @@ jobs: WHEEL_NAME=$(basename "$WHEEL") echo "wheel_name=$WHEEL_NAME" >> "$GITHUB_OUTPUT" printf '%s\n' \ - "Automated build from \`master\` @ [\`${SHORT_SHA}\`](${REPO_URL}/commit/${FULL_SHA})" \ + "Automated build from \`main\` @ [\`${SHORT_SHA}\`](${REPO_URL}/commit/${FULL_SHA})" \ "" \ "**Platform:** Linux amd64 (Alpine-based, portable across Linux distros)" \ "**Built:** ${DATE}" \ From 44beeb5213bd5bc4d5b6acd754568a99e6293ae7 Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Sat, 28 Feb 2026 12:05:26 -0800 Subject: [PATCH 16/44] fix: use SSH deploy key for private verific submodule checkout Made-with: Cursor --- .github/workflows/release.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 291738c40..5cc7f0933 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,12 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - submodules: true + submodules: false + ssh-key: ${{ secrets.VERIFIC_DEPLOY_KEY }} + + - name: Initialize submodules + run: | + git submodule update --init --recursive - name: Build wheel in Alpine container run: | From 708637f65a2cc831dfe7bc8d600860c44e19efe9 Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Sat, 28 Feb 2026 12:07:03 -0800 Subject: [PATCH 17/44] fix: use PAT for private submodule access (abc, verific) Deploy keys are repo-scoped and can't access multiple private repos. Use a PAT (SUBMODULE_PAT) that has access to all required repos. Made-with: Cursor --- .github/workflows/release.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5cc7f0933..23b32ab0a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,11 +19,12 @@ jobs: with: fetch-depth: 0 submodules: false - ssh-key: ${{ secrets.VERIFIC_DEPLOY_KEY }} + token: ${{ secrets.SUBMODULE_PAT }} - name: Initialize submodules run: | - git submodule update --init --recursive + git -c url."https://x-access-token:${{ secrets.SUBMODULE_PAT }}@github.com/".insteadOf="git@github.com:" \ + submodule update --init --recursive - name: Build wheel in Alpine container run: | From 0b0c19b271c898f089e5ee21d17ab16113a343a0 Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Sat, 28 Feb 2026 12:09:28 -0800 Subject: [PATCH 18/44] fix: use SSH_PRIVATE_KEY secret for private submodule access Use the same SSH key approach as source-vendor.yml for cloning private submodules (abc, verific). Made-with: Cursor --- .github/workflows/release.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 23b32ab0a..c114efc01 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,13 +18,8 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - submodules: false - token: ${{ secrets.SUBMODULE_PAT }} - - - name: Initialize submodules - run: | - git -c url."https://x-access-token:${{ secrets.SUBMODULE_PAT }}@github.com/".insteadOf="git@github.com:" \ - submodule update --init --recursive + submodules: recursive + ssh-key: ${{ secrets.SSH_PRIVATE_KEY }} - name: Build wheel in Alpine container run: | From e7e15b612094a9fe977a89cc19b13cea72a3bf20 Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Sat, 28 Feb 2026 12:20:32 -0800 Subject: [PATCH 19/44] fix: add Alpine/musl shims for libtcl and libnsl Verific tclmain links -ltcl and -lnsl. Alpine tcl-dev provides libtcl8.6.so (no libtcl.so symlink), and musl has no libnsl. Create symlink and stub shared lib to satisfy the linker. Made-with: Cursor --- .github/workflows/release.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c114efc01..a86b2d713 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,6 +38,10 @@ jobs: git config --global --add safe.directory /src git submodule foreach --recursive git config --global --add safe.directory \$toplevel/\$sm_path + # Alpine/musl compatibility shims for Verific tclmain link step + ln -sf /usr/lib/libtcl8.6.so /usr/lib/libtcl.so + echo "void dummy_nsl(void){}" | gcc -shared -o /usr/lib/libnsl.so -x c - + # Build Verific TCL main (needed before Yosys) cd /src/verific/tclmain make From 402d6b0566fe31095c4a36d1f74284dd5102628d Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Sat, 28 Feb 2026 12:38:09 -0800 Subject: [PATCH 20/44] fix: add libdwarf-dev and elfutils-dev for backward-cpp headers Made-with: Cursor --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a86b2d713..48ead96dd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,6 +32,7 @@ jobs: apk add --no-cache \ build-base bison flex gperf \ tcl-dev readline-dev zlib-dev libffi-dev \ + libdwarf-dev elfutils-dev \ python3 python3-dev py3-pip py3-setuptools py3-wheel \ git pkgconf ccache From fe4a997549e3e15ad7069090a2a28ffcbb547754 Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Sat, 28 Feb 2026 13:14:04 -0800 Subject: [PATCH 21/44] fix: add flex-dev for FlexLexer.h header Made-with: Cursor --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 48ead96dd..2e88a9a19 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,7 @@ jobs: alpine:3.20 sh -c ' set -ex apk add --no-cache \ - build-base bison flex gperf \ + build-base bison flex flex-dev gperf \ tcl-dev readline-dev zlib-dev libffi-dev \ libdwarf-dev elfutils-dev \ python3 python3-dev py3-pip py3-setuptools py3-wheel \ From 944d0b370a319b00127f717738befee2a7520313 Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Sat, 28 Feb 2026 13:49:53 -0800 Subject: [PATCH 22/44] fix: clean between wheel and tarball builds to avoid TCL mismatch The wheel build uses ENABLE_TCL=0, but the standalone yosys binary needs ENABLE_TCL=1. Without a clean, stale .o files cause undefined reference errors for TCL symbols. Made-with: Cursor --- .github/workflows/release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2e88a9a19..026465e54 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -55,7 +55,8 @@ jobs: ) python3 setup.py bdist_wheel --dist-dir /src/dist # Also build the yosys binary + install tarball - make -j$(nproc) SMALL=1 ENABLE_PLUGINS=0 PREFIX=/usr/local install + make clean + make -j$(nproc) SMALL=1 ENABLE_PLUGINS=0 ENABLE_TCL=1 ENABLE_READLINE=1 PREFIX=/usr/local make DESTDIR=/tmp/install PREFIX=/usr/local install cd /tmp/install tar czf /src/yosys-anylinux-amd64.tar.gz . From df261f46e3b3aad5aca5ec954a5514092e667d65 Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Sat, 28 Feb 2026 15:26:53 -0800 Subject: [PATCH 23/44] feat: bundle shared library deps and set RPATH in release tarball Copies all non-system shared library dependencies into lib/, then uses patchelf to set RPATH to $ORIGIN/../lib for bin/ executables and $ORIGIN for lib/ libraries. Made-with: Cursor --- .github/workflows/release.yml | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 026465e54..3e2ef8282 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,7 +34,7 @@ jobs: tcl-dev readline-dev zlib-dev libffi-dev \ libdwarf-dev elfutils-dev \ python3 python3-dev py3-pip py3-setuptools py3-wheel \ - git pkgconf ccache + git pkgconf ccache patchelf git config --global --add safe.directory /src git submodule foreach --recursive git config --global --add safe.directory \$toplevel/\$sm_path @@ -58,6 +58,33 @@ jobs: make clean make -j$(nproc) SMALL=1 ENABLE_PLUGINS=0 ENABLE_TCL=1 ENABLE_READLINE=1 PREFIX=/usr/local make DESTDIR=/tmp/install PREFIX=/usr/local install + + # Bundle shared library dependencies and set RPATHs + STAGE=/tmp/install/usr/local + mkdir -p "$STAGE/lib" + + copy_deps() { + for f in "$@"; do + [ -f "$f" ] || continue + ldd "$f" 2>/dev/null | awk "/=>/ {print \$3}" | while read -r lib; do + [ -f "$lib" ] || continue + base=$(basename "$lib") + case "$base" in ld-musl-*|libc.musl-*) continue ;; esac + [ -f "$STAGE/lib/$base" ] || cp "$lib" "$STAGE/lib/$base" + done + done + } + + copy_deps "$STAGE"/bin/* + copy_deps "$STAGE"/lib/*.so* + + for f in "$STAGE"/bin/*; do + [ -f "$f" ] && patchelf --set-rpath "\$ORIGIN/../lib" "$f" 2>/dev/null || true + done + for f in "$STAGE"/lib/*.so*; do + [ -f "$f" ] && patchelf --set-rpath "\$ORIGIN" "$f" 2>/dev/null || true + done + cd /tmp/install tar czf /src/yosys-anylinux-amd64.tar.gz . ' From 9e29b7d7610df89de40f2aa76763f8a20e36efbe Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Sat, 28 Feb 2026 16:49:32 -0800 Subject: [PATCH 24/44] Add macOS arm64 build to release workflow Adds a build-macos job on macos-15 that builds Verific tclmain and yosys with SMALL=1, bundles non-system dylibs, and uploads yosys-macos-arm64.tar.gz alongside the existing Linux assets. Made-with: Cursor --- .github/workflows/release.yml | 94 ++++++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3e2ef8282..35d2c7319 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Release (anylinux amd64 wheel) +name: Release (anylinux amd64 + macOS arm64) on: push: @@ -10,9 +10,9 @@ permissions: contents: write jobs: - build-and-release: + build-linux: runs-on: ubuntu-latest - name: Build anylinux amd64 wheel + name: Build anylinux amd64 wheel + tarball steps: - uses: actions/checkout@v4 @@ -104,16 +104,18 @@ jobs: printf '%s\n' \ "Automated build from \`main\` @ [\`${SHORT_SHA}\`](${REPO_URL}/commit/${FULL_SHA})" \ "" \ - "**Platform:** Linux amd64 (Alpine-based, portable across Linux distros)" \ "**Built:** ${DATE}" \ "" \ "### Assets" \ - "- \`yosys-anylinux-amd64.tar.gz\` — standalone tarball" \ - "- \`${WHEEL_NAME}\` — Python wheel (pyosys)" \ + "| File | Platform |" \ + "|------|----------|" \ + "| \`yosys-anylinux-amd64.tar.gz\` | Linux x86-64 (Alpine-based, portable) |" \ + "| \`yosys-macos-arm64.tar.gz\` | macOS Apple Silicon |" \ + "| \`${WHEEL_NAME}\` | Python wheel (pyosys) |" \ "" \ "### Installation (tarball)" \ "\`\`\`bash" \ - "sudo tar xzf yosys-anylinux-amd64.tar.gz -C /" \ + "sudo tar xzf yosys-.tar.gz -C /" \ "\`\`\`" \ "" \ "### Installation (wheel)" \ @@ -148,3 +150,81 @@ jobs: --prerelease env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + build-macos: + runs-on: macos-15 + name: Build macOS arm64 tarball + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + ssh-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Install dependencies + run: | + brew install bison flex gperf tcl-tk@8 readline libffi dwarfutils libelf + + - name: Build Verific tclmain + run: | + export PATH="$(brew --prefix bison)/bin:$(brew --prefix flex)/bin:$PATH" + cd verific/tclmain + make + + - name: Build yosys + run: | + set -ex + export PATH="$(brew --prefix bison)/bin:$(brew --prefix flex)/bin:$PATH" + make -j$(sysctl -n hw.ncpu) SMALL=1 ENABLE_PLUGINS=0 ENABLE_TCL=1 ENABLE_READLINE=1 PREFIX=/usr/local + make DESTDIR=/tmp/install PREFIX=/usr/local install + + - name: Package tarball + run: | + STAGE=/tmp/install/usr/local + mkdir -p "$STAGE/lib" + + copy_deps() { + for f in "$@"; do + [ -f "$f" ] || continue + otool -L "$f" 2>/dev/null | awk 'NR>1 {print $1}' | while read -r lib; do + [ -f "$lib" ] || continue + base=$(basename "$lib") + case "$base" in libSystem*|libc++*|libobjc*) continue ;; esac + case "$lib" in /usr/lib/*|/System/*) continue ;; esac + [ -f "$STAGE/lib/$base" ] || cp "$lib" "$STAGE/lib/$base" + done + done + } + + copy_deps "$STAGE"/bin/* + copy_deps "$STAGE"/lib/*.dylib + + for f in "$STAGE"/bin/*; do + [ -f "$f" ] || continue + install_name_tool -add_rpath "@executable_path/../lib" "$f" 2>/dev/null || true + for lib in "$STAGE"/lib/*.dylib; do + [ -f "$lib" ] || continue + base=$(basename "$lib") + install_name_tool -change "$(otool -L "$f" | grep "$base" | awk '{print $1}')" "@rpath/$base" "$f" 2>/dev/null || true + done + done + for f in "$STAGE"/lib/*.dylib; do + [ -f "$f" ] || continue + install_name_tool -id "@rpath/$(basename "$f")" "$f" 2>/dev/null || true + done + + cd /tmp/install + tar czf "${{ github.workspace }}/yosys-macos-arm64.tar.gz" . + + - name: Upload to permanent release + run: | + SHORT_SHA=$(git rev-parse --short HEAD) + DATE=$(date -u +%Y-%m-%d) + TAG="build-${DATE}-${SHORT_SHA}" + until gh release view "$TAG" >/dev/null 2>&1; do sleep 5; done + gh release upload "$TAG" yosys-macos-arm64.tar.gz --clobber + until gh release view latest >/dev/null 2>&1; do sleep 5; done + gh release upload latest yosys-macos-arm64.tar.gz --clobber + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From e2b343a34f787db799bb0442e49b07ef80e5dd28 Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Sat, 28 Feb 2026 17:27:20 -0800 Subject: [PATCH 25/44] Fix macOS build: install ccache, disable pyosys The macOS runner doesn't have ccache or pybind11 pre-installed. Install ccache via brew and disable ENABLE_PYOSYS since we only need the binary tarball (not the wheel) for macOS. Made-with: Cursor --- .github/workflows/release.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 35d2c7319..fe0a64107 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -164,7 +164,8 @@ jobs: - name: Install dependencies run: | - brew install bison flex gperf tcl-tk@8 readline libffi dwarfutils libelf + brew install bison flex gperf tcl-tk@8 readline libffi dwarfutils libelf ccache + pip3 install pybind11 - name: Build Verific tclmain run: | @@ -176,8 +177,8 @@ jobs: run: | set -ex export PATH="$(brew --prefix bison)/bin:$(brew --prefix flex)/bin:$PATH" - make -j$(sysctl -n hw.ncpu) SMALL=1 ENABLE_PLUGINS=0 ENABLE_TCL=1 ENABLE_READLINE=1 PREFIX=/usr/local - make DESTDIR=/tmp/install PREFIX=/usr/local install + make -j$(sysctl -n hw.ncpu) SMALL=1 ENABLE_PLUGINS=0 ENABLE_TCL=1 ENABLE_READLINE=1 ENABLE_PYOSYS=0 PREFIX=/usr/local + make DESTDIR=/tmp/install PREFIX=/usr/local ENABLE_PYOSYS=0 install - name: Package tarball run: | From 3d5cb87c909118c5c1d5568150a22ac92bb3ba81 Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Sat, 28 Feb 2026 18:02:51 -0800 Subject: [PATCH 26/44] Fix macOS build: remove pip3 install pybind11 pybind11 is not needed since ENABLE_PYOSYS=0, and pip3 fails on the macos-15 runner due to externally-managed-environment. Made-with: Cursor --- .github/workflows/release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fe0a64107..78cc5b999 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -165,7 +165,6 @@ jobs: - name: Install dependencies run: | brew install bison flex gperf tcl-tk@8 readline libffi dwarfutils libelf ccache - pip3 install pybind11 - name: Build Verific tclmain run: | From 1bb440ef15fd42b12a476efb5a8116dd9757f3de Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Sat, 28 Feb 2026 18:14:48 -0800 Subject: [PATCH 27/44] refactor: build wheels only (no tarballs) for linux amd64 and macOS arm64 Remove standalone tarball builds. The release now produces only pyosys Python wheels for both platforms. Made-with: Cursor --- .github/workflows/release.yml | 118 ++++++---------------------------- 1 file changed, 18 insertions(+), 100 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 78cc5b999..5dc87d5f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Release (anylinux amd64 + macOS arm64) +name: Release (pyosys wheels) on: push: @@ -10,9 +10,9 @@ permissions: contents: write jobs: - build-linux: + build-linux-wheel: runs-on: ubuntu-latest - name: Build anylinux amd64 wheel + tarball + name: Build Linux amd64 wheel steps: - uses: actions/checkout@v4 @@ -34,7 +34,7 @@ jobs: tcl-dev readline-dev zlib-dev libffi-dev \ libdwarf-dev elfutils-dev \ python3 python3-dev py3-pip py3-setuptools py3-wheel \ - git pkgconf ccache patchelf + git pkgconf ccache git config --global --add safe.directory /src git submodule foreach --recursive git config --global --add safe.directory \$toplevel/\$sm_path @@ -53,40 +53,6 @@ jobs: _PYOSYS_OVERRIDE_VER=$( grep "^YOSYS_VER " Makefile | head -1 | sed "s/.*:= *//" | tr "+" "." ) python3 setup.py bdist_wheel --dist-dir /src/dist - - # Also build the yosys binary + install tarball - make clean - make -j$(nproc) SMALL=1 ENABLE_PLUGINS=0 ENABLE_TCL=1 ENABLE_READLINE=1 PREFIX=/usr/local - make DESTDIR=/tmp/install PREFIX=/usr/local install - - # Bundle shared library dependencies and set RPATHs - STAGE=/tmp/install/usr/local - mkdir -p "$STAGE/lib" - - copy_deps() { - for f in "$@"; do - [ -f "$f" ] || continue - ldd "$f" 2>/dev/null | awk "/=>/ {print \$3}" | while read -r lib; do - [ -f "$lib" ] || continue - base=$(basename "$lib") - case "$base" in ld-musl-*|libc.musl-*) continue ;; esac - [ -f "$STAGE/lib/$base" ] || cp "$lib" "$STAGE/lib/$base" - done - done - } - - copy_deps "$STAGE"/bin/* - copy_deps "$STAGE"/lib/*.so* - - for f in "$STAGE"/bin/*; do - [ -f "$f" ] && patchelf --set-rpath "\$ORIGIN/../lib" "$f" 2>/dev/null || true - done - for f in "$STAGE"/lib/*.so*; do - [ -f "$f" ] && patchelf --set-rpath "\$ORIGIN" "$f" 2>/dev/null || true - done - - cd /tmp/install - tar czf /src/yosys-anylinux-amd64.tar.gz . ' - name: Generate release notes @@ -98,9 +64,6 @@ jobs: echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT" echo "date=$DATE" >> "$GITHUB_OUTPUT" REPO_URL="${{ github.server_url }}/${{ github.repository }}" - WHEEL=$(ls dist/*.whl | head -1) - WHEEL_NAME=$(basename "$WHEEL") - echo "wheel_name=$WHEEL_NAME" >> "$GITHUB_OUTPUT" printf '%s\n' \ "Automated build from \`main\` @ [\`${SHORT_SHA}\`](${REPO_URL}/commit/${FULL_SHA})" \ "" \ @@ -109,18 +72,12 @@ jobs: "### Assets" \ "| File | Platform |" \ "|------|----------|" \ - "| \`yosys-anylinux-amd64.tar.gz\` | Linux x86-64 (Alpine-based, portable) |" \ - "| \`yosys-macos-arm64.tar.gz\` | macOS Apple Silicon |" \ - "| \`${WHEEL_NAME}\` | Python wheel (pyosys) |" \ + "| \`pyosys-*-linux_x86_64.whl\` | Linux x86-64 |" \ + "| \`pyosys-*-macosx_*_arm64.whl\` | macOS Apple Silicon |" \ "" \ - "### Installation (tarball)" \ + "### Installation" \ "\`\`\`bash" \ - "sudo tar xzf yosys-.tar.gz -C /" \ - "\`\`\`" \ - "" \ - "### Installation (wheel)" \ - "\`\`\`bash" \ - "pip install ${WHEEL_NAME}" \ + "pip install pyosys-*.whl" \ "\`\`\`" \ > release_notes.md @@ -128,7 +85,6 @@ jobs: run: | TAG="build-${{ steps.meta.outputs.date }}-${{ steps.meta.outputs.short_sha }}" gh release create "$TAG" \ - yosys-anylinux-amd64.tar.gz \ dist/*.whl \ --target "${{ github.sha }}" \ --title "Build ${{ steps.meta.outputs.date }} (${{ steps.meta.outputs.short_sha }})" \ @@ -142,7 +98,6 @@ jobs: git push -f origin latest gh release delete latest --yes 2>/dev/null || true gh release create latest \ - yosys-anylinux-amd64.tar.gz \ dist/*.whl \ --target "${{ github.sha }}" \ --title "Latest Build (${{ steps.meta.outputs.date }})" \ @@ -151,9 +106,9 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - build-macos: - runs-on: macos-15 - name: Build macOS arm64 tarball + build-macos-wheel: + runs-on: macos-14 + name: Build macOS arm64 wheel steps: - uses: actions/checkout@v4 @@ -165,6 +120,7 @@ jobs: - name: Install dependencies run: | brew install bison flex gperf tcl-tk@8 readline libffi dwarfutils libelf ccache + pip3 install --break-system-packages pybind11 cxxheaderparser setuptools wheel - name: Build Verific tclmain run: | @@ -172,50 +128,12 @@ jobs: cd verific/tclmain make - - name: Build yosys + - name: Build wheel run: | - set -ex export PATH="$(brew --prefix bison)/bin:$(brew --prefix flex)/bin:$PATH" - make -j$(sysctl -n hw.ncpu) SMALL=1 ENABLE_PLUGINS=0 ENABLE_TCL=1 ENABLE_READLINE=1 ENABLE_PYOSYS=0 PREFIX=/usr/local - make DESTDIR=/tmp/install PREFIX=/usr/local ENABLE_PYOSYS=0 install - - - name: Package tarball - run: | - STAGE=/tmp/install/usr/local - mkdir -p "$STAGE/lib" - - copy_deps() { - for f in "$@"; do - [ -f "$f" ] || continue - otool -L "$f" 2>/dev/null | awk 'NR>1 {print $1}' | while read -r lib; do - [ -f "$lib" ] || continue - base=$(basename "$lib") - case "$base" in libSystem*|libc++*|libobjc*) continue ;; esac - case "$lib" in /usr/lib/*|/System/*) continue ;; esac - [ -f "$STAGE/lib/$base" ] || cp "$lib" "$STAGE/lib/$base" - done - done - } - - copy_deps "$STAGE"/bin/* - copy_deps "$STAGE"/lib/*.dylib - - for f in "$STAGE"/bin/*; do - [ -f "$f" ] || continue - install_name_tool -add_rpath "@executable_path/../lib" "$f" 2>/dev/null || true - for lib in "$STAGE"/lib/*.dylib; do - [ -f "$lib" ] || continue - base=$(basename "$lib") - install_name_tool -change "$(otool -L "$f" | grep "$base" | awk '{print $1}')" "@rpath/$base" "$f" 2>/dev/null || true - done - done - for f in "$STAGE"/lib/*.dylib; do - [ -f "$f" ] || continue - install_name_tool -id "@rpath/$(basename "$f")" "$f" 2>/dev/null || true - done - - cd /tmp/install - tar czf "${{ github.workspace }}/yosys-macos-arm64.tar.gz" . + _PYOSYS_OVERRIDE_VER=$( + grep "^YOSYS_VER " Makefile | head -1 | sed "s/.*:= *//" | tr "+" "." + ) python3 setup.py bdist_wheel --dist-dir dist - name: Upload to permanent release run: | @@ -223,8 +141,8 @@ jobs: DATE=$(date -u +%Y-%m-%d) TAG="build-${DATE}-${SHORT_SHA}" until gh release view "$TAG" >/dev/null 2>&1; do sleep 5; done - gh release upload "$TAG" yosys-macos-arm64.tar.gz --clobber + gh release upload "$TAG" dist/*.whl --clobber until gh release view latest >/dev/null 2>&1; do sleep 5; done - gh release upload latest yosys-macos-arm64.tar.gz --clobber + gh release upload latest dist/*.whl --clobber env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From fe8d30247293e057750465acac5368ddc9459220 Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Sat, 28 Feb 2026 18:53:38 -0800 Subject: [PATCH 28/44] fix: add retries to macOS wheel upload for race condition with Linux job Made-with: Cursor --- .github/workflows/release.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5dc87d5f1..8091b50fc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -141,8 +141,16 @@ jobs: DATE=$(date -u +%Y-%m-%d) TAG="build-${DATE}-${SHORT_SHA}" until gh release view "$TAG" >/dev/null 2>&1; do sleep 5; done - gh release upload "$TAG" dist/*.whl --clobber + for i in 1 2 3 4 5; do + gh release upload "$TAG" dist/*.whl --clobber && break + echo "Upload attempt $i failed, retrying in 15s..." + sleep 15 + done until gh release view latest >/dev/null 2>&1; do sleep 5; done - gh release upload latest dist/*.whl --clobber + for i in 1 2 3 4 5; do + gh release upload latest dist/*.whl --clobber && break + echo "Upload attempt $i failed, retrying in 15s..." + sleep 15 + done env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From b2b1e651f738d5ed5f7e9d8306dc7342169c762d Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Sat, 28 Feb 2026 19:33:39 -0800 Subject: [PATCH 29/44] Fix macOS wheel: use Python 3.13 via setup-python, switch to macos-15 The macos-14 runner ships Python 3.14 by default, producing wheels incompatible with Python 3.13 environments. Pin to 3.13 using actions/setup-python and switch to macos-15 for consistency. Made-with: Cursor --- .github/workflows/release.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8091b50fc..404728a59 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -107,7 +107,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} build-macos-wheel: - runs-on: macos-14 + runs-on: macos-15 name: Build macOS arm64 wheel steps: @@ -117,10 +117,14 @@ jobs: submodules: recursive ssh-key: ${{ secrets.SSH_PRIVATE_KEY }} + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + - name: Install dependencies run: | brew install bison flex gperf tcl-tk@8 readline libffi dwarfutils libelf ccache - pip3 install --break-system-packages pybind11 cxxheaderparser setuptools wheel + pip3 install pybind11 cxxheaderparser setuptools wheel - name: Build Verific tclmain run: | From d62702bd70f8995c3408b736c0e99111cc74d4fa Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Sat, 28 Feb 2026 20:04:59 -0800 Subject: [PATCH 30/44] Fix macOS wheel: set MACOSX_DEPLOYMENT_TARGET=11.0 actions/setup-python sets a deployment target older than 10.15, which makes std::filesystem unavailable. Explicitly set 11.0. Made-with: Cursor --- .github/workflows/release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 404728a59..b1b765525 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -129,12 +129,14 @@ jobs: - name: Build Verific tclmain run: | export PATH="$(brew --prefix bison)/bin:$(brew --prefix flex)/bin:$PATH" + export MACOSX_DEPLOYMENT_TARGET=11.0 cd verific/tclmain make - name: Build wheel run: | export PATH="$(brew --prefix bison)/bin:$(brew --prefix flex)/bin:$PATH" + export MACOSX_DEPLOYMENT_TARGET=11.0 _PYOSYS_OVERRIDE_VER=$( grep "^YOSYS_VER " Makefile | head -1 | sed "s/.*:= *//" | tr "+" "." ) python3 setup.py bdist_wheel --dist-dir dist From 83862bda9927dbe80a4913efac4af24d0a89c1a6 Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Sat, 28 Feb 2026 20:41:07 -0800 Subject: [PATCH 31/44] Fix race condition: wait for Linux wheel in latest before macOS upload The macOS job could upload to the latest release before the Linux job recreated it, causing the macOS wheel to be lost. Now wait for the Linux wheel to appear in latest first. Made-with: Cursor --- .github/workflows/release.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1b765525..bb84f82c2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -152,7 +152,9 @@ jobs: echo "Upload attempt $i failed, retrying in 15s..." sleep 15 done - until gh release view latest >/dev/null 2>&1; do sleep 5; done + # Wait for the Linux job to finish creating the latest release with its wheel + echo "Waiting for Linux wheel to appear in latest release..." + until gh release view latest --json assets --jq '.assets[].name' 2>/dev/null | grep -q linux; do sleep 10; done for i in 1 2 3 4 5; do gh release upload latest dist/*.whl --clobber && break echo "Upload attempt $i failed, retrying in 15s..." From c459a74c13ef4fd723bbaa45914dfaf642bb84dd Mon Sep 17 00:00:00 2001 From: Stan Lee Date: Sun, 1 Mar 2026 15:39:35 -0800 Subject: [PATCH 32/44] autoscoping --- kernel/fstdata.cc | 74 +++++++++++++++++++++++++++++++++++ kernel/fstdata.h | 1 + passes/sat/sim.cc | 18 ++++++++- passes/silimate/reg_rename.cc | 29 +++++++++++--- 4 files changed, 114 insertions(+), 8 deletions(-) diff --git a/kernel/fstdata.cc b/kernel/fstdata.cc index f0f00181c..52f690774 100644 --- a/kernel/fstdata.cc +++ b/kernel/fstdata.cc @@ -280,3 +280,77 @@ std::string FstData::valueOf(fstHandle signal) } return past_data[signal]; } + +// Auto-discover scope from FST by finding the top module +std::string FstData::autoScope(Module *topmod) { + + log("Auto-discovering scope from file...\n"); + std::string top = RTLIL::unescape_id(topmod->name); + + // Option 1 - Instance based scope matching + // Will fail if the DUT instance name != the top module name + log("Trying instance-based scope matching...\n"); + for (const auto& var : vars) { + // Check if this scope ends with our top module + log_debug("Checking scope: %s\n", var.scope.c_str()); + if (var.scope == top || + var.scope.find("." + top) != std::string::npos) { + // Extract the full path up to (and including) the top module + size_t pos = var.scope.find(top); + if (pos != std::string::npos) { + return var.scope.substr(0, pos + top.length()); + } + } + } + + // Option 2 - Post based scope matching + // Matches based on exact port name matching of the top module + log("Trying port-based scope matching...\n"); + + // Map port name to their bit widths (RTL reference point) + dict ports; + for (auto wire : topmod->wires()) { + if (wire->port_input || wire->port_output) { + ports[RTLIL::unescape_id(wire->name)] = wire->width; + } + } + + // For each scope, track which ports were found with matching width + // (VCD reference point) + dict> scope_found_ports; + for (const auto& var : vars) { + + // Strip array '[]' notation from variable name + std::string var_name = var.name; + size_t bracket = var_name.find('['); + if (bracket != std::string::npos) { + var_name = var_name.substr(0, bracket); + } + + // Check if this variable name matches one of our port names + if (ports.count(var_name)) { + // Also check if width matches + if (ports[var_name] == var.width) { + scope_found_ports[var.scope].insert(var_name); + } + } + } + + // Compare RTl and VCD references and find an exact match + for (const auto& entry : scope_found_ports) { + const std::string& scope = entry.first; + const std::set& found = entry.second; + + // Check if all port names exist in this scope + if (found.size() == ports.size()) { + log("Auto-discovered scope: %s (matched all %d ports by name)\n", + scope.c_str(), (int)ports.size()); + return scope; + } + } + + // No match found + log_warning("Could not auto-discover scope for module '%s'...\n", + RTLIL::unescape_id(topmod->name).c_str()); + return ""; +} diff --git a/kernel/fstdata.h b/kernel/fstdata.h index a8ae40301..ba262dcf2 100644 --- a/kernel/fstdata.h +++ b/kernel/fstdata.h @@ -57,6 +57,7 @@ class FstData dict getMemoryHandles(std::string name); double getTimescale() { return timescale; } const char *getTimescaleString() { return timescale_str.c_str(); } + std::string autoScope(Module *topmod); private: void extractVarNames(); diff --git a/passes/sat/sim.cc b/passes/sat/sim.cc index 3530bc7af..6f980ed56 100644 --- a/passes/sat/sim.cc +++ b/passes/sat/sim.cc @@ -1474,8 +1474,22 @@ struct SimWorker : SimShared log_assert(top == nullptr); fst = new FstData(sim_filename); timescale = fst->getTimescaleString(); - if (scope.empty()) - log_error("Scope must be defined for co-simulation.\n"); + if (scope.empty()) { + scope = fst->autoScope(topmod); + if (scope.empty()) { + std::set unique_scopes; + for (const auto& var : fst->getVars()) { + unique_scopes.insert(var.scope); + } + log_warning("Available scopes:\n"); + for (const auto& scope : unique_scopes) { + log_warning(" %s\n", scope.c_str()); + } + log_error("No scope found for module '%s'. Please specify -scope explicitly with above options.\n", + RTLIL::unescape_id(topmod->name).c_str()); + } + } + log("Using scope: \"%s\"\n", scope.c_str()); top = new SimInstance(this, scope, topmod); register_signals(); diff --git a/passes/silimate/reg_rename.cc b/passes/silimate/reg_rename.cc index b4a4871f2..066633780 100644 --- a/passes/silimate/reg_rename.cc +++ b/passes/silimate/reg_rename.cc @@ -188,15 +188,35 @@ struct RegRenamePass : public Pass { } extra_args(args, argidx, design); + // Extract top module + Module *topmod = design->top_module(); + if (!topmod) + log_error("No top module found!\n"); + // Extract pre-optimization register widths from VCD file dict, int> vcd_reg_widths; if (!vcd_filename.empty()) { - if (scope.empty()) { - log_error("No scope provided. Use -scope option.\n"); - } log("Reading VCD file: %s\n", vcd_filename.c_str()); try { FstData fst(vcd_filename); + if (scope.empty()) { + scope = fst.autoScope(topmod); + if (scope.empty()) { + log_warning("No scope found for module '%s'. Please specify -scope explicitly.\n", + RTLIL::unescape_id(topmod->name).c_str()); + std::set unique_scopes; + for (const auto& var : fst.getVars()) { + unique_scopes.insert(var.scope); + } + log_warning("Available scopes:\n"); + for (const auto& scope : unique_scopes) { + log_warning(" %s\n", scope.c_str()); + } + log_error("No scope found for module '%s'. Please specify -scope explicitly with above options.\n", + RTLIL::unescape_id(topmod->name).c_str()); + } + } + log("Using scope: \"%s\"\n", scope.c_str()); for (auto &var : fst.getVars()) { if (var.is_reg) { std::string reg_vcd_scope = var.scope; @@ -223,9 +243,6 @@ struct RegRenamePass : public Pass { } // STEP 2: Build hierarchy and process - Module *topmod = design->top_module(); - if (!topmod) - log_error("No top module found!\n"); log("Building hierarchy from scope: %s\n", scope.c_str()); // Build hierarchy and process register renamings From acc08c96c4d23341bf25f048a38e188f775c47f3 Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Sun, 1 Mar 2026 17:36:32 -0800 Subject: [PATCH 33/44] Add manylinux2014 (CentOS 7, glibc 2.17+) wheel build to release workflow Adds a parallel build-manylinux-wheel job using centos:7 container with devtoolset-11 alongside the existing Alpine/musl wheel build. Uses auditwheel to tag wheel as manylinux2014_x86_64. Made-with: Cursor --- .github/workflows/release.yml | 208 +++++++++++++++++++++++----------- 1 file changed, 144 insertions(+), 64 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bb84f82c2..c61b31f9c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ permissions: jobs: build-linux-wheel: runs-on: ubuntu-latest - name: Build Linux amd64 wheel + name: Build Linux amd64 wheel (musl) steps: - uses: actions/checkout@v4 @@ -55,56 +55,85 @@ jobs: ) python3 setup.py bdist_wheel --dist-dir /src/dist ' - - name: Generate release notes - id: meta - run: | - SHORT_SHA=$(git rev-parse --short HEAD) - FULL_SHA=$(git rev-parse HEAD) - DATE=$(date -u +%Y-%m-%d) - echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT" - echo "date=$DATE" >> "$GITHUB_OUTPUT" - REPO_URL="${{ github.server_url }}/${{ github.repository }}" - printf '%s\n' \ - "Automated build from \`main\` @ [\`${SHORT_SHA}\`](${REPO_URL}/commit/${FULL_SHA})" \ - "" \ - "**Built:** ${DATE}" \ - "" \ - "### Assets" \ - "| File | Platform |" \ - "|------|----------|" \ - "| \`pyosys-*-linux_x86_64.whl\` | Linux x86-64 |" \ - "| \`pyosys-*-macosx_*_arm64.whl\` | macOS Apple Silicon |" \ - "" \ - "### Installation" \ - "\`\`\`bash" \ - "pip install pyosys-*.whl" \ - "\`\`\`" \ - > release_notes.md + - uses: actions/upload-artifact@v4 + with: + name: linux-musl-wheel + path: dist/*.whl - - name: Create permanent release - run: | - TAG="build-${{ steps.meta.outputs.date }}-${{ steps.meta.outputs.short_sha }}" - gh release create "$TAG" \ - dist/*.whl \ - --target "${{ github.sha }}" \ - --title "Build ${{ steps.meta.outputs.date }} (${{ steps.meta.outputs.short_sha }})" \ - --notes-file release_notes.md - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + build-manylinux-wheel: + runs-on: ubuntu-latest + name: Build Linux amd64 wheel (manylinux2014, glibc 2.17+) - - name: Update latest release + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + ssh-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Build wheel in CentOS 7 container run: | - git tag -f latest HEAD - git push -f origin latest - gh release delete latest --yes 2>/dev/null || true - gh release create latest \ - dist/*.whl \ - --target "${{ github.sha }}" \ - --title "Latest Build (${{ steps.meta.outputs.date }})" \ - --notes-file release_notes.md \ - --prerelease - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + docker run --rm \ + -v "${{ github.workspace }}:/src" \ + -w /src \ + --platform linux/amd64 \ + centos:7 bash -c ' + set -ex + + sed -i s/mirror.centos.org/vault.centos.org/g /etc/yum.repos.d/*.repo + sed -i s/^#.*baseurl=http/baseurl=http/g /etc/yum.repos.d/*.repo + sed -i s/^mirrorlist=http/#mirrorlist=http/g /etc/yum.repos.d/*.repo + + yum install -y centos-release-scl epel-release + sed -i s/mirror.centos.org/vault.centos.org/g /etc/yum.repos.d/*.repo + sed -i s/^#.*baseurl=http/baseurl=http/g /etc/yum.repos.d/*.repo + sed -i s/^mirrorlist=http/#mirrorlist=http/g /etc/yum.repos.d/*.repo + + yum install -y devtoolset-11-gcc-c++ make git curl \ + tcl-devel tcl-tclreadline-devel readline-devel zlib-devel libffi-devel \ + flex gperf ccache patchelf + + # Build bison >= 3.x from source + curl -L https://ftp.gnu.org/gnu/bison/bison-3.8.2.tar.gz | tar -xzC /tmp + cd /tmp/bison-3.8.2 && source /opt/rh/devtoolset-11/enable && ./configure && make -j$(nproc) && make install + cd /src + + source /opt/rh/devtoolset-11/enable + + # Install a modern Python (3.13) from the manylinux toolchain + PYBIN=/opt/python/cp313-cp313/bin + if [ ! -d "$PYBIN" ]; then + # Fall back to system Python 3 if manylinux Python not available + yum install -y python3 python3-devel python3-pip python3-setuptools + PYBIN=/usr/bin + fi + export PATH=$PYBIN:$PATH + + git config --global --add safe.directory /src + git submodule foreach --recursive git config --global --add safe.directory \$toplevel/\$sm_path + + # Build Verific TCL main + cd /src/verific/tclmain + make + cd /src + + pip3 install --upgrade pip setuptools wheel pybind11 cxxheaderparser + + _PYOSYS_OVERRIDE_VER=$( + grep "^YOSYS_VER " Makefile | head -1 | sed "s/.*:= *//" | tr "+" "." + ) python3 setup.py bdist_wheel --dist-dir /src/dist-manylinux + + # Tag wheel as manylinux + pip3 install auditwheel || true + for whl in /src/dist-manylinux/*.whl; do + auditwheel repair "$whl" --plat manylinux2014_x86_64 -w /src/dist-manylinux/ 2>/dev/null || true + done + ' + + - uses: actions/upload-artifact@v4 + with: + name: linux-manylinux-wheel + path: dist-manylinux/*.whl build-macos-wheel: runs-on: macos-15 @@ -141,24 +170,75 @@ jobs: grep "^YOSYS_VER " Makefile | head -1 | sed "s/.*:= *//" | tr "+" "." ) python3 setup.py bdist_wheel --dist-dir dist - - name: Upload to permanent release + - uses: actions/upload-artifact@v4 + with: + name: macos-wheel + path: dist/*.whl + + release: + runs-on: ubuntu-latest + needs: [build-linux-wheel, build-manylinux-wheel, build-macos-wheel] + name: Create GitHub releases + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: all-wheels + merge-multiple: true + + - name: Generate release notes + id: meta run: | SHORT_SHA=$(git rev-parse --short HEAD) + FULL_SHA=$(git rev-parse HEAD) DATE=$(date -u +%Y-%m-%d) - TAG="build-${DATE}-${SHORT_SHA}" - until gh release view "$TAG" >/dev/null 2>&1; do sleep 5; done - for i in 1 2 3 4 5; do - gh release upload "$TAG" dist/*.whl --clobber && break - echo "Upload attempt $i failed, retrying in 15s..." - sleep 15 - done - # Wait for the Linux job to finish creating the latest release with its wheel - echo "Waiting for Linux wheel to appear in latest release..." - until gh release view latest --json assets --jq '.assets[].name' 2>/dev/null | grep -q linux; do sleep 10; done - for i in 1 2 3 4 5; do - gh release upload latest dist/*.whl --clobber && break - echo "Upload attempt $i failed, retrying in 15s..." - sleep 15 - done + echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT" + echo "date=$DATE" >> "$GITHUB_OUTPUT" + REPO_URL="${{ github.server_url }}/${{ github.repository }}" + printf '%s\n' \ + "Automated build from \`main\` @ [\`${SHORT_SHA}\`](${REPO_URL}/commit/${FULL_SHA})" \ + "" \ + "**Built:** ${DATE}" \ + "" \ + "### Assets" \ + "| File | Platform |" \ + "|------|----------|" \ + "| \`pyosys-*-linux_x86_64.whl\` | Linux x86-64 (musl) |" \ + "| \`pyosys-*-manylinux2014*.whl\` | Linux x86-64 (manylinux2014, glibc 2.17+) |" \ + "| \`pyosys-*-macosx_*_arm64.whl\` | macOS Apple Silicon |" \ + "" \ + "### Installation" \ + "\`\`\`bash" \ + "pip install pyosys-*.whl" \ + "\`\`\`" \ + > release_notes.md + + - name: Create permanent release + run: | + TAG="build-${{ steps.meta.outputs.date }}-${{ steps.meta.outputs.short_sha }}" + gh release create "$TAG" \ + all-wheels/*.whl \ + --target "${{ github.sha }}" \ + --title "Build ${{ steps.meta.outputs.date }} (${{ steps.meta.outputs.short_sha }})" \ + --notes-file release_notes.md + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Update latest release + run: | + git tag -f latest HEAD + git push -f origin latest + gh release delete latest --yes 2>/dev/null || true + gh release create latest \ + all-wheels/*.whl \ + --target "${{ github.sha }}" \ + --title "Latest Build (${{ steps.meta.outputs.date }})" \ + --notes-file release_notes.md \ + --prerelease env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 965bc9da315bcbf539c08ff0cba5af1c31a874b1 Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Sun, 1 Mar 2026 17:50:29 -0800 Subject: [PATCH 34/44] Fix manylinux2014 build: use quay.io/pypa/manylinux2014_x86_64 image The manylinux2014 image provides Python 3.13 and a GCC toolchain already configured for glibc 2.17 compatibility, avoiding the Python 3.6 syntax issues with bare centos:7. Made-with: Cursor --- .github/workflows/release.yml | 35 ++++++++++------------------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c61b31f9c..097382a4e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -71,47 +71,32 @@ jobs: submodules: recursive ssh-key: ${{ secrets.SSH_PRIVATE_KEY }} - - name: Build wheel in CentOS 7 container + - name: Build wheel in manylinux2014 container run: | docker run --rm \ -v "${{ github.workspace }}:/src" \ -w /src \ --platform linux/amd64 \ - centos:7 bash -c ' + quay.io/pypa/manylinux2014_x86_64 bash -c ' set -ex - sed -i s/mirror.centos.org/vault.centos.org/g /etc/yum.repos.d/*.repo - sed -i s/^#.*baseurl=http/baseurl=http/g /etc/yum.repos.d/*.repo - sed -i s/^mirrorlist=http/#mirrorlist=http/g /etc/yum.repos.d/*.repo - - yum install -y centos-release-scl epel-release - sed -i s/mirror.centos.org/vault.centos.org/g /etc/yum.repos.d/*.repo - sed -i s/^#.*baseurl=http/baseurl=http/g /etc/yum.repos.d/*.repo - sed -i s/^mirrorlist=http/#mirrorlist=http/g /etc/yum.repos.d/*.repo - - yum install -y devtoolset-11-gcc-c++ make git curl \ - tcl-devel tcl-tclreadline-devel readline-devel zlib-devel libffi-devel \ + yum install -y tcl-devel readline-devel zlib-devel libffi-devel \ flex gperf ccache patchelf # Build bison >= 3.x from source curl -L https://ftp.gnu.org/gnu/bison/bison-3.8.2.tar.gz | tar -xzC /tmp - cd /tmp/bison-3.8.2 && source /opt/rh/devtoolset-11/enable && ./configure && make -j$(nproc) && make install + cd /tmp/bison-3.8.2 && ./configure && make -j$(nproc) && make install cd /src - source /opt/rh/devtoolset-11/enable - - # Install a modern Python (3.13) from the manylinux toolchain - PYBIN=/opt/python/cp313-cp313/bin - if [ ! -d "$PYBIN" ]; then - # Fall back to system Python 3 if manylinux Python not available - yum install -y python3 python3-devel python3-pip python3-setuptools - PYBIN=/usr/bin - fi - export PATH=$PYBIN:$PATH + # Use the manylinux2014 Python 3.13 + export PATH=/opt/python/cp313-cp313/bin:$PATH git config --global --add safe.directory /src git submodule foreach --recursive git config --global --add safe.directory \$toplevel/\$sm_path + # musl/manylinux compatibility shim for Verific tclmain link step + echo "void dummy_nsl(void){}" | gcc -shared -o /usr/lib64/libnsl.so -x c - || true + # Build Verific TCL main cd /src/verific/tclmain make @@ -123,7 +108,7 @@ jobs: grep "^YOSYS_VER " Makefile | head -1 | sed "s/.*:= *//" | tr "+" "." ) python3 setup.py bdist_wheel --dist-dir /src/dist-manylinux - # Tag wheel as manylinux + # Tag wheel as manylinux2014 pip3 install auditwheel || true for whl in /src/dist-manylinux/*.whl; do auditwheel repair "$whl" --plat manylinux2014_x86_64 -w /src/dist-manylinux/ 2>/dev/null || true From b19948b03a81f81eaaaecd57fb9fd1b2339984f8 Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Sun, 1 Mar 2026 18:14:55 -0800 Subject: [PATCH 35/44] Fix manylinux2014 build: add elfutils-devel, libdwarf-devel for dwarf.h Made-with: Cursor --- .github/workflows/release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 097382a4e..ffdac3a21 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -81,7 +81,8 @@ jobs: set -ex yum install -y tcl-devel readline-devel zlib-devel libffi-devel \ - flex gperf ccache patchelf + flex gperf ccache patchelf \ + elfutils-devel elfutils-libelf-devel libdwarf-devel # Build bison >= 3.x from source curl -L https://ftp.gnu.org/gnu/bison/bison-3.8.2.tar.gz | tar -xzC /tmp From b03f73653f247747b987fd5c130374ccce7da4cb Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Sun, 1 Mar 2026 21:43:26 -0800 Subject: [PATCH 36/44] Update abc to fix bug --- passes/techmap/abc.cc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/passes/techmap/abc.cc b/passes/techmap/abc.cc index 7a5e849d4..9e8e3f3a2 100644 --- a/passes/techmap/abc.cc +++ b/passes/techmap/abc.cc @@ -188,8 +188,11 @@ struct AbcProcess ~AbcProcess() { if (pid == 0) return; - if (to_child_pipe >= 0) + if (to_child_pipe >= 0) { + static const char quit_cmd[] = "quit\n"; + if (write(to_child_pipe, quit_cmd, sizeof(quit_cmd) - 1)) {} close(to_child_pipe); + } int status; int ret = waitpid(pid, &status, 0); if (ret != pid) { From 7d96a7f73c63c02921f00149ebc1dc37fa38e722 Mon Sep 17 00:00:00 2001 From: Akash Levy Date: Sun, 1 Mar 2026 22:35:06 -0800 Subject: [PATCH 37/44] Update aigmap to go a lot faster using aig template cache and uniquify cache --- kernel/rtlil.cc | 2 +- kernel/rtlil.h | 1 + passes/techmap/aigmap.cc | 52 +++++++++++++++++++++++++++++++--------- 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/kernel/rtlil.cc b/kernel/rtlil.cc index 40a1dd2f8..266aedfe0 100644 --- a/kernel/rtlil.cc +++ b/kernel/rtlil.cc @@ -3151,7 +3151,7 @@ void RTLIL::Module::swap_names(RTLIL::Cell *c1, RTLIL::Cell *c2) RTLIL::IdString RTLIL::Module::uniquify(RTLIL::IdString name) { - int index = 0; + int &index = uniquify_cache_[name]; return uniquify(name, index); } diff --git a/kernel/rtlil.h b/kernel/rtlil.h index ef9a077f3..9dfe0c570 100644 --- a/kernel/rtlil.h +++ b/kernel/rtlil.h @@ -2168,6 +2168,7 @@ public: void swap_names(RTLIL::Wire *w1, RTLIL::Wire *w2); void swap_names(RTLIL::Cell *c1, RTLIL::Cell *c2); + dict uniquify_cache_; RTLIL::IdString uniquify(RTLIL::IdString name); RTLIL::IdString uniquify(RTLIL::IdString name, int &index); diff --git a/passes/techmap/aigmap.cc b/passes/techmap/aigmap.cc index 0932562e4..269df6db9 100644 --- a/passes/techmap/aigmap.cc +++ b/passes/techmap/aigmap.cc @@ -72,16 +72,46 @@ struct AigmapPass : public Pass { dict stat_not_replaced; int orig_num_cells = GetSize(module->cells()); + dict aig_cache; + pool new_sel; for (auto cell : module->selected_cells()) { - Aig aig(cell); + if (cell->type.in(ID($_AND_), ID($_NOT_))) { + not_replaced_count++; + stat_not_replaced[cell->type]++; + if (select_mode) + new_sel.insert(cell->name); + continue; + } - if (cell->type.in(ID($_AND_), ID($_NOT_))) - aig.name.clear(); + if (nand_mode && cell->type == ID($_NAND_)) { + not_replaced_count++; + stat_not_replaced[cell->type]++; + if (select_mode) + new_sel.insert(cell->name); + continue; + } - if (nand_mode && cell->type == ID($_NAND_)) - aig.name.clear(); + if (cell->type[0] != '$') { + not_replaced_count++; + stat_not_replaced[cell->type]++; + if (select_mode) + new_sel.insert(cell->name); + continue; + } + + std::string cache_key = cell->type.str(); + cell->parameters.sort(); + for (auto &p : cell->parameters) + cache_key += stringf(":%s=%s", p.first.c_str(), p.second.as_string().c_str()); + + auto cache_it = aig_cache.find(cache_key); + if (cache_it == aig_cache.end()) { + auto r = aig_cache.insert(std::make_pair(cache_key, Aig(cell))); + cache_it = r.first; + } + const Aig &aig = cache_it->second; if (aig.name.empty()) { not_replaced_count++; @@ -110,8 +140,8 @@ struct AigmapPass : public Pass { if (nand_mode && node.inverter) { bit = module->addWire(NEW_ID2_SUFFIX("bit")); auto gate = module->addNandGate(NEW_ID2_SUFFIX("nand"), A, B, bit); - for (auto attr : cell->attributes) - gate->attributes[attr.first] = attr.second; + for (const auto &attr : cell->attributes) + gate->attributes[attr.first] = attr.second; if (select_mode) new_sel.insert(gate->name); @@ -123,8 +153,8 @@ struct AigmapPass : public Pass { else { bit = module->addWire(NEW_ID2_SUFFIX("bit")); auto gate = module->addAndGate(NEW_ID2_SUFFIX("and"), A, B, bit); - for (auto attr : cell->attributes) - gate->attributes[attr.first] = attr.second; + for (const auto &attr : cell->attributes) + gate->attributes[attr.first] = attr.second; if (select_mode) new_sel.insert(gate->name); } @@ -134,8 +164,8 @@ struct AigmapPass : public Pass { if (node.inverter) { SigBit new_bit = module->addWire(NEW_ID2_SUFFIX("new_bit")); auto gate = module->addNotGate(NEW_ID2_SUFFIX("inv"), bit, new_bit); - for (auto attr : cell->attributes) - gate->attributes[attr.first] = attr.second; + for (const auto &attr : cell->attributes) + gate->attributes[attr.first] = attr.second; bit = new_bit; if (select_mode) new_sel.insert(gate->name); From 6594ff508f5b1aef983242eeb1ce13e86fddd847 Mon Sep 17 00:00:00 2001 From: Stan Lee Date: Mon, 2 Mar 2026 00:42:34 -0800 Subject: [PATCH 38/44] improvement --- kernel/fstdata.cc | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/kernel/fstdata.cc b/kernel/fstdata.cc index 52f690774..377ad1a74 100644 --- a/kernel/fstdata.cc +++ b/kernel/fstdata.cc @@ -308,10 +308,10 @@ std::string FstData::autoScope(Module *topmod) { log("Trying port-based scope matching...\n"); // Map port name to their bit widths (RTL reference point) - dict ports; + dict ports2widths; for (auto wire : topmod->wires()) { if (wire->port_input || wire->port_output) { - ports[RTLIL::unescape_id(wire->name)] = wire->width; + ports2widths[RTLIL::unescape_id(wire->name)] = wire->width; } } @@ -322,15 +322,16 @@ std::string FstData::autoScope(Module *topmod) { // Strip array '[]' notation from variable name std::string var_name = var.name; + log("Checking variable: %s with scope: %s\n", var_name.c_str(), var.scope.c_str()); size_t bracket = var_name.find('['); if (bracket != std::string::npos) { var_name = var_name.substr(0, bracket); } // Check if this variable name matches one of our port names - if (ports.count(var_name)) { + if (ports2widths.count(var_name)) { // Also check if width matches - if (ports[var_name] == var.width) { + if (ports2widths[var_name] == var.width) { scope_found_ports[var.scope].insert(var_name); } } @@ -342,9 +343,9 @@ std::string FstData::autoScope(Module *topmod) { const std::set& found = entry.second; // Check if all port names exist in this scope - if (found.size() == ports.size()) { + if (found.size() == ports2widths.size()) { log("Auto-discovered scope: %s (matched all %d ports by name)\n", - scope.c_str(), (int)ports.size()); + scope.c_str(), (int)ports2widths.size()); return scope; } } From da25b800bc2818aa4a870e1577d1b321b7e604f5 Mon Sep 17 00:00:00 2001 From: Stan Lee Date: Mon, 2 Mar 2026 11:05:44 -0800 Subject: [PATCH 39/44] finalized --- kernel/fstdata.cc | 54 ++++++++++++++++++----------------- passes/sat/sim.cc | 10 +------ passes/silimate/reg_rename.cc | 12 +------- 3 files changed, 30 insertions(+), 46 deletions(-) diff --git a/kernel/fstdata.cc b/kernel/fstdata.cc index 377ad1a74..35e9c139e 100644 --- a/kernel/fstdata.cc +++ b/kernel/fstdata.cc @@ -298,7 +298,9 @@ std::string FstData::autoScope(Module *topmod) { // Extract the full path up to (and including) the top module size_t pos = var.scope.find(top); if (pos != std::string::npos) { - return var.scope.substr(0, pos + top.length()); + std::string scope = var.scope.substr(0, pos + top.length()); + log("Found scope: %s\n", scope.c_str()); + return scope; } } } @@ -307,47 +309,47 @@ std::string FstData::autoScope(Module *topmod) { // Matches based on exact port name matching of the top module log("Trying port-based scope matching...\n"); - // Map port name to their bit widths (RTL reference point) - dict ports2widths; + // Map top moduleport name to their bit widths (RTL reference point) + dict top2widths; for (auto wire : topmod->wires()) { if (wire->port_input || wire->port_output) { - ports2widths[RTLIL::unescape_id(wire->name)] = wire->width; + log("Extracted %d ports from top module\n", GetSize(top2widths)); + top2widths[RTLIL::unescape_id(wire->name)] = wire->width; } } - // For each scope, track which ports were found with matching width - // (VCD reference point) - dict> scope_found_ports; + // For each scope, track the number of matching ports + dict scopes2matches; for (const auto& var : vars) { // Strip array '[]' notation from variable name std::string var_name = var.name; - log("Checking variable: %s with scope: %s\n", var_name.c_str(), var.scope.c_str()); size_t bracket = var_name.find('['); if (bracket != std::string::npos) { var_name = var_name.substr(0, bracket); } - - // Check if this variable name matches one of our port names - if (ports2widths.count(var_name)) { - // Also check if width matches - if (ports2widths[var_name] == var.width) { - scope_found_ports[var.scope].insert(var_name); + + // Check if this variable name matches one of our top module port names and width + if (top2widths.count(var_name) && top2widths[var_name] == var.width) { + scopes2matches[var.scope] += 1; + } + } + + // Find scopes with exact matches + // If there is a tie, return the longest scope + std::string result = ""; + for (const auto& entry : scopes2matches) { + int num_matches = entry.second; + if (num_matches == GetSize(top2widths)) { + std::string scope = entry.first; + if (result.empty() || scope.length() > result.length()) { + result = scope; } } } - - // Compare RTl and VCD references and find an exact match - for (const auto& entry : scope_found_ports) { - const std::string& scope = entry.first; - const std::set& found = entry.second; - - // Check if all port names exist in this scope - if (found.size() == ports2widths.size()) { - log("Auto-discovered scope: %s (matched all %d ports by name)\n", - scope.c_str(), (int)ports2widths.size()); - return scope; - } + if (!result.empty()) { + log("Found scope: %s\n", result.c_str()); + return result; } // No match found diff --git a/passes/sat/sim.cc b/passes/sat/sim.cc index 6f980ed56..ed7ed71ac 100644 --- a/passes/sat/sim.cc +++ b/passes/sat/sim.cc @@ -1477,15 +1477,7 @@ struct SimWorker : SimShared if (scope.empty()) { scope = fst->autoScope(topmod); if (scope.empty()) { - std::set unique_scopes; - for (const auto& var : fst->getVars()) { - unique_scopes.insert(var.scope); - } - log_warning("Available scopes:\n"); - for (const auto& scope : unique_scopes) { - log_warning(" %s\n", scope.c_str()); - } - log_error("No scope found for module '%s'. Please specify -scope explicitly with above options.\n", + log_error("No scope found for module '%s'. Please specify -scope explicitly.\n", RTLIL::unescape_id(topmod->name).c_str()); } } diff --git a/passes/silimate/reg_rename.cc b/passes/silimate/reg_rename.cc index 066633780..5635b6869 100644 --- a/passes/silimate/reg_rename.cc +++ b/passes/silimate/reg_rename.cc @@ -202,17 +202,7 @@ struct RegRenamePass : public Pass { if (scope.empty()) { scope = fst.autoScope(topmod); if (scope.empty()) { - log_warning("No scope found for module '%s'. Please specify -scope explicitly.\n", - RTLIL::unescape_id(topmod->name).c_str()); - std::set unique_scopes; - for (const auto& var : fst.getVars()) { - unique_scopes.insert(var.scope); - } - log_warning("Available scopes:\n"); - for (const auto& scope : unique_scopes) { - log_warning(" %s\n", scope.c_str()); - } - log_error("No scope found for module '%s'. Please specify -scope explicitly with above options.\n", + log_error("No scope found for module '%s'. Please specify -scope explicitly.\n", RTLIL::unescape_id(topmod->name).c_str()); } } From 83e05a6509c98e55c112726afcbe7204fb2f9259 Mon Sep 17 00:00:00 2001 From: Stan Lee Date: Mon, 2 Mar 2026 12:07:59 -0800 Subject: [PATCH 40/44] fixes --- kernel/fstdata.cc | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/kernel/fstdata.cc b/kernel/fstdata.cc index 35e9c139e..d34138e28 100644 --- a/kernel/fstdata.cc +++ b/kernel/fstdata.cc @@ -299,7 +299,6 @@ std::string FstData::autoScope(Module *topmod) { size_t pos = var.scope.find(top); if (pos != std::string::npos) { std::string scope = var.scope.substr(0, pos + top.length()); - log("Found scope: %s\n", scope.c_str()); return scope; } } @@ -313,10 +312,10 @@ std::string FstData::autoScope(Module *topmod) { dict top2widths; for (auto wire : topmod->wires()) { if (wire->port_input || wire->port_output) { - log("Extracted %d ports from top module\n", GetSize(top2widths)); top2widths[RTLIL::unescape_id(wire->name)] = wire->width; } } + log("Extracted %d ports from top module\n", GetSize(top2widths)); // For each scope, track the number of matching ports dict scopes2matches; @@ -348,12 +347,16 @@ std::string FstData::autoScope(Module *topmod) { } } if (!result.empty()) { - log("Found scope: %s\n", result.c_str()); return result; } // No match found log_warning("Could not auto-discover scope for module '%s'...\n", RTLIL::unescape_id(topmod->name).c_str()); + log("Available scopes:\n"); + for (const auto& entry : scopes2matches) { + std::string scope = entry.first; + log(" %s\n", scope.c_str()); + } return ""; } From a449e6ab3846a55127d8d39b1a544925c23fbe7d Mon Sep 17 00:00:00 2001 From: Stan Lee Date: Mon, 2 Mar 2026 12:19:09 -0800 Subject: [PATCH 41/44] always dump available scopes --- kernel/fstdata.cc | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/kernel/fstdata.cc b/kernel/fstdata.cc index d34138e28..6d098dd0f 100644 --- a/kernel/fstdata.cc +++ b/kernel/fstdata.cc @@ -287,6 +287,15 @@ std::string FstData::autoScope(Module *topmod) { log("Auto-discovering scope from file...\n"); std::string top = RTLIL::unescape_id(topmod->name); + log("Available scopes:\n"); + std::set unique_scopes; + for (const auto& var : vars) { + unique_scopes.insert(var.scope); + } + for (const auto& scope : unique_scopes) { + log(" %s\n", scope.c_str()); + } + // Option 1 - Instance based scope matching // Will fail if the DUT instance name != the top module name log("Trying instance-based scope matching...\n"); @@ -353,10 +362,5 @@ std::string FstData::autoScope(Module *topmod) { // No match found log_warning("Could not auto-discover scope for module '%s'...\n", RTLIL::unescape_id(topmod->name).c_str()); - log("Available scopes:\n"); - for (const auto& entry : scopes2matches) { - std::string scope = entry.first; - log(" %s\n", scope.c_str()); - } return ""; } From a1470e14fe2835d6ee849e0b2bd4b5b4d0d80160 Mon Sep 17 00:00:00 2001 From: Stan Lee Date: Mon, 2 Mar 2026 12:57:37 -0800 Subject: [PATCH 42/44] typos --- kernel/fstdata.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kernel/fstdata.cc b/kernel/fstdata.cc index 6d098dd0f..b868890dc 100644 --- a/kernel/fstdata.cc +++ b/kernel/fstdata.cc @@ -313,11 +313,11 @@ std::string FstData::autoScope(Module *topmod) { } } - // Option 2 - Post based scope matching + // Option 2 - Port based scope matching // Matches based on exact port name matching of the top module log("Trying port-based scope matching...\n"); - // Map top moduleport name to their bit widths (RTL reference point) + // Map top module port name to their bit widths (RTL reference point) dict top2widths; for (auto wire : topmod->wires()) { if (wire->port_input || wire->port_output) { From b438fd1fe99487bc7b26b6c574193f5e02fef637 Mon Sep 17 00:00:00 2001 From: tondapusili Date: Mon, 2 Mar 2026 19:33:25 -0800 Subject: [PATCH 43/44] negopt: fix quadratic blowup by adding index hints and deferring nusers to filter --- passes/silimate/negopt.cc | 21 ++++++-- passes/silimate/peepopt_manual2sub.pmg | 72 +++++++++++++++----------- passes/silimate/peepopt_muxneg.pmg | 4 +- passes/silimate/peepopt_neg2sub.pmg | 2 +- passes/silimate/peepopt_negexpand.pmg | 2 +- passes/silimate/peepopt_negmux.pmg | 6 +-- passes/silimate/peepopt_negneg.pmg | 2 +- passes/silimate/peepopt_negrebuild.pmg | 8 +-- 8 files changed, 72 insertions(+), 45 deletions(-) diff --git a/passes/silimate/negopt.cc b/passes/silimate/negopt.cc index e3066b9b7..28d047ed9 100644 --- a/passes/silimate/negopt.cc +++ b/passes/silimate/negopt.cc @@ -85,16 +85,29 @@ struct NegoptPass : public Pass { for (auto module : design->selected_modules()) { if (run_pre) { + // manual2sub and sub2neg only need to run once: no downstream + // pre-subpass creates the patterns they match + // separate pm instances so sub2neg sees the $sub cells manual2sub creates. + { + peepopt_pm pm(module); + pm.setup(module->selected_cells()); + pm.run_manual2sub(); + log_flush(); + } + { + peepopt_pm pm(module); + pm.setup(module->selected_cells()); + pm.run_sub2neg(); + log_flush(); + } + + // negexpand/negneg/negmux can feed each other. did_something = true; for (int iter = 0; iter < max_iterations && did_something; iter++) { did_something = false; peepopt_pm pm(module); pm.setup(module->selected_cells()); - pm.run_manual2sub(); // Reduce manual 2's complement to subtraction first - log_flush(); - pm.run_sub2neg(); - log_flush(); pm.run_negexpand(); log_flush(); pm.run_negneg(); diff --git a/passes/silimate/peepopt_manual2sub.pmg b/passes/silimate/peepopt_manual2sub.pmg index d82e5cf5f..b45a05308 100644 --- a/passes/silimate/peepopt_manual2sub.pmg +++ b/passes/silimate/peepopt_manual2sub.pmg @@ -12,9 +12,8 @@ pattern manual2sub state minuend subtrahend result_sig root_a root_b state is_signed -state inner_y +state inner_y inner_port_sig_A root_port_sig inner_port_sig_B -// 1. Match the "root" add (the one that produces the final result) match root_add select root_add->type == $add set root_a port(root_add, \A) @@ -23,8 +22,7 @@ match root_add set is_signed root_add->getParam(ID::A_SIGNED).as_bool() endmatch -// 2. Case A: (a + ~b) + 1 -// Check if root_add has a constant 1 +// Case A: (a + ~b) + 1 code root_add inner_y { SigSpec pa = root_a; @@ -51,20 +49,28 @@ code root_add inner_y } endcode -// Find the inner add -// inner_y is discovered in code so gating happens in the code block +// Find the inner add whose Y feeds the non-constant port of root_add match inner_add_A select inner_add_A->type == $add + index port(inner_add_A, \Y) === inner_y + filter nusers(port(inner_add_A, \Y)) == 2 endmatch -// Find the NOT gate on one of the ports of inner_add_A +// Branch over both ports of inner_add_A to find the NOT gate +code inner_port_sig_A + inner_port_sig_A = port(inner_add_A, \A); + branch; + inner_port_sig_A = port(inner_add_A, \B); +endcode + match not_gate_A select not_gate_A->type == $not + index port(not_gate_A, \Y) === inner_port_sig_A endmatch code root_add inner_add_A not_gate_A subtrahend minuend result_sig is_signed { - // Require consistent signedness on the root add. + // Require consistent signedness across the chain if (root_add->getParam(ID::B_SIGNED).as_bool() != is_signed) reject; @@ -73,11 +79,7 @@ code root_add inner_add_A not_gate_A subtrahend minuend result_sig is_signed if (inner_add_A->getParam(ID::B_SIGNED).as_bool() != is_signed) reject; - if (port(inner_add_A, \Y) != inner_y) - reject; - if (nusers(port(inner_add_A, \Y)) != 2) - reject; - + // Determine which port is the NOT output and which is the minuend SigSpec not_y = port(not_gate_A, \Y); SigSpec add_a = port(inner_add_A, \A); SigSpec add_b = port(inner_add_A, \B); @@ -92,7 +94,6 @@ code root_add inner_add_A not_gate_A subtrahend minuend result_sig is_signed subtrahend = port(not_gate_A, \A); - // Create the subtraction cell log("manual2sub in %s: Found (a + ~b) + 1 pattern, creating $sub for %s\n", log_id(module), log_signal(result_sig)); Cell *cell = root_add; int width = GetSize(result_sig); @@ -100,27 +101,25 @@ code root_add inner_add_A not_gate_A subtrahend minuend result_sig is_signed // Reject if the +1 wrap boundary is narrower than the final result if (inner_width < width) reject; + // Anchor both operands to the inner add width to preserve carry behavior SigSpec minuend_rs = minuend; SigSpec subtrahend_rs = subtrahend; - // Anchor both operands to the inner add width to preserve carry behavior minuend_rs.extend_u0(inner_width, is_signed); subtrahend_rs.extend_u0(inner_width, is_signed); SigSpec sub_y = result_sig; - // Extend the sub result back to the root width when needed if (inner_width != width) { sub_y = module->addWire(NEW_ID2_SUFFIX("sub_y"), inner_width); } Cell *sub = module->addSub(NEW_ID2_SUFFIX("sub"), minuend_rs, subtrahend_rs, sub_y, is_signed); + // Extend the sub result back to root width when inner is wider if (inner_width != width) { SigSpec sub_y_rs = sub_y; sub_y_rs.extend_u0(width, is_signed); module->connect(result_sig, sub_y_rs); } - // Let fixup_parameters handle width adjustments sub->fixup_parameters(); - // Remove old cells autoremove(root_add); autoremove(inner_add_A); autoremove(not_gate_A); @@ -130,23 +129,36 @@ code root_add inner_add_A not_gate_A subtrahend minuend result_sig is_signed } endcode -// 3. Case B: a + (~b + 1) +// Case B: a + (~b + 1) + +// Branch over both ports of root_add to find the inner (~b + 1) add +code root_port_sig + root_port_sig = port(root_add, \A); + branch; + root_port_sig = port(root_add, \B); +endcode -// Find the inner add on either port of root_add match inner_add_B select inner_add_B->type == $add - filter port(inner_add_B, \Y) == root_a || port(inner_add_B, \Y) == root_b - select nusers(port(inner_add_B, \Y)) == 2 + index port(inner_add_B, \Y) === root_port_sig + filter nusers(port(inner_add_B, \Y)) == 2 endmatch -// Check if inner_add_B has a constant 1 and a NOT gate +// Branch over both ports of inner_add_B to find the NOT gate +code inner_port_sig_B + inner_port_sig_B = port(inner_add_B, \A); + branch; + inner_port_sig_B = port(inner_add_B, \B); +endcode + match not_gate_B select not_gate_B->type == $not + index port(not_gate_B, \Y) === inner_port_sig_B endmatch code root_add inner_add_B not_gate_B minuend subtrahend result_sig is_signed { - // Require consistent signedness on the root add. + // Require consistent signedness across the chain if (root_add->getParam(ID::B_SIGNED).as_bool() != is_signed) reject; @@ -155,6 +167,8 @@ code root_add inner_add_B not_gate_B minuend subtrahend result_sig is_signed if (inner_add_B->getParam(ID::B_SIGNED).as_bool() != is_signed) reject; + // Verify inner_add_B has the form (~b + 1): one port is constant 1, + // the other is the NOT output SigSpec pa = inner_add_B->getPort(ID::A); SigSpec pb = inner_add_B->getPort(ID::B); SigSpec not_y = port(not_gate_B, \Y); @@ -175,13 +189,13 @@ code root_add inner_add_B not_gate_B minuend subtrahend result_sig is_signed if (!valid) reject; + // The minuend is whichever root_add port is NOT the inner_add_B output subtrahend = port(not_gate_B, \A); if (inner_add_B->getPort(ID::Y) == root_add->getPort(ID::A)) minuend = root_b; else minuend = root_a; - // Create the subtraction cell log("manual2sub in %s: Found a + (~b + 1) pattern, creating $sub for %s\n", log_id(module), log_signal(result_sig)); Cell *cell = root_add; int width = GetSize(result_sig); @@ -190,32 +204,30 @@ code root_add inner_add_B not_gate_B minuend subtrahend result_sig is_signed // Reject if the +1 wrap boundary is narrower than the final result if (inner_width < width) reject; - + + // Anchor both operands to the inner add width to preserve carry behavior SigSpec minuend_rs = minuend; SigSpec subtrahend_rs = subtrahend; - // Anchor both operands to the inner add width to preserve carry behavior minuend_rs.extend_u0(inner_width, is_signed); subtrahend_rs.extend_u0(inner_width, is_signed); SigSpec sub_y = result_sig; - // Extend the sub result back to the root width when needed if (inner_width != width) { sub_y = module->addWire(NEW_ID2_SUFFIX("sub_y"), inner_width); } Cell *sub = module->addSub(NEW_ID2_SUFFIX("sub"), minuend_rs, subtrahend_rs, sub_y, is_signed); + // Extend the sub result back to root width when inner is wider if (inner_width != width) { SigSpec sub_y_rs = sub_y; sub_y_rs.extend_u0(width, is_signed); module->connect(result_sig, sub_y_rs); } - // Let fixup_parameters handle width adjustments sub->fixup_parameters(); - // Remove old cells autoremove(root_add); autoremove(inner_add_B); autoremove(not_gate_B); diff --git a/passes/silimate/peepopt_muxneg.pmg b/passes/silimate/peepopt_muxneg.pmg index 7a8c33eb5..e81fc7971 100644 --- a/passes/silimate/peepopt_muxneg.pmg +++ b/passes/silimate/peepopt_muxneg.pmg @@ -20,7 +20,7 @@ endmatch match neg_a select neg_a->type == $neg - select nusers(port(neg_a, \Y)) == 2 + filter nusers(port(neg_a, \Y)) == 2 index port(neg_a, \Y) === mux_a set neg_a_in port(neg_a, \A) set neg_a_y port(neg_a, \Y) @@ -29,7 +29,7 @@ endmatch match neg_b select neg_b->type == $neg - select nusers(port(neg_b, \Y)) == 2 + filter nusers(port(neg_b, \Y)) == 2 index port(neg_b, \Y) === mux_b set neg_b_in port(neg_b, \A) set neg_b_y port(neg_b, \Y) diff --git a/passes/silimate/peepopt_neg2sub.pmg b/passes/silimate/peepopt_neg2sub.pmg index 6a958e431..9594e3b47 100644 --- a/passes/silimate/peepopt_neg2sub.pmg +++ b/passes/silimate/peepopt_neg2sub.pmg @@ -28,7 +28,7 @@ endcode match neg select neg->type == $neg - select nusers(port(neg, \Y)) == 2 + filter nusers(port(neg, \Y)) == 2 index port(neg, \Y) === (neg_on_a ? add_a : add_b) set neg_a port(neg, \A) set neg_y port(neg, \Y) diff --git a/passes/silimate/peepopt_negexpand.pmg b/passes/silimate/peepopt_negexpand.pmg index a764bcb34..7ddc0b950 100644 --- a/passes/silimate/peepopt_negexpand.pmg +++ b/passes/silimate/peepopt_negexpand.pmg @@ -20,7 +20,7 @@ endmatch match add select add->type == $add index port(add, \Y) === neg_a - select nusers(port(add, \Y)) == 2 + filter nusers(port(add, \Y)) == 2 set add_a port(add, \A) set add_b port(add, \B) endmatch diff --git a/passes/silimate/peepopt_negmux.pmg b/passes/silimate/peepopt_negmux.pmg index 579732484..d1918e3bf 100644 --- a/passes/silimate/peepopt_negmux.pmg +++ b/passes/silimate/peepopt_negmux.pmg @@ -18,8 +18,8 @@ match neg endmatch match mux - select mux->type == $mux - select nusers(port(mux, \Y)) == 2 + select mux->type == $mux + select nusers(port(mux, \Y)) == 2 set mux_a port(mux, \A) set mux_b port(mux, \B) set mux_s port(mux, \S) @@ -43,7 +43,7 @@ code neg_a neg_y mux_a mux_b mux_s mux_y a_signed } { - // Anchor negations to the original negation width + // Anchor negations to the original neg output width int width = GetSize(neg_y); Cell *cell = neg; diff --git a/passes/silimate/peepopt_negneg.pmg b/passes/silimate/peepopt_negneg.pmg index eafebc0d2..166e641b0 100644 --- a/passes/silimate/peepopt_negneg.pmg +++ b/passes/silimate/peepopt_negneg.pmg @@ -18,7 +18,7 @@ endmatch match neg2 select neg2->type == $neg index port(neg2, \Y) === neg1_a - select nusers(port(neg2, \Y)) == 2 + filter nusers(port(neg2, \Y)) == 2 set neg2_a port(neg2, \A) endmatch diff --git a/passes/silimate/peepopt_negrebuild.pmg b/passes/silimate/peepopt_negrebuild.pmg index 57739b210..37c8fba5e 100644 --- a/passes/silimate/peepopt_negrebuild.pmg +++ b/passes/silimate/peepopt_negrebuild.pmg @@ -39,16 +39,16 @@ code add_a add_b add_y neg1_a neg1_y neg2_a neg2_y add_signed add_b_signed neg1_ if (add_signed != add_b_signed) reject; - // Require negations to share the add signedness + // Require both negations share the add's signedness if (neg1_signed != add_signed || neg2_signed != add_signed) reject; { - // Avoid matching the same neg cell twice + // Avoid matching the same neg cell for both inputs if (neg1 == neg2) reject; - // Require a single wrap boundary for both negations and the sum + // Require a single wrap boundary across both negations and the sum if (GetSize(neg1_y) != GetSize(neg2_y)) reject; if (GetSize(add_y) != GetSize(neg1_y)) @@ -89,11 +89,13 @@ code add_a add_b add_y neg1_a neg1_y neg2_a neg2_y add_signed add_b_signed neg1_ neg1_a_rs.extend_u0(width, add_signed); neg2_a_rs.extend_u0(width, add_signed); + // Build -(a + b) to replace (-a) + (-b) SigSpec sum = module->addWire(NEW_ID2_SUFFIX("sum"), width); Cell *new_add = module->addAdd(NEW_ID2_SUFFIX("add"), neg1_a_rs, neg2_a_rs, sum, add_signed); SigSpec neg_out = module->addWire(NEW_ID2_SUFFIX("neg_y"), width); Cell *new_neg = module->addNeg(NEW_ID2_SUFFIX("neg"), sum, neg_out, add_signed); + // Extend result back to original add output width SigSpec neg_out_rs = neg_out; neg_out_rs.extend_u0(GetSize(add_y), add_signed); module->connect(add_y, neg_out_rs); From 7e8331dd95e11765d33b26d67c043ee6a9249ae2 Mon Sep 17 00:00:00 2001 From: Stan Lee Date: Tue, 3 Mar 2026 15:15:26 -0800 Subject: [PATCH 44/44] greptile --- kernel/fstdata.cc | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/kernel/fstdata.cc b/kernel/fstdata.cc index f417048f2..d9cc321da 100644 --- a/kernel/fstdata.cc +++ b/kernel/fstdata.cc @@ -116,10 +116,13 @@ void FstData::extractVarNames() // Track nested fork scopes using a stack to handle nested packed structs // Begins with outmost scope and ends with innermost scope + // Scopes are not normalized on the stack std::vector fork_scope_stack; // Start fork handles after the maximum real handle from FST file to avoid collisions fstHandle next_fork_handle = fstReaderGetMaxHandle(ctx) + 1; + + // Map of fork scopes to their members, which are all normalized std::map> fork_scopes; while ((h = fstReaderIterateHier(ctx))) { @@ -152,6 +155,7 @@ void FstData::extractVarNames() // If this is a nested fork scope, add its handle to the parent fork scope if (fork_scope_stack.size() > 1) { std::string parent_fork = fork_scope_stack[fork_scope_stack.size() - 2]; + normalize_brackets(parent_fork); fork_scopes[parent_fork].push_back(fork_handle); } @@ -176,7 +180,9 @@ void FstData::extractVarNames() // Add variable to the innermost fork scope in the fork scope stack if (!fork_scope_stack.empty()) { - fork_scopes[fork_scope_stack.back()].push_back(h->u.var.handle); + std::string current_fork = fork_scope_stack.back(); + normalize_brackets(current_fork); + fork_scopes[current_fork].push_back(h->u.var.handle); } std::string clean_name;