Skip to content

fix: keep bit widths consistent across connections and components - #78

Merged
Rn86222 merged 8 commits into
mainfrom
fix/bit-width-consistency
Nov 14, 2026
Merged

Rn86222 merged 8 commits into
mainfrom
fix/bit-width-consistency

Conversation

@Rn86222

@Rn86222 Rn86222 commented Nov 14, 2026

Copy link
Copy Markdown
Contributor

Summary

On main, a connection whose two ends came to disagree on their bit width could survive, and the simulation then silently passed values of the wrong width. Two cases were missed:

  • Resizing a split pin of an Aggregate node changes the width of its output, but the connection on that output was kept.
  • A change inside a component that changed the width of one of its pins was not re-checked in the components using it, because the didUpdateConfig handler in connection.ts only looked inside the same component.

An earlier attempt re-checked every connection after each change and dropped the inconsistent ones. It was abandoned for two reasons:

  • It was slow: the cost grew with the cube of the number of stages.
  • It dropped valid connections. The inference assumed that every auto-width pin of a user-defined node shares one width, so two independent paths through a component were forced to the same width.

Design

Bit width groups (src/store/bitWidth.ts). Within a component, pins share a group when

  • a connection ties them,
  • they are inferred pins of the same intrinsic node (e.g. A, B and Out of And), or
  • they are pins of the same user-defined node whose implementations share a group inside that component.

Pins of independent paths through a component therefore stay apart. Some pins fix their own width: configurable pins, calculated pins, and pins of user-defined nodes whose implementation group is fixed. A group takes the width its pins fix, and is in conflict when they fix more than one. The groups of a component are computed in one pass with union-find and cached until the circuit changes.

Conflicts are kept, not dropped. There is no undo, and a later change can resolve a conflict, so a connection in a conflicting group stays and is drawn red and dashed.

  • A new connection that would bring a new width into a conflict is refused.
  • A pin that fixes no width may still join a conflicting group, e.g. a node appended to probe it. It takes whatever width the group settles on.
  • A connection is only checked against the component it is drawn in. A connection inside a component can still cause a conflict where the component is used, just as a config change inside it can.

Simulation.

  • The netlist hands each group inside a component instance the width given by the pins of the node using it, instead of one width for the whole instance.
  • Groups that receive no width fall back to the default (1 bit).
  • A component with a conflict is refused with SimulationBitWidthConflictError.
  • At run time, every value an instance reads or writes is checked against the width of its pin.

Behavior changes

  • A connection that used to be dropped on a width change is now kept and highlighted.
  • Circuits saved while the bug was present open with their inconsistent connections highlighted.
  • A path through a component that is independent of the others no longer inherits their width.
    • Example: a component has In → Out plus an independent True → Not → B, and is used with an 8-bit input. B is now 1 bit; it used to be 8.
  • "Create a new component" moves a connection in conflict as it is. It used to drop it.

Performance

Measured on a chain of a Const and 100 Not nodes, as in the review of the abandoned attempt:

Operation Abandoned attempt This PR
Add a node 207 ms 0.4 ms
Delete 5 nodes 1.4 s 1.2 ms

Testing

  • npx vitest run: 56 tests pass. New tests in src/store/bitWidth.test.ts cover:
    • config and split pin changes
    • a change inside a component reaching the components using it
    • independent paths
    • reuse of a component at different widths, two levels deep
    • which connections are refused while a conflict exists
    • queries made in the middle of a removal
    • the simulation errors
  • npx tsc -p . and npx biome check src pass.
  • Checked by hand in the browser:
    • conflicting connections turn red and recover when the width is restored
    • resized aggregate pins
    • a width change inside a component reaching the outside
    • independent paths
    • the simulation error
    • appending a node to a conflicting net

🤖 Generated with Claude Code

