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 functions. #920

Merged
merged 4 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
114 changes: 109 additions & 5 deletions pil-analyzer/src/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,19 @@ pub fn evaluate_function_call<'a, T: FieldElement, C: Custom>(
symbols: &impl SymbolLookup<'a, T, C>,
) -> Result<Value<'a, T, C>, EvalError> {
match function {
Value::BuiltinFunction(b) => internal::evaluate_builtin_function(b, arguments),
Value::Closure(Closure {
lambda,
environment,
}) => {
if lambda.params.len() != arguments.len() {
Err(EvalError::TypeError(format!(
"Invalid function call: Supplied {} arguments to function that takes {} parameters.",
"Invalid function call: Supplied {} arguments to function that takes {} parameters.\nFunction: {lambda}\nArguments: {}",
arguments.len(),
lambda.params.len())
))?
lambda.params.len(),
arguments.iter().format(", ")

)))?
}

let local_vars = arguments.into_iter().chain(environment).collect::<Vec<_>>();
Expand Down Expand Up @@ -78,6 +81,8 @@ pub enum EvalError {
SymbolNotFound(String),
/// Data not (yet) available
DataNotAvailable,
/// Failed assertion, with reason.
FailedAssertion(String),
}

impl Display for EvalError {
Expand All @@ -89,6 +94,7 @@ impl Display for EvalError {
EvalError::NoMatch() => write!(f, "Unable to match pattern."),
EvalError::SymbolNotFound(msg) => write!(f, "Symbol not found: {msg}"),
EvalError::DataNotAvailable => write!(f, "Data not (yet) available."),
EvalError::FailedAssertion(msg) => write!(f, "Assertion failed: {msg}"),
}
}
}
Expand All @@ -100,6 +106,7 @@ pub enum Value<'a, T, C> {
Tuple(Vec<Self>),
Array(Vec<Self>),
Closure(Closure<'a, T, C>),
BuiltinFunction(BuiltinFunction),
Custom(C),
}

Expand Down Expand Up @@ -130,12 +137,27 @@ impl<'a, T: FieldElement, C: Custom> Value<'a, T, C> {
format!("[{}]", elements.iter().map(|e| e.type_name()).format(", "))
}
Value::Closure(c) => c.type_name(),
Value::BuiltinFunction(b) => format!("builtin_{b:?}"),
Value::Custom(c) => c.type_name(),
}
}
}

pub trait Custom: Display + Clone + PartialEq + fmt::Debug {
const BUILTINS: [(&str, BuiltinFunction); 2] = [
("std::array::len", BuiltinFunction::ArrayLen),
("std::check::panic", BuiltinFunction::Panic),
];

#[derive(Clone, Copy, PartialEq, Debug)]
pub enum BuiltinFunction {
/// std::array::len: [_] -> int, returns the length of an array
ArrayLen,
/// std::check::panic: string -> !, fails evaluation and uses its parameter for error reporting.
/// Returns the empty tuple.
Panic,
}

pub trait Custom: Display + fmt::Debug + Clone + PartialEq {
fn type_name(&self) -> String;
}

Expand All @@ -147,6 +169,7 @@ impl<'a, T: Display, C: Custom> Display for Value<'a, T, C> {
Value::Tuple(items) => write!(f, "({})", items.iter().format(", ")),
Value::Array(elements) => write!(f, "[{}]", elements.iter().format(", ")),
Value::Closure(closure) => write!(f, "{closure}"),
Value::BuiltinFunction(b) => write!(f, "{b:?}"),
Value::Custom(c) => write!(f, "{c}"),
}
}
Expand Down Expand Up @@ -396,7 +419,48 @@ mod internal {
) -> Result<Value<'a, T, C>, EvalError> {
Ok(match reference {
Reference::LocalVar(i, _name) => (*locals[*i as usize]).clone(),
Reference::Poly(poly) => symbols.lookup(&poly.name)?,

Reference::Poly(poly) => {
if let Some((_, b)) = BUILTINS.iter().find(|(n, _)| (n == &poly.name)) {
Value::BuiltinFunction(*b)
} else {
symbols.lookup(&poly.name)?
}
}
})
}

