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

Extension Guide

How to add new instructions, builtins, and optimization passes.

This guide explains how to extend Achronyme with new IR instructions, circuit builtins, optimization passes, and VM native functions.

Adding a New IR Instruction

1. Define the variant

In ir/src/types.rs, add a new variant to the Instruction enum:

pub enum Instruction {
    // ... existing variants ...
    MyOp { result: SsaVar, operand: SsaVar },
}

2. Implement trait methods

In the same file, update three methods:

// result_var() — return the result variable
Instruction::MyOp { result, .. } => *result,

// has_side_effects() — true if the instruction must not be eliminated
// Add to the matches! if it has side effects

// operands() — return all input SSA variables
Instruction::MyOp { operand, .. } => vec![*operand],

3. Emit from IR lowering

In ir/src/lower.rs, handle the new instruction in the appropriate method (e.g., lower_call for builtins, lower_expr for operators):

"my_op" => {
    let arg = self.lower_expr(args[0])?;
    let result = self.program.fresh_var();
    self.program.push(Instruction::MyOp { result, operand: arg });
    Ok(EnvValue::Scalar(result))
}

4. Add evaluation logic

In ir/src/eval.rs, handle the instruction in the evaluate function:

Instruction::MyOp { result, operand } => {
    let val = values[operand];
    let out = /* compute result */;
    values.insert(*result, out);
}

5. Handle in optimization passes

Constant folding (ir/src/passes/const_fold.rs): Add folding logic if the operation can be computed at compile time on constants.

Dead code elimination (ir/src/passes/dce.rs): If the instruction is pure (no side effects), add it to the eliminable set. If it has side effects, keep it conservative.

Boolean propagation (ir/src/passes/bool_prop.rs): If the result is always boolean, add it as a seed.

6. Compile to R1CS

In compiler/src/r1cs_backend.rs, add a match arm in compile_ir:

Instruction::MyOp { result, operand } => {
    let lc_op = self.vars[operand].clone();
    // Build constraints...
    let lc_result = /* ... */;
    self.vars.insert(*result, lc_result);
}

7. Compile to Plonkish

In compiler/src/plonkish_backend.rs, add a match arm in compile_ir:

Instruction::MyOp { result, operand } => {
    let val = self.materialize_val(/* ... */)?;
    // Emit gate rows...
    self.vals.insert(*result, PlonkVal::Cell(cell));
}

8. Add witness generation

If the instruction allocates intermediate variables, record a WitnessOp in compiler/src/witness_gen.rs.

Adding a Circuit Builtin

Circuit builtins are high-level functions that lower to one or more IR instructions.

1. Add the name to the parser

No changes needed — the parser already handles function calls generically.

2. Handle in IR lowering

In ir/src/lower.rs, add a match arm in lower_call:

"my_builtin" => {
    if args.len() != 2 {
        return Err(IrError::WrongArgumentCount { ... });
    }
    let a = self.lower_expr_scalar(&args[0])?;
    let b = self.lower_expr_scalar(&args[1])?;
    let result = self.program.fresh_var();
    self.program.push(Instruction::MyOp { result, lhs: a, rhs: b });
    Ok(EnvValue::Scalar(result))
}

3. Add to evaluator, passes, and backends

Follow steps 4-8 from “Adding a New IR Instruction” above.

Adding an Optimization Pass

1. Create the pass file

Create ir/src/passes/my_pass.rs:

use crate::types::{IrProgram, Instruction, SsaVar};

pub fn my_pass(program: &mut IrProgram) {
    // Walk program.instructions and transform
}

2. Register in the pass manager

In ir/src/passes/mod.rs, add the module and call it from optimize:

mod my_pass;

pub fn optimize(program: &mut IrProgram) {
    const_fold::const_fold(program);
    dce::dce(program);
    bool_prop::bool_prop(program);
    my_pass::my_pass(program);  // new pass
}

Pass guidelines

  • Forward passes (like const_fold, bool_prop) iterate program.instructions front-to-back, building up state
  • Backward passes (like dce) iterate back-to-front, tracking which results are used
  • Passes should be O(n) in instruction count
  • Avoid quadratic behavior — use HashMap<SsaVar, ...> for lookups

Adding a VM Native Function

1. Implement the function

In vm/src/stdlib/core.rs, add the implementation:

pub fn native_my_func(vm: &mut VM, args: &[Value]) -> Result<Value, RuntimeError> {
    let arg = args[0];
    // ... process ...
    Ok(Value::int(result))
}

The signature is always fn(&mut VM, &[Value]) -> Result<Value, RuntimeError>.

2. Register in the dispatch table

In vm/src/specs.rs, add an entry to NATIVE_TABLE:

pub const NATIVE_TABLE: &[NativeSpec] = &[
    // ... existing entries ...
    NativeSpec { name: "my_func", arity: 1 },
];

3. Update NATIVE_COUNT

In the same file, update the constant:

pub const NATIVE_COUNT: usize = 24;  // was 23

The compile-time assertion will catch any mismatch.

4. Map in bootstrap

In vm/src/machine/native.rs, add the match arm in bootstrap_natives:

"my_func" => stdlib::core::native_my_func,

Adding a VM Opcode

1. Define the opcode

In vm/src/opcode.rs, add a constant:

pub const MY_OP: u8 = /* next available number */;

2. Emit in the bytecode compiler

In compiler/src/compiler.rs, emit the opcode during compilation:

// In the appropriate compilation method
self.emit(opcode::MY_OP, a, b, c);

3. Handle in the VM interpreter

In vm/src/machine/vm.rs, add a match arm in interpret_inner:

opcode::MY_OP => {
    let a = inst_a!(inst);
    let b = inst_b!(inst);
    // ... execute ...
}

4. Track line info

In the bytecode compiler, ensure current_line is set before emitting:

self.current_line = node.line as u32;
self.emit(opcode::MY_OP, a, b, c);

This enables error location tracking ([line N] in func: Error).

Checklist

When adding a new instruction or builtin, verify:

  • ir/src/types.rsInstruction enum + result_var + has_side_effects + operands
  • ir/src/lower.rs — emit instruction from AST
  • ir/src/eval.rs — concrete evaluation
  • ir/src/passes/const_fold.rs — fold on constants (if applicable)
  • ir/src/passes/dce.rs — mark as eliminable or conservative
  • ir/src/passes/bool_prop.rs — propagate boolean-ness (if applicable)
  • compiler/src/r1cs_backend.rs — R1CS constraint generation
  • compiler/src/plonkish_backend.rs — Plonkish gate emission
  • compiler/src/witness_gen.rs — witness computation (if intermediate wires)
  • Tests in all relevant crates
Navigation