Introducing Achronyme — a language for zero-knowledge proofs. Read the announcement arrow_right_alt

Lysis VM (in-progress)

Third VM: structural template instantiation with hash-consing. Status: Phase 3.B shipped.

Status

Lysis is in-progress. Phases 0–2 + 3.A + 3.B + 3.C.6 P1 + P1.5 are shipped (as of 2026-04-24). Phase 3.C (oracle gate, 7 fixtures × 3 fields, 38 circom E2E via Lysis path, SHA-256(64) HARD GATE) is the next deliverable.

This page is subject to change — opcode numbers and sink semantics may evolve before Phase 3.C closes. Treat it as the current state of the implementation, not a stable contract.

Why Lysis exists

Akron and Artik both execute once per proof. Lysis is different: it separates compile/instantiate from runtime emission. The walker emits a Lysis program that, when run, produces SSA IR — and the executor’s interning sink collapses structurally identical sub-trees during emission.

Concretely: the legacy ir::prove_ir::instantiate (~3,025 LOC) eagerly expands templates and unrolls loops. Lysis defers that expansion to runtime and dedupes redundant sub-graphs via hash-consing — a 58.3× constraint reduction was measured on the SHA-256(64) skeleton in Phase 2.

Where Lysis sits

ProveIR


ir-forge::lysis_lift::walker  ── BTA + extract + diff + symbolic ──▶  Lysis bytecode (.lysis)


                                                              lysis::execute (interpreter)


                                                              InterningSink (hash-cons)


                                                              Vec<Instruction<F>>  (IR-SSA)


                                                                          R1CS / Plonkish

The walker is the IR-side lifter: it consumes ExtendedInstruction<F> (from ir-forge) and produces Lysis bytecode. The executor is the VM side: it runs that bytecode and emits SSA via a sink.

Lift pipeline (ir-forge::lysis_lift)

File: crates/ir-forge/src/lysis_lift/mod.rs. Five phases:

PhaseModuleDeliverable
Symbolic walksymbolic.rsSymbolicTree with placeholder slots for loop-var derivatives
Structural diffdiff.rsDiff with AST-path slot identity (3.B.4)
Binding-Time Analysisbta.rs3-point classification (Constant / Invariant / DataDependent)
Template extractionextract.rsLambda lifting + frame size, capture layout
Walker (driver)walker.rsOrchestrates the full lift pass

All five implement RFC §6 (Lysis Phase 3.B) with deferred evaluation — templates remain symbolic until proof-time.

Bytecode header

0..4    magic "AALSS"  (0xAA 0x4C 0x53 0x53)
4..5    version (u8)
5..6    field_family (u8)        — BnLike256 | Goldilocks64
6..…    const_pool (FieldConst entries + bytecode blobs for Artik calls)
…..     body (instruction stream)

Sections beyond the body are TBD — Phase 3 may add a template-table section.

Register / frame model

  • Registers: 256 per frame, stored as Vec<Option<FieldElement<F>>> (undefined until written).
  • Frame stack: Vec<Frame> with pc, body_start_idx, body_end_idx, template_id, loop_stack: Vec<LoopState>.
  • Loop state: each LoopUnroll pushes a LoopState tracking iteration bounds; the dispatcher’s advance_loops() step increments and detects completion.

Opcode table (29 opcodes)

Subject to change. File: crates/lysis/src/bytecode/opcode.rs.

CodeMnemonicOperandsSemantics
0x01LoadCapturedst, idxR[dst] = captures[idx]
0x02LoadConstdst, idxR[dst] = const_pool[idx]
0x03LoadInputdst, name_idx, visR[dst] = inputs[name_idx] (Public/Witness)
0x04EnterScopescope_depth += 1
0x05ExitScopescope_depth −= 1
0x10JumpoffsetPC += offset
0x11JumpIfcond, offsetconditional
0x12Returnpop frame
0x13Haltend execution
0x14Trapcodeabort
0x20LoopUnrolliter_var, start, end, body_leninline body N times
0x21LoopRollediter_var, start, end, template_idcall template repeatedly (Phase 4+)
0x22LoopRangeiter_var, R(end), template_iddynamic loop (Phase 4+)
0x30DefineTemplatetemplate_id, frame_size, n_params, body_offset, body_lenregister template metadata
0x31InstantiateTemplatetemplate_id, capture_regs, output_regspush frame + call
0x32TemplateOutputoutput_idx, srcpublish output value
0x40EmitConstdst, srcemit Const(R[src])
0x41EmitAdddst, lhs, rhsemit Add
0x42EmitSubdst, lhs, rhsemit Sub
0x43EmitMuldst, lhs, rhsemit Mul
0x44EmitNegdst, srcemit Neg
0x45EmitMuxdst, cond, t, femit Mux
0x46EmitDecomposedst_arr, src, n_bitsemit Decompose
0x47EmitAssertEqlhs, rhsemit AssertEq
0x48EmitRangeCheckvar, max_bitsemit RangeCheck
0x49EmitWitnessCallbytecode_idx, in_regs, out_regsemit WitnessCall (Artik)
0x4AEmitPoseidonHashdst, in_regsemit PoseidonHash
0x4BEmitIsEqdst, lhs, rhsemit IsEq
0x4CEmitIsLtdst, lhs, rhsemit IsLt
0x4DEmitIntDivdst, lhs, rhs, max_bitsemit IntDiv (Phase P1.5)
0x4EEmitIntModdst, lhs, rhs, max_bitsemit IntMod (Phase P1.5)

