Skip to content

Commit

Permalink
chore: Add remaining slice methods to the interpreter (#5422)
Browse files Browse the repository at this point in the history
# Description

## Problem\*

## Summary\*

Adds the remaining builtin slice methods to the interpreter.

Also adds `is_unconstrained` which always returns true. This way
`comptime` can also take advantage of any `if is_unconstrained()`
optimizations (like `break`) used in unconstrained code in called
functions.

## Additional Context

## Documentation\*

Check one:
- [x] No documentation needed.
- [ ] Documentation included in this PR.
- [ ] **[For Experimental Features]** Documentation to be submitted in a
separate PR.

# PR Checklist\*

- [x] I have tested the changes locally.
- [x] I have formatted the changes with [Prettier](https://prettier.io/)
and/or `cargo fmt` on default settings.
  • Loading branch information
jfecher authored Jul 5, 2024
1 parent 44abe7f commit 688448f
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 16 deletions.
137 changes: 123 additions & 14 deletions compiler/noirc_frontend/src/hir/comptime/interpreter/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ use std::rc::Rc;
use noirc_errors::Location;

use crate::{
ast::IntegerBitSize,
hir::comptime::{errors::IResult, InterpreterError, Value},
macros_api::NodeInterner,
macros_api::{NodeInterner, Signedness},
token::{SpannedToken, Token, Tokens},
QuotedType, Type,
};
Expand All @@ -18,10 +19,16 @@ pub(super) fn call_builtin(
match name {
"array_len" => array_len(interner, arguments, location),
"as_slice" => as_slice(interner, arguments, location),
"is_unconstrained" => Ok(Value::Bool(true)),
"slice_insert" => slice_insert(interner, arguments, location),
"slice_pop_back" => slice_pop_back(interner, arguments, location),
"slice_pop_front" => slice_pop_front(interner, arguments, location),
"slice_push_back" => slice_push_back(interner, arguments, location),
"slice_push_front" => slice_push_front(interner, arguments, location),
"slice_remove" => slice_remove(interner, arguments, location),
"struct_def_as_type" => struct_def_as_type(interner, arguments, location),
"struct_def_generics" => struct_def_generics(interner, arguments, location),
"struct_def_fields" => struct_def_fields(interner, arguments, location),
"struct_def_generics" => struct_def_generics(interner, arguments, location),
_ => {
let item = format!("Comptime evaluation for builtin function {name}");
Err(InterpreterError::Unimplemented { item, location })
Expand All @@ -42,6 +49,36 @@ fn check_argument_count(
}
}

fn failing_constraint<T>(message: impl Into<String>, location: Location) -> IResult<T> {
let message = Some(Value::String(Rc::new(message.into())));
Err(InterpreterError::FailingConstraint { message, location })
}

fn get_slice(
interner: &NodeInterner,
value: Value,
location: Location,
) -> IResult<(im::Vector<Value>, Type)> {
match value {
Value::Slice(values, typ) => Ok((values, typ)),
value => {
let type_var = Box::new(interner.next_type_variable());
let expected = Type::Slice(type_var);
Err(InterpreterError::TypeMismatch { expected, value, location })
}
}
}

fn get_u32(value: Value, location: Location) -> IResult<u32> {
match value {
Value::U32(value) => Ok(value),
value => {
let expected = Type::Integer(Signedness::Unsigned, IntegerBitSize::ThirtyTwo);
Err(InterpreterError::TypeMismatch { expected, value, location })
}
}
}

fn array_len(
interner: &NodeInterner,
mut arguments: Vec<(Value, Location)>,
Expand Down Expand Up @@ -85,18 +122,9 @@ fn slice_push_back(
check_argument_count(2, &arguments, location)?;

let (element, _) = arguments.pop().unwrap();
let (slice, _) = arguments.pop().unwrap();
match slice {
Value::Slice(mut values, typ) => {
values.push_back(element);
Ok(Value::Slice(values, typ))
}
value => {
let type_var = Box::new(interner.next_type_variable());
let expected = Type::Slice(type_var);
Err(InterpreterError::TypeMismatch { expected, value, location })
}
}
let (mut values, typ) = get_slice(interner, arguments.pop().unwrap().0, location)?;
values.push_back(element);
Ok(Value::Slice(values, typ))
}

/// fn as_type(self) -> Quoted
Expand Down Expand Up @@ -198,3 +226,84 @@ fn struct_def_fields(
])));
Ok(Value::Slice(fields, typ))
}

fn slice_remove(
interner: &mut NodeInterner,
mut arguments: Vec<(Value, Location)>,
location: Location,
) -> Result<Value, InterpreterError> {
check_argument_count(2, &arguments, location)?;

let index = get_u32(arguments.pop().unwrap().0, location)? as usize;
let (mut values, typ) = get_slice(interner, arguments.pop().unwrap().0, location)?;

if values.is_empty() {
return failing_constraint("slice_remove called on empty slice", location);
}

if index >= values.len() {
let message = format!(
"slice_remove: index {index} is out of bounds for a slice of length {}",
values.len()
);
return failing_constraint(message, location);
}

let element = values.remove(index);
Ok(Value::Tuple(vec![Value::Slice(values, typ), element]))
}

fn slice_push_front(
interner: &mut NodeInterner,
mut arguments: Vec<(Value, Location)>,
location: Location,
) -> Result<Value, InterpreterError> {
check_argument_count(2, &arguments, location)?;

let (element, _) = arguments.pop().unwrap();
let (mut values, typ) = get_slice(interner, arguments.pop().unwrap().0, location)?;
values.push_front(element);
Ok(Value::Slice(values, typ))
}

fn slice_pop_front(
interner: &mut NodeInterner,
mut arguments: Vec<(Value, Location)>,
location: Location,
) -> Result<Value, InterpreterError> {
check_argument_count(1, &arguments, location)?;

let (mut values, typ) = get_slice(interner, arguments.pop().unwrap().0, location)?;
match values.pop_front() {
Some(element) => Ok(Value::Tuple(vec![element, Value::Slice(values, typ)])),
None => failing_constraint("slice_pop_front called on empty slice", location),
}
}

fn slice_pop_back(
interner: &mut NodeInterner,
mut arguments: Vec<(Value, Location)>,
location: Location,
) -> Result<Value, InterpreterError> {
check_argument_count(1, &arguments, location)?;

let (mut values, typ) = get_slice(interner, arguments.pop().unwrap().0, location)?;
match values.pop_back() {
Some(element) => Ok(Value::Tuple(vec![Value::Slice(values, typ), element])),
None => failing_constraint("slice_pop_back called on empty slice", location),
}
}

fn slice_insert(
interner: &mut NodeInterner,
mut arguments: Vec<(Value, Location)>,
location: Location,
) -> Result<Value, InterpreterError> {
check_argument_count(3, &arguments, location)?;

let (element, _) = arguments.pop().unwrap();
let index = get_u32(arguments.pop().unwrap().0, location)?;
let (mut values, typ) = get_slice(interner, arguments.pop().unwrap().0, location)?;
values.insert(index as usize, element);
Ok(Value::Slice(values, typ))
}
12 changes: 11 additions & 1 deletion docs/docs/noir/standard_library/is_unconstrained.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,14 @@ pub fn external_interface(){

```

The is_unconstrained result is resolved at compile time, so in unconstrained contexts the compiler removes the else branch, and in constrained contexts the compiler removes the if branch, reducing the amount of compute necessary to run external_interface.
The is_unconstrained result is resolved at compile time, so in unconstrained contexts the compiler removes the else branch, and in constrained contexts the compiler removes the if branch, reducing the amount of compute necessary to run external_interface.

Note that using `is_unconstrained` in a `comptime` context will also return `true`:

```
fn main() {
comptime {
assert(is_unconstrained());
}
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "comptime_slice_methods"
type = "bin"
authors = [""]
compiler_version = ">=0.31.0"

[dependencies]
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
fn main() {
comptime
{
// Comptime scopes are counted as unconstrained
assert(std::runtime::is_unconstrained());

let slice = &[2];
let slice = slice.push_back(3);
let slice = slice.push_front(1);
assert_eq(slice, &[1, 2, 3]);

let slice = slice.insert(1, 10);
assert_eq(slice, &[1, 10, 2, 3]);

let (slice, ten) = slice.remove(1);
assert_eq(slice, &[1, 2, 3]);
assert_eq(ten, 10);

let (one, slice) = slice.pop_front();
assert_eq(slice, &[2, 3]);
assert_eq(one, 1);

let (slice, three) = slice.pop_back();
assert_eq(slice, &[2]);
assert_eq(three, 3);
}
}
3 changes: 2 additions & 1 deletion tooling/nargo_cli/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,14 @@ const IGNORED_BRILLIG_TESTS: [&str; 11] = [
/// Certain features are only available in the elaborator.
/// We skip these tests for non-elaborator code since they are not
/// expected to work there. This can be removed once the old code is removed.
const IGNORED_NEW_FEATURE_TESTS: [&str; 6] = [
const IGNORED_NEW_FEATURE_TESTS: [&str; 7] = [
"macros",
"wildcard_type",
"type_definition_annotation",
"numeric_generics_explicit",
"derive_impl",
"comptime_traits",
"comptime_slice_methods",
];

fn read_test_cases(
Expand Down

0 comments on commit 688448f

Please sign in to comment.