/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2026 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
use std::{io::Result, str::Chars};
use darkfi_serial::{serialize, VarInt};
use super::{
ast::{Arg, Constant, Literal, Statement, StatementType, Witness},
constants::{
SECTION_CIRCUIT, SECTION_CONSTANT, SECTION_DEBUG, SECTION_LITERAL, SECTION_WITNESS,
},
error::ErrorEmitter,
types::HeapType,
};
/// Version of the binary
pub const BINARY_VERSION: u8 = 2;
/// Magic bytes prepended to the binary
pub const MAGIC_BYTES: [u8; 4] = [0x0b, 0x01, 0xb1, 0x35];
pub struct Compiler {
namespace: String,
k: u32,
constants: Vec,
witnesses: Vec,
statements: Vec,
literals: Vec,
debug_info: bool,
error: ErrorEmitter,
}
impl Compiler {
#[allow(clippy::too_many_arguments)]
pub fn new(
filename: &str,
source: Chars,
namespace: String,
k: u32,
constants: Vec,
witnesses: Vec,
statements: Vec,
literals: Vec,
debug_info: bool,
) -> Self {
// For nice error reporting, we'll load everything into a string
// vector so we have references to lines.
let lines: Vec = source.as_str().lines().map(|x| x.to_string()).collect();
let error = ErrorEmitter::new("Compiler", filename, lines);
Self { namespace, k, constants, witnesses, statements, literals, debug_info, error }
}
pub fn compile(&self) -> Result> {
let mut bincode = vec![];
// Write the magic bytes and version
bincode.extend_from_slice(&MAGIC_BYTES);
bincode.push(BINARY_VERSION);
// Write the circuit's k param
bincode.extend_from_slice(&serialize(&self.k));
// Write the circuit's namespace
bincode.extend_from_slice(&serialize(&self.namespace));
// Temporary heap vector for lookups
let mut tmp_heap = vec![];
// In the .constant section of the binary, we write the constant's type,
// and the name so the VM can look it up from `src/crypto/constants/`.
bincode.extend_from_slice(SECTION_CONSTANT);
for i in &self.constants {
tmp_heap.push(i.name.as_str());
bincode.push(i.typ as u8);
bincode.extend_from_slice(&serialize(&i.name));
}
// Currently, our literals are only Uint64 types, in the binary we'll
// add them here in the .literal section. In the VM, they will be on
// their own heap, used for reference by opcodes.
bincode.extend_from_slice(SECTION_LITERAL);
for i in &self.literals {
bincode.push(i.typ as u8);
bincode.extend_from_slice(&serialize(&i.name));
}
// In the .witness section, we write all our witness types, on the heap
// they're in order of appearance.
bincode.extend_from_slice(SECTION_WITNESS);
for i in &self.witnesses {
tmp_heap.push(i.name.as_str());
bincode.push(i.typ as u8);
}
bincode.extend_from_slice(SECTION_CIRCUIT);
for i in &self.statements {
match i.typ {
StatementType::Assign => tmp_heap.push(&i.lhs.as_ref().unwrap().name),
// In case of a simple call, we don't append anything to the heap
StatementType::Call => {}
_ => unreachable!("Invalid statement type in circuit: {:?}", i.typ),
}
bincode.push(i.opcode as u8);
bincode.extend_from_slice(&serialize(&VarInt(i.rhs.len() as u64)));
for arg in &i.rhs {
match arg {
Arg::Var(arg) => {
let heap_idx =
Compiler::lookup_heap(&tmp_heap, &arg.name).ok_or_else(|| {
self.error.abort(
&format!("Failed finding a heap reference for `{}`", arg.name),
arg.line,
arg.column,
)
})?;
bincode.push(HeapType::Var as u8);
bincode.extend_from_slice(&serialize(&VarInt(heap_idx as u64)));
}
Arg::Lit(lit) => {
let lit_idx = Compiler::lookup_literal(&self.literals, &lit.name)
.ok_or_else(|| {
self.error.abort(
&format!("Failed finding literal `{}`", lit.name),
lit.line,
lit.column,
)
})?;
bincode.push(HeapType::Lit as u8);
bincode.extend_from_slice(&serialize(&VarInt(lit_idx as u64)));
}
_ => unreachable!(),
};
}
}
// If we're not doing debug info, we're done here and can return.
if !self.debug_info {
return Ok(bincode)
}
// Otherwise, we proceed appending debug info.
bincode.extend_from_slice(SECTION_DEBUG);
// Write source locations for each opcode.
// This allows mapping runtime errors back to source lines.
bincode.extend_from_slice(&serialize(&VarInt(self.statements.len() as u64)));
for stmt in &self.statements {
bincode.extend_from_slice(&serialize(&VarInt(stmt.line as u64)));
// For column, use the lhs variable's column if available
let column = stmt.lhs.as_ref().map(|v| v.column).unwrap_or(0);
bincode.extend_from_slice(&serialize(&VarInt(column as u64)));
}
// Write heap variable names.
// The heap contains constants, witnesses, assigned variables (in order).
// This allows showing meaningful names instead of heap indices.
let heap_size = self.constants.len() +
self.witnesses.len() +
self.statements.iter().filter(|s| s.typ == StatementType::Assign).count();
bincode.extend_from_slice(&serialize(&VarInt(heap_size as u64)));
for constant in &self.constants {
bincode.extend_from_slice(&serialize(&constant.name));
}
for witness in &self.witnesses {
bincode.extend_from_slice(&serialize(&witness.name));
}
for stmt in &self.statements {
if stmt.typ == StatementType::Assign {
bincode.extend_from_slice(&serialize(&stmt.lhs.as_ref().unwrap().name));
}
}
// Write literal names (the literal values as strings, e.g. "42")
bincode.extend_from_slice(&serialize(&VarInt(self.literals.len() as u64)));
for literal in &self.literals {
bincode.extend_from_slice(&serialize(&literal.name));
}
Ok(bincode)
}
fn lookup_heap(heap: &[&str], name: &str) -> Option {
heap.iter().position(|&n| n == name)
}
fn lookup_literal(literals: &[Literal], name: &str) -> Option {
literals.iter().position(|n| n.name == name)
}
}