Skip to content

BINIUS-247: deduplicate the circuit interpreters via a shared execution-context trait#1753

Open
tcoratger wants to merge 2 commits into
binius-zk:mainfrom
tcoratger:thomascoratger/dedup-circuit-interpreters
Open

BINIUS-247: deduplicate the circuit interpreters via a shared execution-context trait#1753
tcoratger wants to merge 2 commits into
binius-zk:mainfrom
tcoratger:thomascoratger/dedup-circuit-interpreters

Conversation

@tcoratger

Copy link
Copy Markdown
Contributor

Part of BINIUS-247 — the second checklist item ("deduplicate BatchInterpreter and the single-instance Interpreter via a shared trait over the eval/execution context"). That issue has several bullet points; this is one of several PRs and does not close it.

Merges the two circuit interpreters into one shared executor. Pure refactor — no behavior change.

The problem

A circuit is a little bytecode program: AND two words, shift one right by 7, assert two are equal. Something has to run it to fill in every wire value.

There were two runners doing exactly that:

  • single-instance: runs the program once, on one value vector.
  • batched: runs the same program on many instances at once (the data-parallel prover path).

They did the same thing opcode for opcode. The ~40-branch dispatch, every operation handler, and the bytecode readers all existed twice — differing only in where a decoded instruction reads and writes.

That is a maintenance trap: touch one opcode and you must edit both copies identically, or the batched prover silently disagrees with the single-instance one.

The idea

Look at what actually differs between the two. It is tiny:

                read a value        write a value
single:         one value vector    one value vector
batched:        column i of a       column i of a
                wide table          wide table

The operation logic is identical. Only "where do I read/write" differs.

So put that one difference behind a small trait over the execution context:

  • how many instances?
  • read register r of instance i?
  • write register r of instance i?
  • note an assertion failure for instance i?

Then write the opcode logic once, as "for each instance i, apply the operation". The single-instance interpreter is just the case where there is one instance.

Per opcode

Take AND dst, src1, src2.

// before — two hand-written copies
single:   dst        = src1 & src2
batched:  for i:  dst[i] = src1[i] & src2[i]

// after — one copy, parameterized by the context
for i in 0..ctx.n_instances():
    v = ctx.load(src1, i) & ctx.load(src2, i)
    ctx.store(dst, i, v)
  • single context: n_instances() == 1, so the loop runs once and reads/writes the lone value vector. The 0..1 loop inlines away.
  • batched context: n_instances() is the column count, so the loop covers every instance.

Same source line, both behaviors — chosen purely by which context is handed in.

The change

- interpreter.rs:      own copy of the dispatch + all exec_* handlers + readers
- batch_interpreter.rs: own copy of the dispatch + all exec_* handlers + readers
+ exec.rs:             the dispatch + all exec_* handlers + readers, once, over a trait
  • The single-instance interpreter and the batched interpreter are now thin wrappers that build a context and hand it to the shared executor.
  • Net: +630 / -966 lines across the four files.

Soundness / behavior preservation

This is a pure refactor; two details are load-bearing and were kept identical:

  • Assertion diagnostics. The failure message text and the blamed instance index are byte-for-byte the same. The batched context adds its stripe's global offset; the single context ignores the index.
  • Hint reads. The one opcode that calls out to external hint code now reads its output registers up front (the batched order) instead of interleaved. Same bytes consumed in the same order, so behavior is unchanged.

The batched-machine-code path is unchanged (same monomorphization). The single-instance path gains a 0..1 loop that the compiler elides.

Scope

  • new: crates/frontend/src/compiler/eval_form/exec.rs — the shared trait and executor.
  • changed: interpreter.rs and batch_interpreter.rs reduced to wrappers; mod.rs registers the new module.
  • left untouched: the public API of both interpreters, the bytecode format, and the equivalence tests.

Verification

  • cargo check -p binius-frontend --all-targets — clean
  • cargo clippy -p binius-frontend --all-targets — clean
  • cargo +nightly fmt --all -- --check — clean
  • cargo test -p binius-frontend — 161 + 2 pass, including the batched-equals-single equivalence test and the assertion-path tests
  • cargo test -p binius-m4-prover value_table2 — 9 pass (real-circuit batch witness generation, parallel stripes, failure diagnostics)

Note: cargo check --workspace --all-targets fails on evaluate_batched_parallel (a rayon feature-unification quirk with into_par_strides). This reproduces on a clean main and is unrelated to this change, so it is left alone here.

🤖 Generated with Claude Code

…on-context trait

The single-instance and batched circuit interpreters ran the same bytecode
through the same ~40-opcode dispatch, every operation handler, and the same
bytecode readers. All of it was written twice, differing only in where a
decoded instruction reads and writes: one value vector versus one column per
instance. Changing an opcode meant editing both copies identically or the two
provers would silently disagree.

Name the one thing that differs behind a small trait over the execution
context: how many instances, read register r of instance i, write register r
of instance i, note an assertion failure for instance i. Write the opcode
logic once, as "for each instance, apply the operation". The single-instance
interpreter becomes the degenerate case of one instance; the batched one is the
many-column case. Both interpreters are now thin wrappers over the shared
executor.

Pure refactor, no behavior change: the assertion messages and blamed-instance
index are byte-identical, and the batched-equals-single equivalence test is
untouched and still passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tcoratger tcoratger requested a review from jimpo as a code owner July 9, 2026 20:51

@jimpo jimpo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

Comment thread crates/frontend/src/compiler/eval_form/exec.rs Outdated
The exec module is private, so a private ancestor already gates external
reachability; per CONTRIBUTING, prefer plain pub over pub(crate) in that case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tcoratger

Copy link
Copy Markdown
Contributor Author

Thanks!

Just fixed the comment

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.

2 participants