pub fn evaluate_builtin_function<T: FieldElement, C: Custom>(
b: BuiltinFunction,
mut arguments: Vec<Rc<Value<'_, T, C>>>,
) -> Result<Value<'_, T, C>, EvalError> {
let params = match b {
BuiltinFunction::ArrayLen => 1,
BuiltinFunction::Panic => 1,
};

if arguments.len() != params {
Err(EvalError::TypeError(format!(
"Invalid function call: Supplied {} arguments to function that takes {params} parameters.",
arguments.len(),
)))?
}
Ok(match b {
BuiltinFunction::ArrayLen => match arguments.pop().unwrap().as_ref() {
Value::Array(arr) => Value::Number((arr.len() as u64).into()),
v => Err(EvalError::TypeError(format!(
"Expected array for std::array::len, but got {v}: {}",
v.type_name()
)))?,
},
BuiltinFunction::Panic => {
let msg = match arguments.pop().unwrap().as_ref() {
Value::String(msg) => msg.clone(),
// As long as we do not yet have types, we just format any argument.
x => x.to_string(),
};
Err(EvalError::FailedAssertion(msg))?
}
})
}
}
Expand Down Expand Up @@ -468,4 +532,44 @@ mod test {
"99".to_string()
);
}

#[test]
pub fn array_len() {
let src = r#"
constant %N = 2;
namespace std::array(%N);
let len = 123;
namespace F(%N);
let x = std::array::len([1, 2, 3]);
let y = std::array::len([]);
"#;
assert_eq!(parse_and_evaluate_symbol(src, "F.x"), "3".to_string());
assert_eq!(parse_and_evaluate_symbol(src, "F.y"), "0".to_string());
}

#[test]
#[should_panic = r#"FailedAssertion("[1, \"text\"]")"#]
pub fn panic_complex() {
let src = r#"
constant %N = 2;
namespace std::check(%N);
let panic = 123;
namespace F(%N);
let x = (|i| if i == 1 { std::check::panic([i, "text"]) } else { 9 })(1);
"#;
parse_and_evaluate_symbol(src, "F.x");
}

#[test]
#[should_panic = r#"FailedAssertion("text")"#]
pub fn panic_string() {
let src = r#"
constant %N = 2;
namespace std::check(%N);
let panic = 123;
namespace F(%N);
let x = std::check::panic("text");
"#;
parse_and_evaluate_symbol(src, "F.x");
}
}
8 changes: 8 additions & 0 deletions pipeline/tests/asm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,3 +338,11 @@ fn hello_world_asm_fail() {
let i = [1];
verify_asm::<GoldilocksField>(f, slice_to_vec(&i));
}

#[test]
#[should_panic = "FailedAssertion(\"This should fail.\")"]
fn test_failing_assertion() {
let f = "asm/failing_assertion.asm";
let i = [];
verify_asm::<GoldilocksField>(f, slice_to_vec(&i));
}
16 changes: 16 additions & 0 deletions std/array.asm
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/// This is a built-in function taking an array argument and returning
/// the length of the array.
/// This symbol is not an empty array, the actual semantics are overridden.
let len = [];
Copy link
Member

Choose a reason for hiding this comment

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

This is also confusing. Why is it done like this?

Copy link
Member Author

Choose a reason for hiding this comment

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

If I just use let len;, then it will be a witness column. I have to assign something.
Although thinking about it, maybe it could work?
But in any case, since we don't have any annotations or stuff like this, it has to be weird in some way.


/// Evaluates to the array [f(0), f(1), ..., f(length - 1)].
let new = |length, f| std::utils::fold(length, f, [], |acc, e| (acc + [e]));

/// Evaluates to the array [f(arr[0]), f(arr[1]), ..., f(arr[len(arr) - 1])].
let map = |arr, f| new(len(arr), |i| f(arr[i]));

/// Computes folder(...folder(folder(initial, arr[0]), arr[1]) ..., arr[len(arr) - 1])
let fold = |arr, initial, folder| std::utils::fold(len(arr), |i| arr[i], initial, folder);

/// Returns the sum of the array elements.
let sum = [|arr| fold(arr, 0, |a, b| a + b)][0];
12 changes: 12 additions & 0 deletions std/check.asm
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/// This is a built-in function taking a string argument and terminating
/// evaluation unsuccessfully with this argument as explanation.
/// This symbol is not an empty array, the actual semantics are overridden.
let panic = [];
Copy link
Member

Choose a reason for hiding this comment

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