Emit* opcodes don’t compute results — they push entries into the sink.

Sinks

The dispatcher writes IR-emitting opcodes into a IrSink trait object. Two implementations exist:

  • StubSink — no-op (Phase 1, used for skeleton tests)
  • InterningSink — hash-cons via IndexMap<NodeKey, NodeId>. Phase 2 default. Materialises a flat Vec<Instruction<F>> via sink.materialize().

Future: Phase 4 may add a RecordingSink for the oracle gate.

Dispatcher loop

File: crates/lysis/src/execute/mod.rs:77-179. Outer loop over frames + inner step:

loop {
    check_instruction_budget()?;
    let instr = program.body[frame.pc];
    match dispatch(instr, ...)? {
        Step::Next => frame.pc += 1,
        Step::JumpToIndex(idx) => frame.pc = idx,
        Step::PushFrame(f) => stack.push(f),
        Step::PopFrame => stack.pop(),
        Step::Halt => break,
    }
    advance_loops()?;
}

Memory model

No GC. Frame registers live in the frame struct. The interner internally uses IndexMap<NodeKey, NodeId> borrowed mutably during execution.

Lift output → walker

File: crates/ir-forge/src/lysis_lift/walker.rs. Driver that consumes Vec<ExtendedInstruction<F>>:

VariantWalker action
Plain(ir::Instruction)emit corresponding Emit* opcode
LoopUnroll { iter_var, start, end, body }emit LoopUnroll opcode
TemplateCall {…}Phase 4+ — currently rejected
TemplateBody {…}Phase 4+ — currently rejected

Oracle (Phase 3.C scaffold)

File: crates/zkc/src/lysis_oracle/. Compares Lysis-emitted R1CS against circom-direct R1CS via:

  1. canonicalize_ssa — topological renaming of SSA vars
  2. semantic_equivalence — multiset constraint comparison + witness solver

Phase 3.C must pass: 7 fixtures × 3 fields = 21 oracle cells, all green.

SHA-256(64) HARD GATE

The blocking benchmark is Decompose(254) in Num2Bits_strict for SHA bit-array slicing.

  • Phase 2 (cleared 2026-04-20): interner achieved 58.3× constraint reduction vs eager unroll.
  • Phase 3 gate: SHA-256(64) wallclock < 60s, constraint count within ±5% of circom O2.

Phase status snapshot

PhaseDateDeliverable
0–22026-04-20Tri-color mark-sweep + hash-cons interner + BTA classifier (HARD GATE cleared)
3.A2026-04-20ExtendedInstruction schema + bridge (ir-forge → lysis dependency)
3.B2026-04-21Lysis-leaf primitives, IR-side lifter, LoopUnroll unblocked
3.C.6 P12026-04-23Walker scaffolding, 24/24 walker tests
P1.52026-04-24Mechanical top-level template split, IntDiv/IntMod opcodes, lazy one
3.C (next)TBDOracle gate (7 fixtures × 3 fields), 38 circom E2E, SHA-256(64) under gate

Source files

ComponentFile
Bytecode opcodescrates/lysis/src/bytecode/opcode.rs
Encodingcrates/lysis/src/bytecode/encoding.rs
Validatorcrates/lysis/src/bytecode/validate.rs
Const poolcrates/lysis/src/bytecode/const_pool.rs
Dispatchercrates/lysis/src/execute/mod.rs
Framecrates/lysis/src/execute/frame.rs
Interning sinkcrates/lysis/src/execute/interning_sink.rs
Internercrates/lysis/src/intern/interner.rs
Buildercrates/lysis/src/builder.rs
Walker (lift)crates/ir-forge/src/lysis_lift/walker.rs
BTA classifiercrates/ir-forge/src/lysis_lift/bta.rs
Symbolic treecrates/ir-forge/src/lysis_lift/symbolic.rs
Structural diffcrates/ir-forge/src/lysis_lift/diff.rs
Template extractioncrates/ir-forge/src/lysis_lift/extract.rs
Oracle scaffoldcrates/zkc/src/lysis_oracle/

See ProveIR for the layer feeding Lysis. See Akron VM and Artik Witness VM for the other two VMs.

Navigation