Skip to content

✨ Support sequential networks in write_aiger - #699

Open
marcelwa wants to merge 3 commits into
lsils:masterfrom
marcelwa:upstream-seq-write-aiger
Open

✨ Support sequential networks in write_aiger#699
marcelwa wants to merge 3 commits into
lsils:masterfrom
marcelwa:upstream-seq-write-aiger

Conversation

@marcelwa

@marcelwa marcelwa commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Part of a three-PR chain, please review in this order:
#700 (AIGER reader fixes) → #701 (register initialization) → #699 (this PR).
Each builds on the previous one; the diff shown here collapses to the sequential
write_aiger change alone once the other two land.

Description

write_aiger hardcoded a latch count of 0 and asserted the network was combinational,
so a sequential<aig_network> could be passed in but silently lost its registers — they
degrade into extra PI/PO pairs, and the assertion is a no-op in release builds.

This adds real latch support, gated on num_registers / foreach_ri / foreach_ro via
if constexpr. The combinational path is untouched and still produces byte-identical
output — the two pre-existing byte-exact tests pass unchanged.

Reset values

Reset values are now always written explicitly, which matters more than it first looks:

  • An omitted reset field means 0 in the AIGER format, so omitting it for an
    uninitialized register would silently convert it into a zero-initialized one.
  • An undefined reset is therefore encoded the way the format prescribes: by
    repeating the latch's own current-state literal.

Verified in both directions — ABC reports the expected Init0 = 1. Init1 = 1. InitDC = 1.
for a network carrying one register of each kind, and the output round-trips through
aiger_reader preserving register count, reset values, and names.

CI ordering

The binary format encodes CIs implicitly, requiring variables 1..I to be the primary
inputs and I+1..I+L the register outputs. That invariant was previously assumed
silently; it is now documented and checked by a debug assertion.

Drive-by

Added the missing <cassert> include. The header uses assert but only ever compiled
because its includers happened to provide it — a TU that includes write_aiger.hpp
first currently fails to build.

Testing

Five new test cases in test/io/write_aiger.cpp (26 assertions total), passing in both
debug and NDEBUG builds:

  • byte-exact output for a sequential AIG with an undefined reset
  • byte-exact output for reset values 0 and 1
  • write → aiger_reader round trip preserving PI/PO/register counts and reset values
  • latch names emitted as l<n> and surviving a round trip
  • undefined reset encoded as the self-literal and read back as undefined

Context: this came out of building a Python-side ABC bridge in
aigverse, where sequential designs could not be
handed to ABC at all because there was no way to write them. Offering it upstream since
it seems generally useful.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GbUqcnZayPooFgekEXVSjz

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.68293% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.07%. Comparing base (25beb0e) to head (0358a0f).

Files with missing lines Patch % Lines
include/mockturtle/io/aiger_reader.hpp 75.00% 2 Missing ⚠️
include/mockturtle/io/write_aiger.hpp 95.65% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master     #699   +/-   ##
=======================================
  Coverage   84.06%   84.07%           
=======================================
  Files         190      190           
  Lines       29468    29510   +42     
=======================================
+ Hits        24773    24810   +37     
- Misses       4695     4700    +5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

A sequential network written by ABC could not be read back correctly. Three
separate defects, all in the AIGER latch and output handling.

**Omitted latch reset values were read as undefined.** Both the binary and the
ASCII reader defaulted to NONDETERMINISTIC when a latch line carried no reset
field. The format specifies the opposite: the original AIGER format initialized
every latch to zero, and the reset field added in 1.9 is optional, so its absence
means 0. ABC relies on this and omits the field for zero-initialized latches, so
every zero-initialized register came back undefined.

**The ASCII reader compared the wrong token.** The branch recognizing a reset
value of 1 tested `tokens[1u]`, the next-state literal, instead of `tokens[2u]`.
An explicit reset of 1 was therefore never recognized, and a latch whose
next-state literal happened to be 1 was wrongly reported as one-initialized.