This is quite confusing

Copy link
Member Author

Choose a reason for hiding this comment

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

The main other way I could think of is something like:

std::core:
let builtin = |name| (); // The only weird thing
std::check:
let panic = std::core::builtin("panic");

And then builtin would be the only real builtin function.

Copy link
Member Author

Choose a reason for hiding this comment

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

although this would not pass strict type checking

Copy link
Member Author

Choose a reason for hiding this comment

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

ok, we could use something like let panic = std::core::builtin::<"panic">();

Copy link
Member

Choose a reason for hiding this comment

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

if it's already built-in, why not make it fully built-in?

Copy link
Member

Choose a reason for hiding this comment

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

so we keep it like this for now?


/// Checks the condition and panics if it is false.
/// IMPORTANT: Since this does not generate any constraints, the verifier will not
/// check these assertions. This function should only be used to verify
/// prover-internal consistency.
/// The panic message is obtained by calling the function `reason`.
/// Returns an empty array on success, which allows it to be used at statement level.
let assert = |condition, reason| if !condition { panic(reason()) } else { [] };
3 changes: 1 addition & 2 deletions std/hash/poseidon_bn254.asm
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::utils::map;
use std::array::map;
use std::utils::unchanged_until;

// Implements the poseidon permutation for the BN254 curve.
Expand Down Expand Up @@ -112,7 +112,6 @@ machine PoseidonBN254(LASTBLOCK, operation_id) {

map(
[input_in0, input_in1, input_cap],
3,
|c| unchanged_until(c, LAST)
);
}
14 changes: 7 additions & 7 deletions std/hash/poseidon_gl.asm
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::utils::fold;
use std::utils::make_array;
use std::array;

// Implements the Poseidon permutation for the Goldilocks field.
machine PoseidonGL(LASTBLOCK, operation_id) {
Expand Down Expand Up @@ -169,12 +169,12 @@ machine PoseidonGL(LASTBLOCK, operation_id) {
];

let equal_unless_last = |a, b| (1 - LAST) * (a - b) == 0;
make_array(8, |i| equal_unless_last(inp[i]', c[i]));
make_array(4, |i| equal_unless_last(cap[i]', c[8 + i]));
make_array(8, |i| equal_unless_last(input_inp[i], input_inp[i]'));
make_array(4, |i| equal_unless_last(input_cap[i], input_cap[i]'));
array::new(array::len(inp), |i| equal_unless_last(inp[i]', c[i]));
array::new(array::len(cap), |i| equal_unless_last(cap[i]', c[8 + i]));
array::map(input_inp, |x| equal_unless_last(x, x'));
array::map(input_cap, |x| equal_unless_last(x, x'));

let equal_on_first_block = |a, b| FIRSTBLOCK * (a - b) == 0;
make_array(8, |i| equal_on_first_block(input_inp[i], inp[i]));
make_array(4, |i| equal_on_first_block(input_cap[i], cap[i]));
array::new(array::len(input_inp), |i| equal_on_first_block(input_inp[i], inp[i]));
array::new(array::len(input_cap), |i| equal_on_first_block(input_cap[i], cap[i]));
}
2 changes: 2 additions & 0 deletions std/mod.asm
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
mod array;
mod binary;
mod check;
mod hash;
mod shift;
mod split;
Expand Down
6 changes: 0 additions & 6 deletions std/utils.asm
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,6 @@ let fold = |length, f, initial, folder|
folder(fold((length - 1), f, initial, folder), f((length - 1)))
};

/// Evaluates to the array [f(0), f(1), ..., f(length - 1)].
let make_array = |length, f| fold(length, f, [], |acc, e| (acc + [e]));

/// Evaluates to the array [f(arr[0]), f(arr[1]), ..., f(arr[length - 1])].
let map = |arr, length, f| make_array(length, |i| f(arr[i]));

/// Evaluates to f(0) + f(1) + ... + f(length - 1).
let sum = |length, f| fold(length, f, 0, |acc, e| (acc + e));

Expand Down
11 changes: 11 additions & 0 deletions test_data/asm/failing_assertion.asm
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use std::check;

machine Empty {
let line = |i| i - 7;
col witness w;
w = line;


check::assert(line(7) == 0, || "This should succeed.");
check::assert(line(7) != 0, || "This should fail.");
}
Loading