Rn86222 and others added 8 commits November 14, 2026 12:02
The store counted its revision on a hand-written list of events, and
`useCanSimulate` subscribed to a similar but slightly different list.
Expose that list once as `subscribeToCircuitChanges`, so that anything
derived from the whole circuit listens to exactly the changes the
revision counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Connections whose two ends came to disagree on their bit width were only
dropped on a config change, and only inside the component of the node.
They survived when a split pin of an aggregate node was resized, or when
a change inside a component changed the width of one of its pins, so the
simulation silently fed values of the wrong width onward.

Dropping them everywhere was tried before and rejected: re-checking every
connection after each change grew with the cube of the circuit size, and
it dropped valid connections. The inference assumed that every pin of a
user-defined node whose width is not fixed shares one width, so two
independent paths through a component were forced to the same width.

Group the pins instead. Within a component, pins share a bit width group
when a connection ties them, when they are inferred pins of one intrinsic
node, or when they are pins of one user-defined node whose
implementations share a group inside it. A group takes the width its pins
fix, and is in conflict when they fix more than one. The groups of a
component are computed in one pass and cached until the circuit changes.

A connection in a conflicting group is kept, since there is no undo and a
later change can resolve the conflict. A new connection that would cause
a conflict is still refused. The netlist gives each group of a component
instance the width the pins of the node using it hand over, instead of a
single width for the whole instance, and refuses to simulate a component
with a conflict.

In a chain of a constant and 100 not nodes, adding a node now takes
0.4 ms and deleting five 1.2 ms, against 207 ms and 1.4 s for the
rejected re-check of every connection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The netlist refuses a circuit whose connected pins disagree on their bit
width, but a value can still come out wrong: an intrinsic node can
compute a value of the wrong width, and the caller can supply an input
value that does not fit its pin. Either used to flow on silently. Check
the width of every value an instance reads or writes, and name the pin
in the error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Connections whose tied pins fix different bit widths are now kept rather
than dropped, so the editor has to show them. Draw them red and dashed,
and mention the conflict among the reasons the simulation can fail.

The conflict can be caused or resolved by a change anywhere in the
circuit, such as inside a component used by a node at one end, so the
renderer re-checks it on every circuit change. The check reads a cached
analysis, so that costs a lookup per connection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ponent

"Create a new component" copies the selected connections into the new
component and then removes the originals. The copies went through the
bit width check, so a connection in conflict, which is now kept instead
of dropped, was refused as a copy while its original was still removed.

Let `register` keep a connection in conflict when asked to, and use that
for the copies.

Also document that a connection is only checked against the component
it is drawn in, so one inside a component can still cause a conflict in
the components using it, like a config change inside it can.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Collect the inputs of an instance outside the `try` that prefixes
  evaluation errors with the instance, so a misfit input no longer names
  the instance twice.
- Update the description of `defaultBitWidth` of `simulateComponent` to
  match the netlist.
- Stop exporting `requireManualBitWidth`, which only its module uses.
- Note that `importJson` must run before `mount`, since some stores add
  the objects without announcing them and so without clearing the bit
  widths derived from them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A component used twice at different widths, two levels deep.
- An input value as wide as the pins tied to its input pin.
- Bit width queries on every change event while objects are removed.
- A connection inside a component causing a conflict where it is used.
- A connection in conflict copied as it is only when asked to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A connection touching a group in conflict was always refused, so a
conflicting net could not be extended or probed until it was fixed.

Allow a connection whose other side fixes no width. Those pins take
whatever width the group settles on, so fixing the original conflict
fixes them too. A pin fixing a width of its own is still refused, even
one the group already has, since it would stay in conflict if the
conflict were fixed the other way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying create-cpu-c8345196-9409-4cce-afc4-389f413ad8ee with  Cloudflare Pages  Cloudflare Pages

Latest commit: 6e55865
Status: ✅  Deploy successful!
Preview URL: https://7282a97e.create-cpu-c8345196-9409-4cce-afc4-389f413ad8ee.pages.dev
Branch Preview URL: https://fix-bit-width-consistency.create-cpu-c8345196-9409-4cce-afc4-389f413ad8ee.pages.dev

View logs

@Rn86222
Rn86222 merged commit 6db12e1 into main Nov 14, 2026
2 checks passed
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