**Bad state properties were dropped.** A writer emitting the AIGER 1.9 extended
header moves the primary outputs into the bad-state section; ABC does this for
any design with a non-zero latch initialization. `on_bad_state` was not
overridden, so those outputs disappeared and the network came back with none.
They are now kept as primary outputs, which is what they were, along with their
names from the symbol table.

Also fix an adjacent defect in the destructor: the output index only advanced for
outputs that carried a name, so with a sparse symbol table every name after the
first unnamed output landed on the wrong one.

Verified end to end against ABC. A network with three registers initialized to
0, 1, and undefined previously returned with no outputs and a corrupted first
register; it now round-trips through `resyn2` with its inputs, outputs,
registers, and reset values intact.

Note that AIGER constraints are still ignored. They are assumptions rather than
outputs, so mapping them onto primary outputs would change the meaning of the
network.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbUqcnZayPooFgekEXVSjz
marcelwa and others added 2 commits July 29, 2026 00:11
`register_t::init` carried three different encodings of "undefined" at once.
`register_t` itself defaulted to 3, `blif_reader` used 2 for a nondeterministic
latch and 3 for an unspecified one, and `aiger_reader` produced 255 -- the
result of narrowing an `int8_t` of -1 into a `uint8_t` field rather than a
deliberate choice.

That last one corrupted output. `write_blif` emits the initialization verbatim
and the BLIF `.latch` statement accepts only 0, 1, 2, and 3, so a sequential
AIGER file with an undefined latch reset read back and written as BLIF produced

    .latch li0 new_n2   255

which no BLIF consumer accepts, ABC included.

Introduce `register_init` with the four documented values, following the BLIF
`.latch` field since it is the most expressive of the supported formats, and use
it consistently across the readers and writers. AIGER has no counterpart for
`unknown`, so a latch without a defined reset maps to `dont_care`.

`register_init::is_defined` expresses the test that callers actually want,
namely whether a reset value is 0 or 1, so code stays correct if a format ever
introduces further undefined states. `register_init::sanitize` keeps `write_blif`
from emitting a value the format cannot represent.

The only value that changes is the one AIGER produced for a nondeterministic
latch, from 255 to 2. Comparisons of the form `init > 1` are unaffected; only
code testing against 255 would notice, and that value was never intentional.

Verified end to end: the AIGER file above now yields `.latch li0 new_n2   2`,
which ABC reads back as one don't-care-initialized latch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbUqcnZayPooFgekEXVSjz
`write_aiger` hardcoded a latch count of 0 and asserted that the network was
combinational, so `sequential<aig_network>` could be passed in but silently
lost its registers: they degraded into extra PI/PO pairs.

Emit registers as AIGER latches when the network type provides `num_registers`,
`foreach_ri`, and `foreach_ro`. The combinational path is unchanged and still
produces byte-identical output.

Reset values are always written explicitly. An omitted reset field means 0 in
the AIGER format, so omitting it would silently turn an uninitialized register
into a zero-initialized one; an undefined reset is instead encoded by repeating
the latch's own current-state literal, as the format prescribes. Latch names are
emitted as `l<n>` symbol table entries, mirroring `on_latch_name`.

Because the binary format encodes CIs implicitly, PIs must be variables 1..I and
register outputs I+1..I+L. That invariant is now checked by a debug assertion
rather than being silently assumed.

Also add the missing `<cassert>` include; the header used `assert` but only
compiled because its includers happened to provide it.

Verified against both readers: the output round-trips through `aiger_reader`
preserving register count, reset values, and names, and ABC reads it back with
the expected `Init0`/`Init1`/`InitDC` counts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbUqcnZayPooFgekEXVSjz
@marcelwa
marcelwa force-pushed the upstream-seq-write-aiger branch from ac6759d to 0358a0f Compare July 28, 2026 22:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant