Refactor the crate's internal structures to make invalid states unrepresentable - #43
Open
marlls1989 wants to merge 62 commits into
Open
Refactor the crate's internal structures to make invalid states unrepresentable#43marlls1989 wants to merge 62 commits into
marlls1989 wants to merge 62 commits into
Conversation
…pe its columns Edge and Racer gain a Display impl (arrow()/rf() go), four hand-rolled edge-from-level correspondences collapse onto Edge::settled_level/from_settled_level, and the -vector/-ic/-pinlist columns of arcs_tcl.rs, define_cell.rs and liberty.rs move from hand-joined String lists to typed VectorValue/IcColumn/Symbol columns rendered through the new Words/Braced wrappers, replacing five separate join-names-with-spaces implementations with one.
The module holds the vocabulary the Liberate emitters write a command in: the value of one -vector or -ic column, and the wrappers Tcl's syntax puts around a list of them. Nothing outside src/emit reaches it, so it is pub(crate), and src/emit's own documentation names it without linking into a private item. The indenting writer arrives with library_liberty, which is what writes through it.
…regenerate the example decks The logic layer stops rendering conditions and states: Minterm::condition and logic::product build a BoolExpr instead of a hand-joined string, and Hazard exposes condition()/path()/pre_state() over the same values rather than pre-rendered text. Every -when line and the stderr when: field render that BoolExpr through Display, respelling the condition from A*!B to A & !B. The diagnostic report gets its own borrowing Display adapters (report::State/Path/Commas), and hazard_warning writes fields straight into the locked stderr handle instead of building strings. The # oscillation: Tcl comment and the /* oscillation: */ Liberty comment are deleted along with the code that built and spliced them; a hazard reaches the user through the stderr report alone. Deleting the splice merges an annotated block with its otherwise-identical unannotated twin where one exists, which the masked-arc conflation warning now reports. README, the hazard-detection doc and the Unreleased changelog entries are updated to match, and the eight examples/* artifacts are regenerated with the tool.
…ate block with a single Display impl, replacing DefineArc/LeakageBlock/BlockKind/MaskedArc Every define_arc -type and define_leakage form is now a Block variant carrying exactly its own fields (PinEdge, RacingPins, Column/LevelColumn), deriving PartialEq/Eq/Hash so the value itself is the dedup key (an IndexMap<Block, Vec<Minterm<Symbol>>>) instead of a rendered-text HashMap key. Conflation replaces MaskedArc, naming its subject through the same Block value a Description adapter renders. The parallel per-cell analysis now produces structure (Vec<Block>/Vec<Conflation>) rather than text; a single Display walk writes the deck through the caller's writer at the sink, and banner/write_file take &impl Display instead of &str bodies.
… own cardinality
`Cause::Race` held a `Vec<Racer>` that detection filled with one pin or two, so every use site
re-derived which it had: constraint generation matched the slice to decide whether a separation
could be stated at all, and the stderr report matched it to choose between `toggling A↑` and
`too little separation between A↑ and B↑`. The cause states it now. `Cause::Toggle { pin: Racer }`
is one input toggled alone, `Cause::Race { pins: [Racer; 2] }` a pair probed together, and an empty
or three-pin cause cannot be built.
Confluence files a Toggle from its single-toggle capture and a Race from each pair probe, holding
the pair in the order the probe named the pins; the sorted view a situation key needs stays in
`SituationKind::of`. `remedy` answers `None` for a toggle in an arm of its own, and the report's
`orders` field belongs to the pair, which is where it already went: a lone toggle is only ever filed
as an oscillation, so no occasion of one carried an order. `Hazard::ordinal` ranks a toggle with a
race, both being inputs that fail to converge, so the `(discovered, ordinal)` tie-break picks the
same representatives.
`orders_str` writes the pair's two arrival orders directly, retiring the general permutation helper,
and `trigger_str` reads the cause. The emitted artifacts and the warnings are unchanged.
…torisation on EdgeArcs, and name the bench thread-sweep profiles
The candidate edge arc — the currency the whole classification is decided in — becomes a named
ActiveEdge { clock: Symbol, edge: Edge } in place of the (Symbol, bool) alias that also shadowed
super::arcs::Arc. The direction reaches generates, born and types_edge as an Edge derived once at
the toggle, and a node's per-clock direction list is Vec<Edge>, so a pair whose two halves disagree
about which way the clock moved cannot be built. retain_active_edges compares a state's clock level
against edge.settled_level().
Reordering follows: Edge::Rise sorts before Edge::Fall where false (Fall) sorted before true (Rise),
so every BTreeSet and BTreeMap keyed on the arc iterates the other way within a clock. Nothing reads
that order. The emitted grouping — clocks in cell input-pin order, Rise before Fall inside a clock —
is built explicitly from the input list, and the remaining consumers ask only for membership or
emptiness. retain_active_edges searches its set for something to remove, but its keep-predicate is
monotone in the set (the set appears only under the "is this co-resident toggle itself an active
edge" test, which guards against removal), so the loop computes the greatest fixed point of a
monotone operator over the subsets of the initial set and lands on the same set whichever candidate
it takes first.
EdgeArcs gains factored: the outputs the read-gate factorisation took apart, stated by the pass that
decides it. cell_verilog asks that set whether an output emits a continuous assign instead of a UDP,
rather than rebuilding the reader map to test membership.
regions_from documents why hysteretic comes out true: everything it assembles is a capture or an
off-edge of an edge register, and a node that carries its value across the phase between two active
edges is a state variable.
The benchmark thread sweep was a (parallel, heavy) bool pair with only three meanings; it is now a
Profile enum with a variant per meaning, reached through Profile::parallel/serial and asked for its
points. The HEAVY cell list stays as it is — that one is data.
…te_holding from the cached regions, and type the CLI's spec source and output destination MachineAnalysis becomes an enum: Derived carries the arcs, hidden arcs, constraints, hazards, leakage states, edge classification and the exploration handed on to a second view, and Stopped carries the budget counter alone. The derivations are fields of Derived, so a stopped pass has no empty collections to hold. AnalysedCell.unexplored and exposed_view keep their Option types and their docs now state that each records what its one write site decided. AnalysedCell::state_holding reads the cached regions instead of a bool field set beside them: a region is hysteretic exactly when the signal is a state variable, so any(hysteretic) is the same verdict the deleted holds_state computed. The CLI's positional spec is a SpecSource behind a value parser, so "-" is the Stdin variant rather than a token compared at each use, and the output destination resolves once into a Destination matched at the emission site.
… artifact's alphabet through its own Display cell_liberty hands back the Vec<Group> it builds. library_liberty states the whole run as a Library value the sink displays: the library group, then every cell's groups one level in, nested by tcl's Indented, a fmt::Write that puts the prefix on a line at its first character and passes an empty line through bare. Indented arrives here with the emitter that writes through it. The statetable alphabets are Display newtypes local to the artifact whose syntax they belong to. Liberty writes a cube value as Level (H/L/-), a cube as Levels, a clock column as Token (R/F/~R/~F/-), a row's next-state actions as NextPattern (H/L/N/-), an edge row's input field as EdgeInputs and the joint body as Table. Verilog writes a UDP column as its own Level (1/0/?) and a cube as Pattern. The two alphabets stay apart: they are different value sets in different external syntaxes.
…dently A target's sweep is set by two independent flags: whether the cell is one of HEAVY, and whether the stage being measured parallelises internally. All four combinations are configurations a target can be in, a light cell on a serial stage among them. The n=1 baseline and the default width are informative for every target, one being the reference the other is read against. The intermediate widths show scaling only where the stage parallelises and the cell is wide enough for it: on a serial stage they re-measure the same single-threaded work, and on a cheap cell they measure noise. Light targets carry the baseline their documentation names.
CELLSMITH_BENCH_THREADS names the widths as a comma-separated list, so CELLSMITH_BENCH_THREADS=1 measures the single-threaded point and 1,2,4,8 sweeps those four. A width is a point on one axis rather than a mode. Unset is one measurement with nothing pinned, running on the global pool as the tool does. The list reaches the benchmarks through the environment because criterion_main! owns argv and rejects arguments it does not recognise. Every target is measured over the same list. The cell names that stood for a harder workload, and the per-target annotation of whether a stage parallelises internally, decided thread counts from the source and are gone; what a target runs on is now asked for at the point of running it.
CELLSMITH_BENCH_THREADS=1,2,4,max reaches the top of the host without naming a number that holds on only one machine. The token resolves as the list is read and the measurement is reported as the count it resolved to, that being what ran. Resolving it can name a width the list already holds, so a width reached twice is measured once: criterion takes one registration per benchmark id.
analyse_machine returns Result<Derivations, machine::ExplorationLimit>, the form Machine::build and Exploration::Reused already use for this outcome, and Cell::analyse matches its Ok and Err arms into the AnalysedCell fields.
…hrough Display adapters and buffer it onto stderr PinEdge lives beside Edge in logic::arcs and carries the derives Racer had, so a detected hazard's racing pin and an emitted block's switching pin are one type. emit::block and emit::arcs_tcl import it from there, keeping the dependency running from emit to logic. main.rs's header, orders and trigger renderers are borrowing Display adapters writing into the warning's own writer. Trigger's variant is which transition the cause names, and the caller emits the field only where the cause names one. The hazard report goes out through a BufWriter over the locked stderr handle, flushed before the artifacts are written, so the report's many small writes reach the unbuffered stream in batches.
…gs, and update the hazard docs and comments to the three-cause model when_str/hidden_when_str/pinlist_str return BoolExpr/Vec<Symbol> options and structures, not strings; renamed to when/hidden_when/pinlist to match their siblings. Fixed the dangling constraint_vector_str reference in model.rs to name constraint_columns, the function that actually reads the prevector's held levels. docs/hazard-detection.md, and the comments in constraint.rs and arcs_tcl.rs that state the tie-break, now describe three causes (Toggle, Race, Pulse) crossed with two outcomes, with a lone toggle its own cause rather than a degenerate race, and the tie-break's six (cause, outcome) pairs collapsing into four ranks by Hazard::ordinal.
…ne adapter benches/stages.rs: cell_liberty now renders through the Liberty wrapper, matching aggregate.rs, so the target measures Liberty rendering again instead of just group construction. Words, Commas, Projected, Levels, NextPattern, EdgeInputs and Pattern each wrote their own "separator between consecutive items" loop. They now share one adapter, Joined, that carries the separator and a per-item projection; each keeps its own name and call shape as a thin wrapper over it. EdgeInputs' per-column choice between a Level and a Token becomes a small EdgeInputColumn variant instead of writing either inline, so its projection still returns one Display type.
- Rewrite MUT cell comment in examples/cells.toml to accurately describe that oscillations are detected and reported on stderr, and constraint blocks are generated only when constraint_arcs is explicitly requested - Record the leakage-block conflation asymmetry in KNOWN-ISSUES.md: leakage blocks conflate when no expose list is present, but combinational and hidden blocks do not, despite facing the same difficulty; the question is whether the constraint-block handling code arms are unreachable in practice
`PinEdge` lives in `logic::arcs` beside the `Edge` it carries, and both the hazard report and the emitted block name it from there. `emit::block` keeps its `use` private so that path stays the only one, and both modules say so where a reader would otherwise have to work out whether a second path was intended.
…run at an over-budget cell ModelError and ExplorationLimit are written out by hand: per-variant docs, a Display arm each, an Error impl whose source() chains into the wrapped error, and From impls for composition and for the io boundary. ExplorationLimit is a leaf again, carrying only the ceiling it passed, and ModelError::Exploration adds the cell and the flag that raises it. An exploration stopped by a budget now fails the analysis at that cell rather than leaving an unexplored cell for the CLI to scan for, so AnalysedCell::unexplored and the per-cell scan are gone and run returns io::Result. thiserror is removed from Cargo.toml/Cargo.lock.
Drop the claim that an over-budget run reports the cell that failed first: the cells are analysed in parallel and rayon does not say which error a collect returns, so the README and main's comment now say the run fails at an over-budget cell without promising which. analyse_machine's doc no longer describes a view mirroring a ceiling, a path that went with Exploration::Reused's Result payload, and the KNOWN-ISSUES entry cites the constraint arms and their covering test where they actually are. thiserror leaves the README's dependency list, where it outlived its removal from Cargo.toml. State's and Path's Display render through Joined, so the adapter's claim to be the one separator loop holds. ModelError::source() matches every variant explicitly, so a variant added later has to answer whether it wraps an error rather than compiling against a wildcard. The stage benchmarks take a cell that fails to analyse as a broken fixture and stop: the cells come from the checked-in examples/cells.toml and every one of them analyses under the default budget, so a silently shorter run would hide the breakage.
… the sink Delete render(), the last helper forcing an emitter to return a String and concatenate a Vec<String>. cell_verilog now returns Vec<Item> (Primitive, EdgeRegister, Constant, Wrapper) and cell_define_cell returns Vec<DefineCell>; both render through a borrowing Display at the sink in main.rs, matching the Tcl deck and Liberty group emitters already converted.
The manifest declares `liberty-parser = "0.3"`, and every module path, doc comment and README mention spells the crate `liberty_parser` — the name Cargo derives from the published package.
…'s brace groups through Braced, hold Verilog UDP rows as ordered values compared by row set, and document the fail-fast changelog entry and the ModelError/ExplorationLimit derives
…arcs deck's Deck adapter beside cell_arcs, fold Arc and HiddenArc's pin/edge fields into one PinEdge each, and correct the conflation KNOWN-ISSUES entry
…t pin/edge fields into PinEdge Closes the last instance of the PR's own principle: a pin together with the edge it makes is one PinEdge value, not two separately-stored fields.
…ve the pulse-constraint test key a named struct over PinEdge Closes the split pin/edge sweep for PR #43: the last two stored Symbol+Edge carriers in the edge-register classifier become one PinEdge each, and the width.rs test helper's positional (String, Edge, String) key becomes a named ConstraintKey holding a PinEdge and Vec<Symbol>.
`OutputLevels` carries an output with its levels at both ends of one toggle, the pair that decides an arc's direction. `Literal` carries a variable and whether it appears positive, which is what `logic::product` folds. `ReportField` carries a diagnostic's label and the value written beside it. The minimiser's duplicate groups and demotions, the seed ranking's candidate and settlement, the statetable's level signals and cover passes, and edge.rs's read-gate pass levels and per-clock active directions each carry their components by name. One shared type states the region-and-action pairing that the Verilog and statetable emitters each spelled in their own order.
…them positional Replaces bare tuples used as test fixtures/keys across the model, logic and emit layers with named structs (AnalysedPair, PulseKey, ReleaseKey, LabelSite, OpenPhase, AwkwardVoltage, RenderedStatetable, ConditionedCounts, ContextKey, PinSignature, ExposureFixture, SignalDef/ParsedSystem, AbValues, TransitionKey), and shares one analyse_both test helper across the four emit modules that each carried an identical copy. All changes are confined to #[cfg(test)] scope and tests/cli.rs; no test expectation changes.
…, and the rendered-row token alphabet to an enum width.rs: PulseKey.nodes is now Vec<Symbol> (was a comma-joined String), keys() clones the hazard's group directly, and on_nodes takes the node list as a name slice compared against Symbol::as_str. fn keys() gets back its own doc comment, transposed onto the PulseKey struct since 299688d. statetable.rs: RenderedStatetable's input_names/state_names are Vec<Symbol>. RenderedRow's three token fields move off Vec<String> onto a new Token enum with a parsing function that panics on any unrecognised text; row_matches, predict_node and settle_rendered match on Token variants instead of token strings.
…two test-only pairings arcs_tcl.rs: fn groups's Columns doc drops the sentence duplicating what the per-field docs already say, and its masked-toggle test fixture pairs a conflation report with the pin it names through a MaskedToggle struct instead of a positional tuple. liberty.rs: the multi-name test's PinSignature carries an Option<Direction> over Liberty's closed direction keywords instead of a Debug-rendered, default-collapsed String, so a pin missing its direction attribute stays distinct from one that has it.
Step 3: Restore dedup_pass doc above function, trim DuplicateGroup doc to 2 lines, fix SignalDef and system! macro docs Step 4: Fix Literal doc ordering claim Step 7: Restore ExposedLevel and TemplateSpec intra-doc links, widen TemplateSpec to pub Step 9: Revert slice::from_ref assertion Step 10: Delete swap-test justifications from SubblockField and AbValues docs
…forced Test-only LabelSite drops its &'a str output field for the Symbol l.output already holds, dropping the lifetime parameter along with it. Forcing.forced is deleted: it had three writers and no reader, and the #[allow(dead_code)] that shielded it goes with it. The two agreement checks the classifier relies on — pinned_value's shape-match and the posts.len() == 1 conjunct — are untouched; only the consumption of each match's now-unused value is dropped.
The fn doc and arcs.rs's ExposedLevel cite were converted; the module doc's cite was left in bare code font.
Split run() into apply_overrides, artifacts, diagnostics and emit_stdout/emit_files, each callable and assertable in process, and move every test tests/cli.rs held into the modules they exercise: ten bin tests on the split functions' Result and written output, eleven value-level arcs tests on Block in arcs_tcl, and one edge/level Liberty-form test in liberty.rs using analyse_both instead of a CLI-flag process run. Delete tests/cli.rs and its CARGO_BIN_EXE_cellsmith harness; no test spawns the cellsmith binary or asserts an exit code.
A test states a property and asserts it semantically, against an expectation derived from the input, and holds for every output the tool is free to produce. Running the code twice and asserting the two results agree passes whenever the two paths are self-consistent, including when both are wrong. Two relations survive: a correspondence the consuming format imposes, and an equivalence or non-interference claim that is itself the documented behaviour.
The pin order a run produced is the same order in its -pinlist, its -vector's characters and its -ic's columns. That claim fixes no order and needs no second run, so it holds for every output the tool is free to produce and is asserted like any other property.
A statetable's rows carry their values in the order that table's own header declares its pin columns — the same correspondence the pinlist, vector and ic columns hold, and the one a test can assert on any valid output.
The property a test asserts is one the code or its documentation states. Where neither states it, the claim belongs there first, otherwise the test pins something nothing promised. The correspondences named are examples rather than the list.
A justification for keeping an order names what outside this crate requires it, as part of a contract with that reader. Justifying an order by our own use of it answers to nothing; where no external reader requires it, the order is free.
Liberate reads a block's -pinlist, -vector and -ic as positional columns of one argument, so those three agreeing on the pin order a run produced is the contract with that reader.
Seed settling has no reason left to run sequentially once the candidate ranking is gone, and the key that picks among equally dominant observations could go — with the sites a removal would have to answer for.
Each `--when` test now derives its expectation from the analysed cell it emits — the distinct transition identities its arcs carry and the distinct hidden events its toggles carry — rather than re-emitting a sibling analysis and comparing the two outputs. Where a test needs several selections it assigns `when` on the one analysed cell between emissions, which `cell_arcs` is deterministic over. The helpers those comparisons went through (`arc_shape`, `shaped_blocks`, `sorted_blocks`) and the block multiset counter go with them, and the key one general block per transition is counted under is hoisted beside `transition_of` for the two tests that read it. `ConstraintRank`'s doc states what the tie-break settles without appealing to a fold landing on one answer. `leakage_section_follows_hidden_arcs` is dropped: nothing outside the crate requires `define_leakage` to follow the arc blocks. `when_output_contains_every_default_arc_block` is dropped for its claim being stated directly in `bare_when_emits_arc_when_lines` — every conditioned block's transition, or hidden event, is one the general layer states too.
The ranking ordered the whole candidate pool and pruned nothing, so the seed set is unchanged; only the order seeds enter the BFS queue, which nothing reads. With it go the per-candidate settlement maps, the state variables' depth relaxation and its tie-break, and the `Candidate` pair that carried a seed alongside its settlement map — a seed is now a bare `Minterm<Symbol>`. The module's budget doc, `Coordinates`, `ExplorationBudget::default` and `explore` drop the sentences that described the ranking and the order it fixed.
…tion Liberty, statetable and Verilog emitters each carried a test that ran analysis twice (default vs forced no-edge-collapse, or direct vs CLI-flag opt-out) and asserted the two renderings matched byte-for-byte or as an equal Debug string. That agreement is self-consistent even when both runs are wrong, so it is dropped in favour of the per-side assertions the tests already carry alongside it. verilog.rs's table_row_set helper, whose only callers were these asserts, goes with them. model.rs's determinism test ran the same analysis eight times and asserted the repeats agreed; it becomes a direct assertion that the first-declared offending output is the one reported, which is the property the code actually promises. main.rs's hazard-landing test compared a race's two alternatives as a single order-joined string; race alternatives are a set, so the test now splits on the group/landings boundary and compares the landings as a HashSet. hazard.rs and edge.rs comments that justified an internal tie-break key by appeal to "deterministic"/"byte-identical" report output are restated as description of what the key does, since nothing outside the crate reads which of several equally dominant observations is picked.
…is.rs The edge-classification invariance test now compares its two runs by value field-by-field, comparing AnalysedOutput and StateRegions (which carry no PartialEq) on their identifying fields rather than Debug text, and canonicalises hazard_shapes' settled set before comparing it instead of relying on its container's incidental order. The exploration-untouched test's comment no longer claims an order its assertions never check.
The classify opt-out leaves the annotation at its plain Default, and RDFF's reset arcs take the same classification SYNCR takes. Neither claim is about bytes.
…tity/HiddenFiringIdentity, and Projected's named fields
… its cost An order needs a motivation stated where it is, checkable and checked: an outside reader of the position, or an algorithm made cheaper than its unordered form — sorting to dedup is a reason by itself. What fails is an order bought only for its own stability, and where determinism is not required the cheaper variant wins. In tests no economy is a reason, and equality often asserts immutability rather than a property: checking the output matches a reference does not establish correctness, the test has to reason about why it is right.
…found State that a name is a Symbol and a name-to-Boolean mapping is a Minterm as one rule, under proper use of support types: a variable a Minterm does not define is by definition a don't-care, so a partial assignment needs no separate type and no absent-key convention. The Symbol bullet moves out of Language and style into that section, so the rule is stated once. Add "Structure is held until the edge" as its own section: a value stays structured until the sink renders it. Scope "efficiency and correctness come first" to the rules about how the code is shaped and tested, leaving British spelling and the Git conventions outside the trade-off. Require that a reason outranking a rule's letter amend the rule in the same change. Drop the demand that an order's motivation be checked, keeping checkable. Restate the test-economy rule as never a reason to assert less. The collection bullet defers to the order rule instead of restating it narrower.
Deletes assignment(), whose BTreeMap<Symbol, bool> result dropped a minterm's don't-cares; its callers in the arcs emitter now read the minterm directly through value_of, and the four -when literal builders walk vars() against iter(). ConstraintColumns.probed, Hazard.node_levels (and its node_levels_at/victims plumbing), and forcing_pins' forcing-pin row (dissolving the single-field Forcing struct) all become Minterm<Symbol>, keeping the fully-initialised-state panics that read a missing group node as a defect rather than a silent don't-care.
Add "Use the library's operations, not your own": espresso-logic's operations come verified, so a hand-rolled equivalent is a second implementation nobody tests and is a defect whether or not it works. The rule names the step where this goes wrong — concluding the library lacks an operation after failing to find one method that does the whole job, when the answer is a composition of two — and lists the calls this crate has needed, so the loop is not written again. It also says what is not the defect: projecting a row onto its columns as Liberty's statetable or the Verilog UDP is written, which those formats demand. Under proper use of support types, add that a set of tri-state rows over a shared header is a Cover, since splitting the header off the rows is what forces every reader downstream to re-pair them by position.
Convert every StateRegions reader (emit.rs, verilog.rs, statetable.rs, liberty.rs, edge.rs) to read the on/off/hold Espresso Cover directly by name via Minterm::value_of/project_to_labels, render functions with Cover::to_expr_by_index instead of hand-joined SOP strings, and fold sampled points into a BDD with one Cover::from_cubes + build_cover call instead of a manual OR/AND loop. Delete the now-redundant StateCube type alias, region_cubes projection and the duplicated on/off/hold vector fields, leaving each region held once as its cover.
Liberty and Verilog both read `&`, `|` and `!`, and this design uses no buses, so bitwise and logical negation are the same token. The Liberty and Verilog sinks therefore write the `BoolExpr` a region lowers to, and the expression layer's own rendering is the one both formats get. The tests that named a function by its operator spelling name the same functions in that rendering. A new test asserts the shape instead of the text: a lowered region is a sum of products, over two fixtures whose primes respectively cannot and can share a literal, so factoring is accepted without being required and a change in how the expression layer renders surfaces here rather than in a deck. examples/cells.lib and examples/sequentials.lib are regenerated.
…e state comparisons with espresso's Minterm API The arc/rest level families (HeldLevel, ExposedLevel, ArcLevels, RestLevels) and the constraint victim family (VictimNode) each held a mapping from a node name to a Boolean as a struct or a Vec of them, with adapters restoring by-name lookup for consumers. These become Minterm<Symbol> rows, read back with value_of, built once from accumulated (Symbol, Option<bool>) pairs. merged_victims's union goes through Minterm::or, its levels through project_to_labels. The per-variable divergence closures in confluence::detect and width::detect are replaced by the Kleene XOR/OR operators. A stray doc comment claiming byte-compatible emitted cubes is restated as a claim about shared computation, not output bytes.
A constraint block's victim columns are read off the emitter's own column list by name, so no minterm's stored header order reaches the output and a node the row does not define renders as the don't-care it is. A new test states the correspondence the block must keep between its parts, without fixing which order a run produces. The exploration's candidate pool is a HashSet: its order was never read, and the pool reaches four million entries on the candidate path. Which observation supplies a general block is now schedule-dependent, recorded in KNOWN-ISSUES.md beside the free choice it belongs to. A state-table column is held once under both its names, so the table node and the signal it came from cannot desynchronise. A hazard derives its victim levels from the state it probed rather than storing them again, and the one-probed-state invariant is stated where the remaining views are declared and asserted where they are copied. The CLI names the path in its io errors and renames its artifacts into place together, so a failed run leaves no half-written output directory. A warning's outcomes are filed in a HashMap, since nothing reads their order. Test and documentation corrections the review found: each minimisation fixture is pinned to the answer it forces and builder-independence is documented as the property being checked; the two-latch opt-out test asserts the model each switch states; the -ic gate test compares decks as multisets of blocks; the block key's converse direction is argued rather than asserted; the engine document drops the candidate ranking that no longer exists; comments name items instead of line numbers; sorts that cannot observe stability use sort_unstable; two type names this branch made ambiguous are TransitionEvent and SeparationRole.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is for. Making the crate more maintainable and reducing its technical debt — by
sharpening
GUIDELINES.md, the standing rules every contribution is held to, and bringing the codeinto compliance with the sharpened version. Six rules are involved: two that shaped the work from the
start, two added after a review found the first pass had drifted from them, and two more added after
an audit of how the crate uses its own logic library. Two behaviours change, both named below.
A classification is a variant
Picking the variant is the classification, and the variant carries exactly the fields that kind has;
a field that only sometimes applies is an
Optioninside it.src/emit/block.rsholdsBlock: eleven variants over six payload structs, one for each Liberateblock form, with a single
Displaywriting the whole block and the-typeliteral owned by the armthat emits it. It is owned and hashable, so it is also the dedup key —
BlocksisIndexMap<Block, Vec<Minterm<Symbol>>>, keyed on the block value.DefineArc,LeakageBlock,BlockKindandMaskedArcare gone, andConflation { block, states }makes the conflationreport's subject the emitted block itself.
Toggle { pin: PinEdge },Race { pins: [PinEdge; 2] },Pulse { pin: PinEdge }. Empty and three-or-more areunrepresentable, and the rule that a lone toggle yields no constraint is structural rather than
prose.
Result<Derivations, machine::ExplorationLimit>, so the invariant its doc usedto state — stopped means every derived field is empty — holds because a stopped exploration hands
back no
Derivationsto hold fields.edge.rsspeaksEdgethrough its decision core instead of anis_rise: bool, so a pair whose twohalves disagree cannot be built, and
EdgeArcsstates its read-gate factorisation once.state_holdingis derived from the cached regions; the CLI's spec source and output destination aretypes rather than a sentinel string and a loose bool-plus-path.
Structure is held until the edge
Displayis for output; a function that returns rendered text has already lost.BoolExprend to end.logic::productbuilds one from literals and the logiclayer's rendering toolkit is deleted, along with
Hazard's five*_strmethods.-vector,-icand-pinlistare three projections of one column walk, typed asVectorValueand
IcColumninsrc/emit/tcl.rs, so they cannot fall out of alignment.Displayadapters insrc/report.rs.cell_arcsreturns blocks,mainwrites them. TheLiberty library writes through an indenting
fmt::Writeadapter rather than rendering a string andre-parsing it to re-indent, and each artifact renders its own alphabet through its own
Display.The library's types are the ones to use
An audit of every espresso-logic call site, and of the crate's own vocabulary against the library's,
found the same shape rebuilt by hand in a dozen places.
GUIDELINES.mdgains two sections — onenaming which type owns which job, one stating that the library's operations come verified, so a
hand-rolled equivalent is a second implementation nobody tests and is a defect whether or not it
works. The audit's own lesson is written down with them: most of these were not written in ignorance
of the library but after failing to find one method that did the whole job, when the answer was a
composition of two.
A name-to-Boolean mapping is a
Minterm<Symbol>. A variable a minterm does not define is bydefinition a don't-care, so a partial mapping needs no separate type and no absent-key convention.
Gone, each replaced by the row it always was:
logic::assignment, which took aMintermand returneda map of it;
confluence::node_levels_at;ExposedLevel;VictimNode; and the by-name adapters thatexisted only to restore lookup over vectors of them.
ArcLevelsandRestLevelsstay, because neither was ever the name-to-Boolean map — their fieldswere.
ArcLevelsgroups the three rows one arc samples, and onmasterheld them asVec<(Symbol, bool)>andVec<ExposedLevel>; it now holdsoutputs,exposed_startandexposed_end, aMinterm<Symbol>each. A minterm gives one value per variable, so two mappings overone node set cannot be one row.
Constraint.nodesandConstraintColumns.probedare minterms for thesame reason, and
Hazard::node_levelsis a method deriving its row from the state the hazard probedrather than a field storing it again.
A set of tri-state rows over a shared header is a
Cover.StateRegionsheld each of its threeregions twice — once as an espresso
Cover, once asVec<StateCube>, a positionalVec<Option<bool>>aligned by hand to a separate name vector — with a projection keeping them instep, and its own comment admitting the two were the same cover. The hand-rolled half is gone, and
with it
region_cubes,verilog.rs'slevel_at(which re-implementedMinterm::value_ofas alinear name scan), and
statetable.rs's two index maps and the loop over them that re-implementedMinterm::project_to.Operations come from the library. Three hand-written copies of
BddBuilder::build_cover'sper-cube fold are gone. A pin's function is
Cover::to_expr_by_index, rendered byBoolExpr's ownDisplayat the sink; both hand-written sum-of-products assemblers went with them, including onethat decided a function was a tautology by string-comparing its rendered text. Comparing two machine
states is a Kleene XOR of the two rows rather than a per-variable loop, and the one-reference-against-
many case in
width.rsis that XOR folded with OR.merged_victimsisMinterm::orfor the variableset composed with
project_to_labelsfor the levels.One datum, one representation. A state-table column is held once under both its names
(
StateNode { signal, node }) rather than as a vector and a map that a test helper had to invert,relying on an injectivity nothing enforced. A hazard derives its victim levels from the state it
probed instead of storing them again, and the one-probed-state invariant is stated where the
remaining views are declared and asserted where they are copied.
Ordered containers that bought nothing are unordered. The exploration's candidate pool is a
HashSet— its order was never read and it reaches four million entries on the candidate path — and awarning's outcomes are filed in a
HashMap. Which observation supplies a general block is nowschedule-dependent, recorded in
KNOWN-ISSUES.mdbeside the free choice it belongs to.Errors
The crate has one error style, taken from espresso-logic: hand-written
Displayandstd::error::Errorwith a realsource()chain,Fromfor composition,From<_> for io::Errorat theboundary,
#[non_exhaustive], and small leaf errors wrapped by composites that add the context theyhave.
thiserroris dropped from the dependency list andBox<dyn Error>fromrun, which returnsio::Result<()>.ExplorationLimitis a leaf naming which budget stopped an exploration and at whatceiling;
ModelError::Explorationwraps it and names the cell, the way its seventeen siblings do. TheCLI's io boundaries name the path they were given, so a missing spec or an unwritable directory says
which one.
Two behaviour changes
A run that passes an exploration budget fails at that cell and exits, rather than analysing the rest
and listing every offending cell at the end.
ExplorationLimitpropagates with?instead of beingstashed on the cell and scanned later, so
AnalysedCell.unexploredand the scan are gone.A run that fails partway through writing its artifacts leaves none of them: each is written to a
sibling temporary path and renamed into place at the end, so a downstream Liberate run cannot read a
half-written output directory.
What changes in the output
Conditions and functions spell as
A & !Bwhere they spelledA*!B— both notations are accepted byLiberty and Verilog alike, and the hand-rolled joins were the defect. A Liberty
functionmay alsocome out factored,
A & (B | C)rather thanA & B | A & C, since the library's own lowering is nowwhat produces it; nothing ever required a flat sum of products. Hazards are reported on stderr and
nowhere else: the emitted Tcl and
.libstate the timing that removes a hazard and carry no commentabout the hazard itself. Nothing else moves.
A value with more than one component takes a name
A tuple is an ordinary Rust value inside a function — a map's key and value,
enumerate,zip, abuffer a foreign signature dictates — but it may not outlive the scope that makes it, and neither may
a struct or enum variant that tells its components apart by position.
GUIDELINES.mdstates the rule;the crate is brought into line with it. Named where they were positional:
Capture,Sample/Firing/Move,Delta,DerivedArcs,RegionAction,SignalDef,OutcomeField,System,FiringIdentity/HiddenFiringIdentity,StateNode,TransitionEvent,SeparationRole,and
Projected's columns.src/emit/block.rsowns the emitted-form vocabulary, so a type naming whatthe analysis decided is named for that instead:
TransitionEventis the transition an arc records,SeparationRolethe kind of separation a hazard calls for and which end it constrains.Tests assert properties
A test states a property of the thing under test and asserts it semantically, and it holds for every
output the tool is free to produce. Running the code twice and asserting the two results agree is not
a test of behaviour: it passes whenever the two paths are self-consistent, including when both are
wrong. That holds whatever the currency — text, values, counts or multisets. A test resting on
equality often asserts no property at all but immutability, which is a fact about the previous run
rather than about the thing under test. The property a test asserts is one the code or its
documentation states; where neither does, the claim belongs there first.
Where a correspondence between the parts of one output is the claim, that correspondence is itself a
property: Liberate reads a block's
-pinlist,-vectorand-icas positional columns of oneargument, so those three agreeing on the pin order a run produced is the contract with that reader.
Emitted columns are now read by name off the emitter's own column list, so no minterm's stored header
order reaches the output, and a test states that correspondence without fixing which order a run
picks.
An order kept in the code needs a motivation stated where the order is, and two kinds qualify: a
reader outside the crate that requires the position, or an algorithm made cheaper than its unordered
form — sorting to dedup is a reason by itself. An order bought only for its own stability is not;
"held sorted so the report is stable" answers to nothing. Where determinism is not required the
cheaper variant wins, so a sort whose stability cannot be observed is
sort_unstable.Above all of it,
GUIDELINES.mdnow states that efficiency and correctness come first and that everyrule about how the code is shaped serves those two — code that compromises efficiency to keep an order
is wrong, and a test that checks the output matches a reference is not establishing correctness.
Twenty comparison-shaped tests across
arcs_tcl.rs,liberty.rs,statetable.rs,verilog.rs,model.rs,main.rsandedge.rsnow assert properties instead, and the three comparator helpersarc_shape,shaped_blocksandsorted_blocksare deleted with their last consumers.tests/cli.rsis dissolved into the modules it tests: no test spawns the cellsmith binary, and none asserts an exit
code.
Dead determinism scaffolding removed
explore's candidate ranking — the settlement and depth quantities, the depth relaxation, the rankedpool and its sort — is deleted. It ordered the whole pool and pruned nothing, so the seed set is
unchanged and only the order seeds enter the queue, which nothing may depend on. The walk was already
schedule-shaped past the seed segment: each BFS level collects into a
HashMapwhose iteration orderfeeds both
ex.orderand the next frontier, whichexplore's own comments call free choices. Thecrate's only
#[allow]went with a write-only field inedge.rs's forcing scan.How that is established
The green bar, and nothing else:
cargo fmt --all --check,cargo clippy --all-targets --locked -- -D warnings,cargo build --all-targets --locked,cargo test --locked,cargo doc. 408 lib and 27 bin tests pass; no#[allow]remains insrc/.Correctness here is the same set of arcs, hazards and constraints, never identical bytes — emission is
not reproducible run to run, and it is not meant to be. Measured on one binary twice over
examples/cells.toml --when:cells_arcs.tcldiffers by 6016 lines whilecells.v,cells.libandcells_cells.tclare byte-stable. The tests are what catch a regression, at the site where it happens.Findings declined, and shapes left alone
Two findings from review were declined on their merits. One asked for
*std::slice::from_ref(&node)to be replaced;clippy::cloned_ref_to_slice_refsrecommends exactlythat form, so the existing code was already right. The other assumed an intra-doc link would be
dropped; instead
TemplateSpecwas widened topubso the link resolves.Deliberately untouched, each because it is a shape the rule permits:
model.rs'ssignal_regions()and its call sites, which is std's collectible
(K, V)protocol; the product-literal buffers inemit/arcs_tcl.rs, iterated on the spot; the argument buffers whose shapeMinterm::with_labelsandcompose_mapdictate; andstatetable.rs's positional row types, which are a projection made at thepoint of writing, since the Liberty
statetableand the Verilog UDP are columnar formats.Record correction. Commit
5017397's message names a typeReportField; the type delivered isSubblockFieldinsrc/main.rs. The message is not rewritten.Deferred, and recorded in
KNOWN-ISSUES.md. Seed settling still runs sequentially, a shape thatonly survived as the ordering's justification; parallelising it is a critical-path change and its own
pass. And the
(discovered, ordinal)key that picks among equally dominant observations stays, withthe sites a removal would have to answer for written down.
Benchmarks
The thread count a target is measured at is asked for at the point of running it:
CELLSMITH_BENCH_THREADSnames the widths as a comma-separated list,maxstanding for the width theglobal pool was built with, and unset measures once with nothing pinned. A width is a point on one axis
rather than a mode, so the single-threaded measurement is the
n=1point of a sweep. Every targetreads the same list.