Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Builtin operators #197

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions examples/arithmetic.no
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,21 @@ fn main(pub public_input: Field, private_input: Field) {
let xx = private_input + public_input;
let yy = private_input * public_input;
assert_eq(xx, yy);

// modulus constant
let mod_res = (xx + yy) % 3;
assert_eq(mod_res, 2);

// modulus var
let mod_var_res = (xx + yy) % (private_input + 1);
assert_eq(mod_var_res, 2);

// divide constant
let div_res = xx / 2;
assert_eq(div_res, 2);

// divide var
let div_var_res = xx / private_input;
assert_eq(div_res, 2);

}
64 changes: 64 additions & 0 deletions src/backends/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,70 @@ pub trait Backend: Clone {

Ok(res)
}
Value::VarDivVar(dividend, divisor) => {
let dividend = self.compute_var(env, dividend)?.to_biguint();
let divisor = self.compute_var(env, divisor)?.to_biguint();
let res = Self::Field::from(dividend / divisor);

env.cached_values.insert(cache_key, res);
Ok(res)
}
Value::CstDivVar(dividend, divisor) => {
let divisor = self.compute_var(env, divisor)?;
let res = *dividend / divisor;
Ok(res)
}
Value::VarDivCst(dividend, divisor) => {
let dividend = self.compute_var(env, dividend)?;
// convert to bigint
let dividend = dividend.to_biguint();
let divisor = divisor.to_biguint();

let res = Self::Field::from(dividend / divisor);
env.cached_values.insert(cache_key, res);
Ok(res)
}
Value::CstDivCst(dividend, divisor) => {
let res = *dividend / *divisor;
env.cached_values.insert(cache_key, res);
Ok(res)
}
Value::VarModVar(dividend, divisor) => {
let dividend = self.compute_var(env, dividend)?;
let divisor = self.compute_var(env, divisor)?;
// convert to bigint
let dividend = dividend.to_biguint();
let divisor = divisor.to_biguint();
let res = Self::Field::from(dividend % divisor);

env.cached_values.insert(cache_key, res);
Ok(res)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

why not a single Value::Div(VarOrCst, VarOrCst)?

Value::CstModVar(dividend, divisor) => {
let divisor = self.compute_var(env, divisor)?;
// convert to bigint
let dividend = dividend.to_biguint();
let divisor = divisor.to_biguint();
let res = Self::Field::from(dividend % divisor);
env.cached_values.insert(cache_key, res);
Ok(res)
}
Value::VarModCst(dividend, divisor) => {
let dividend = self.compute_var(env, dividend)?;
// convert to bigint
let dividend = dividend.to_biguint();
let divisor = divisor.to_biguint();
let res = Self::Field::from(dividend % divisor);
env.cached_values.insert(cache_key, res);
Ok(res)
}
Value::CstModCst(dividend, divisor) => {
let dividend = dividend.to_biguint();
let divisor = divisor.to_biguint();
let res = Self::Field::from(dividend % divisor);
env.cached_values.insert(cache_key, res);
Ok(res)
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/circuit_writer/writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -619,14 +619,15 @@ impl<B: Backend> CircuitWriter<B> {
Op2::Addition => field::add(self, &lhs[0], &rhs[0], expr.span),
Op2::Subtraction => field::sub(self, &lhs[0], &rhs[0], expr.span),
Op2::Multiplication => field::mul(self, &lhs[0], &rhs[0], expr.span),
Op2::Modulus => field::modulus(self, &lhs[0], &rhs[0], expr.span),
Op2::Division => field::div(self, &lhs[0], &rhs[0], expr.span),
Op2::Equality => field::equal(self, &lhs, &rhs, expr.span),
Op2::Inequality => field::not_equal(self, &lhs, &rhs, expr.span),
// todo: refactor the input vars from Var to VarInfo,
// which contain the type to provide the info about the bit length
Op2::LessThan => field::less_than(self, None, &lhs[0], &rhs[0], expr.span),
Op2::BoolAnd => boolean::and(self, &lhs[0], &rhs[0], expr.span),
Op2::BoolOr => boolean::or(self, &lhs[0], &rhs[0], expr.span),
Op2::Division => todo!(),
};

Ok(Some(VarOrRef::Var(res)))
Expand Down
144 changes: 142 additions & 2 deletions src/constraints/field.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
use crate::{
backends::Backend,
circuit_writer::CircuitWriter,
circuit_writer::{CircuitWriter, VarInfo},
constants::Span,
parser::types::{GenericParameters, TyKind},
stdlib::bits::to_bits,
var::{ConstOrCell, Value, Var},
};

use super::boolean;

use ark_ff::{One, Zero};
use ark_ff::{Field, One, PrimeField, Zero};
use kimchi::o1_utils::FieldHelpers;

use std::ops::Neg;

Expand Down Expand Up @@ -99,6 +102,143 @@ pub fn mul<B: Backend>(
}
}

fn constrain_div_mod<B: Backend>(
compiler: &mut CircuitWriter<B>,
lhs: &ConstOrCell<B::Field, B::Var>,
rhs: &ConstOrCell<B::Field, B::Var>,
span: Span,
) -> (B::Var, B::Var) {
// to constrain lhs − q * rhs − rem = 0
// where rhs is the modulus
// so 0 <= rem < rhs

let one = B::Field::one();

// todo: to avoid duplicating a lot of code due the different combinations of the input types
// until we refactor the backend to handle ConstOrCell or some kind of wrapper that encapsulate the different variable types
// convert cst to var for easier handling
let lhs = match lhs {
ConstOrCell::Const(lhs) => compiler.backend.add_constant(
Copy link
Contributor

Choose a reason for hiding this comment

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

Note that we also wanted to remove this add_constant fn :p

Some("wrap a constant as var"),
*lhs,
span,
),
ConstOrCell::Cell(lhs) => lhs.clone(),
};

let rhs = match rhs {
ConstOrCell::Const(rhs) => compiler.backend.add_constant(
Some("wrap a constant as var"),
*rhs,
span,
),
ConstOrCell::Cell(rhs) => rhs.clone(),
};

// witness var for quotient
let q = Value::VarDivVar(lhs.clone(), rhs.clone());
let q_var = compiler.backend.new_internal_var(q, span);

// witness var for remainder
let rem = Value::VarModVar(lhs.clone(), rhs.clone());
let rem_var = compiler.backend.new_internal_var(rem, span);

// rem < rhs
Copy link
Contributor

Choose a reason for hiding this comment

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

If lhs < rhs then we will have rem = lhs. So either we forbid lhs < rhs or we add an edge case to make it work as well. Both might lead to extra constraints tho...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah, this constraint seems not well done. let me research a bit on how others constrain this.

let lt_rem = &less_than(compiler, None, &ConstOrCell::Cell(rem_var.clone()), &ConstOrCell::Cell(rhs.clone()), span)[0];
let lt_rem = lt_rem.cvar().expect("expected a cell var");
compiler.backend.assert_eq_const(lt_rem, one, span);

// foundamental constraint: lhs - q * rhs - rem = 0
let q_mul_rhs = compiler.backend.mul(&q_var, &rhs, span);
let lhs_sub_q_mul_rhs = compiler.backend.sub(&lhs, &q_mul_rhs, span);

// cell representing the foundamental constraint
let fc_var = compiler.backend.sub(&lhs_sub_q_mul_rhs, &rem_var, span);
compiler.backend.assert_eq_const(&fc_var, B::Field::zero(), span);

(rem_var, q_var)
}

/// Divide operation
pub fn div<B: Backend>(
compiler: &mut CircuitWriter<B>,
lhs: &ConstOrCell<B::Field, B::Var>,
rhs: &ConstOrCell<B::Field, B::Var>,
span: Span,
) -> Var<B::Field, B::Var> {
// to constrain lhs − q * rhs − rem = 0
// rhs can't be zero
match rhs {
ConstOrCell::Const(rhs) => {
if rhs.is_zero() {
panic!("division by zero");
}
}
_ => {
let is_zero = is_zero_cell(compiler, rhs, span);
let is_zero = is_zero[0].cvar().unwrap();
compiler.backend.assert_eq_const(is_zero, B::Field::zero(), span);
}
};

match (lhs, rhs) {
// if rhs is a constant, we can just divide lhs by rhs
(ConstOrCell::Const(lhs), ConstOrCell::Const(rhs)) => {
// to bigint
let lhs = lhs.to_biguint();
let rhs = rhs.to_biguint();
let res = lhs / rhs;

Var::new_constant(B::Field::from(res), span)
}
_ => {
let (_, q) = constrain_div_mod(compiler, lhs, rhs, span);
Var::new_var(q, span)
},
}
}

/// Modulus operation
pub fn modulus<B: Backend>(
compiler: &mut CircuitWriter<B>,
lhs: &ConstOrCell<B::Field, B::Var>,
rhs: &ConstOrCell<B::Field, B::Var>,
span: Span,
) -> Var<B::Field, B::Var> {
// to constrain lhs − q * rhs − rem = 0

let zero = B::Field::zero();

// rhs can't be zero
match &rhs {
ConstOrCell::Const(rhs) => {
if rhs.is_zero() {
panic!("modulus by zero");
}
}
_ => {
let is_zero = is_zero_cell(compiler, rhs, span);
let is_zero = is_zero[0].cvar().unwrap();
compiler.backend.assert_eq_const(is_zero, zero, span);
}
};

match (lhs, rhs) {
// if rhs is a constant, we can just divide lhs by rhs
(ConstOrCell::Const(lhs), ConstOrCell::Const(rhs)) => {
let lhs = lhs.to_biguint();
let rhs = rhs.to_biguint();
let res = lhs % rhs;

Var::new_constant(res.into(), span)
}
_ => {
let (rem, _) = constrain_div_mod(compiler, lhs, rhs, span);
Var::new_var(rem, span)
}
}
}

/// This takes variables that can be anything, and returns a boolean
// TODO: so perhaps it's not really relevant in this file?
pub fn equal<B: Backend>(
Expand Down
5 changes: 5 additions & 0 deletions src/lexer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ pub enum TokenKind {
Minus, // -
RightArrow, // ->
Star, // *
Percent, // %
Ampersand, // &
DoubleAmpersand, // &&
Pipe, // |
Expand Down Expand Up @@ -184,6 +185,7 @@ impl Display for TokenKind {
Minus => "`-`",
RightArrow => "`->`",
Star => "`*`",
Percent => "%",
Ampersand => "`&`",
DoubleAmpersand => "`&&`",
Pipe => "`|`",
Expand Down Expand Up @@ -392,6 +394,9 @@ impl Token {
'*' => {
tokens.push(TokenKind::Star.new_token(ctx, 1));
}
'%' => {
tokens.push(TokenKind::Percent.new_token(ctx, 1));
}
'&' => {
let next_c = chars.peek();
if matches!(next_c, Some(&'&')) {
Expand Down
1 change: 1 addition & 0 deletions src/mast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,7 @@ fn monomorphize_expr<B: Backend>(
| Op2::Subtraction
| Op2::Multiplication
| Op2::Division
| Op2::Modulus
| Op2::BoolAnd
| Op2::BoolOr => lhs_mono.typ,
};
Expand Down
3 changes: 3 additions & 0 deletions src/parser/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ pub enum Op2 {
Subtraction,
Multiplication,
Division,
Modulus,
Equality,
Inequality,
LessThan,
Expand Down Expand Up @@ -437,6 +438,7 @@ impl Expr {
| TokenKind::Minus
| TokenKind::Star
| TokenKind::Slash
| TokenKind::Percent
| TokenKind::DoubleEqual
| TokenKind::NotEqual
| TokenKind::Less
Expand All @@ -452,6 +454,7 @@ impl Expr {
TokenKind::Minus => Op2::Subtraction,
TokenKind::Star => Op2::Multiplication,
TokenKind::Slash => Op2::Division,
TokenKind::Percent => Op2::Modulus,
TokenKind::DoubleEqual => Op2::Equality,
TokenKind::NotEqual => Op2::Inequality,
TokenKind::Less => Op2::LessThan,
Expand Down
1 change: 1 addition & 0 deletions src/type_checker/checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ impl<B: Backend> TypeChecker<B> {
| Op2::Subtraction
| Op2::Multiplication
| Op2::Division
| Op2::Modulus
| Op2::BoolAnd
| Op2::BoolOr => lhs_node.typ,
};
Expand Down
22 changes: 22 additions & 0 deletions src/var.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,20 @@ where
// but does this make sense to all different backends? is it possible that some backend doesn't allow certain out of circuit calculations like this?
NthBit(B::Var, usize),

/// Divide
// todo: refactor to use a argument wrapper to encapsulate its own type,
// so that a variant can have an argument to be either B::Var or B::Field
CstDivVar(B::Field, B::Var),
VarDivCst(B::Var, B::Field),
VarDivVar(B::Var, B::Var),
CstDivCst(B::Field, B::Field),

/// Modulo
VarModVar(B::Var, B::Var),
CstModVar(B::Field, B::Var),
VarModCst(B::Var, B::Field),
CstModCst(B::Field, B::Field),

/// A public or private input to the function
/// There's an index associated to a variable name, as the variable could be composed of several field elements.
External(String, usize),
Expand All @@ -78,6 +92,14 @@ impl<B: Backend> std::fmt::Debug for Value<B> {
Value::PublicOutput(..) => write!(f, "PublicOutput"),
Value::Scale(..) => write!(f, "Scaling"),
Value::NthBit(_, _) => write!(f, "NthBit"),
Value::CstDivVar(_, _) => write!(f, "CstDivVar"),
Value::VarDivCst(_, _) => write!(f, "VarDivCst"),
Value::VarDivVar(_, _) => write!(f, "VarDivVar"),
Value::CstDivCst(_, _) => write!(f, "CstDivCst"),
Value::VarModVar(_, _) => write!(f, "VarModVar"),
Value::CstModVar(_, _) => write!(f, "CstModVar"),
Value::VarModCst(_, _) => write!(f, "VarModCst"),
Value::CstModCst(_, _) => write!(f, "CstModCst"),
}
}
}
Expand Down