Ir al contenido

Guía de Extensión

Esta guía explica cómo extender Achronyme con nuevas instrucciones IR, funciones integradas de circuito, pases de optimización y funciones nativas de la VM.

En ir/src/types.rs, agrega una nueva variante al enum Instruction:

pub enum Instruction {
// ... variantes existentes ...
MyOp { result: SsaVar, operand: SsaVar },
}

En el mismo archivo, actualiza tres métodos:

// result_var() — devuelve la variable resultado
Instruction::MyOp { result, .. } => *result,
// has_side_effects() — true si la instrucción no debe eliminarse
// Agregar al matches! si tiene efectos secundarios
// operands() — devuelve todas las variables SSA de entrada
Instruction::MyOp { operand, .. } => vec![*operand],

En ir/src/lower.rs, maneja la nueva instrucción en el método apropiado (ej., lower_call para funciones integradas, lower_expr para operadores):

"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))
}

En ir/src/eval.rs, maneja la instrucción en la función evaluate:

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

Plegado de constantes (ir/src/passes/const_fold.rs): Agrega lógica de plegado si la operación puede calcularse en tiempo de compilación sobre constantes.

Eliminación de código muerto (ir/src/passes/dce.rs): Si la instrucción es pura (sin efectos secundarios), agrégala al conjunto eliminable. Si tiene efectos secundarios, mantenla conservadora.

Propagación booleana (ir/src/passes/bool_prop.rs): Si el resultado es siempre booleano, agrégalo como semilla.

En compiler/src/r1cs_backend.rs, agrega un brazo de match en compile_ir:

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

En compiler/src/plonkish_backend.rs, agrega un brazo de match en compile_ir:

Instruction::MyOp { result, operand } => {
let val = self.materialize_val(/* ... */)?;
// Emitir filas de puerta...
self.vals.insert(*result, PlonkVal::Cell(cell));
}

Si la instrucción asigna variables intermedias, registra un WitnessOp en compiler/src/witness_gen.rs.

Las funciones integradas de circuito son funciones de alto nivel que bajan a una o más instrucciones IR.

No se necesitan cambios — el parser ya maneja llamadas a funciones genéricamente.

En ir/src/lower.rs, agrega un brazo de match en 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))
}

Sigue los pasos 4-8 de “Agregar una Nueva Instrucción IR” arriba.

Crea ir/src/passes/my_pass.rs:

use crate::types::{IrProgram, Instruction, SsaVar};
pub fn my_pass(program: &mut IrProgram) {
// Recorrer program.instructions y transformar
}

En ir/src/passes/mod.rs, agrega el módulo y llámalo desde 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); // nuevo pase
}
  • Los pases hacia adelante (como const_fold, bool_prop) iteran program.instructions de frente a atrás, construyendo estado
  • Los pases hacia atrás (como dce) iteran de atrás a frente, rastreando qué resultados se usan
  • Los pases deben ser O(n) en conteo de instrucciones
  • Evita comportamiento cuadrático — usa HashMap<SsaVar, ...> para búsquedas

En vm/src/stdlib/core.rs, agrega la implementación:

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

La firma siempre es fn(&mut VM, &[Value]) -> Result<Value, RuntimeError>.

En vm/src/specs.rs, agrega una entrada a NATIVE_TABLE:

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

En el mismo archivo, actualiza la constante:

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

La aserción en tiempo de compilación detectará cualquier discrepancia.

En vm/src/machine/native.rs, agrega el brazo de match en bootstrap_natives:

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

En vm/src/opcode.rs, agrega una constante:

pub const MY_OP: u8 = /* siguiente número disponible */;

En compiler/src/compiler.rs, emite el opcode durante la compilación:

// En el método de compilación apropiado
self.emit(opcode::MY_OP, a, b, c);

En vm/src/machine/vm.rs, agrega un brazo de match en interpret_inner:

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

En el compilador de bytecode, asegúrate de que current_line esté establecido antes de emitir:

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

Esto habilita el rastreo de ubicación de errores ([line N] in func: Error).

Al agregar una nueva instrucción o función integrada, verifica:

  • ir/src/types.rs — enum Instruction + result_var + has_side_effects + operands
  • ir/src/lower.rs — emitir instrucción desde AST
  • ir/src/eval.rs — evaluación concreta
  • ir/src/passes/const_fold.rs — plegar sobre constantes (si aplica)
  • ir/src/passes/dce.rs — marcar como eliminable o conservadora
  • ir/src/passes/bool_prop.rs — propagar booleano (si aplica)
  • compiler/src/r1cs_backend.rs — generación de restricciones R1CS
  • compiler/src/plonkish_backend.rs — emisión de puertas Plonkish
  • compiler/src/witness_gen.rs — computación de testigo (si hay cables intermedios)
  • Pruebas en todos los crates relevantes