Skip to content

Commit

Permalink
feat: Implement format strings in the comptime interpreter (#5596)
Browse files Browse the repository at this point in the history
# Description

## Problem\*

Resolves #5482

## Summary\*

Implements format strings in the interpreter.

These are a bit weird since we immediately interpolate them and thus
have no need to actually distinguish them from regular strings. They are
also lowered into runtime code as normal strings.

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

---------

Co-authored-by: Michael J Klein <[email protected]>
  • Loading branch information
jfecher and michaeljklein authored Jul 24, 2024
1 parent 4c3bf97 commit fd7002c
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 16 deletions.
43 changes: 39 additions & 4 deletions compiler/noirc_frontend/src/hir/comptime/interpreter.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::collections::VecDeque;
use std::{collections::hash_map::Entry, rc::Rc};

use acvm::{acir::AcirField, FieldElement};
Expand Down Expand Up @@ -457,16 +458,50 @@ impl<'local, 'interner> Interpreter<'local, 'interner> {
self.evaluate_integer(value, is_negative, id)
}
HirLiteral::Str(string) => Ok(Value::String(Rc::new(string))),
HirLiteral::FmtStr(_, _) => {
let item = "format strings in a comptime context".into();
let location = self.elaborator.interner.expr_location(&id);
Err(InterpreterError::Unimplemented { item, location })
HirLiteral::FmtStr(string, captures) => {
self.evaluate_format_string(string, captures, id)
}
HirLiteral::Array(array) => self.evaluate_array(array, id),
HirLiteral::Slice(array) => self.evaluate_slice(array, id),
}
}

fn evaluate_format_string(
&mut self,
string: String,
captures: Vec<ExprId>,
id: ExprId,
) -> IResult<Value> {
let mut result = String::new();
let mut escaped = false;
let mut consuming = false;

let mut values: VecDeque<_> =
captures.into_iter().map(|capture| self.evaluate(capture)).collect::<Result<_, _>>()?;

for character in string.chars() {
match character {
'\\' => escaped = true,
'{' if !escaped => consuming = true,
'}' if !escaped && consuming => {
consuming = false;

if let Some(value) = values.pop_front() {
result.push_str(&value.to_string());
}
}
other if !consuming => {
escaped = false;
result.push(other);
}
_ => (),
}
}

let typ = self.elaborator.interner.id_type(id);
Ok(Value::FormatString(Rc::new(result), typ))
}

fn evaluate_integer(
&self,
value: FieldElement,
Expand Down
28 changes: 16 additions & 12 deletions compiler/noirc_frontend/src/hir/comptime/interpreter/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ impl<'local, 'context> Interpreter<'local, 'context> {
quoted_as_trait_constraint(interner, arguments, location)
}
"quoted_as_type" => quoted_as_type(self, arguments, location),
"zeroed" => zeroed(return_type, location),
"zeroed" => zeroed(return_type),
_ => {
let item = format!("Comptime evaluation for builtin function {name}");
Err(InterpreterError::Unimplemented { item, location })
Expand Down Expand Up @@ -461,12 +461,12 @@ fn trait_constraint_eq(
}

// fn zeroed<T>() -> T
fn zeroed(return_type: Type, location: Location) -> IResult<Value> {
fn zeroed(return_type: Type) -> IResult<Value> {
match return_type {
Type::FieldElement => Ok(Value::Field(0u128.into())),
Type::Array(length_type, elem) => {
if let Some(length) = length_type.evaluate_to_u32() {
let element = zeroed(elem.as_ref().clone(), location)?;
let element = zeroed(elem.as_ref().clone())?;
let array = std::iter::repeat(element).take(length as usize).collect();
Ok(Value::Array(array, Type::Array(length_type, elem)))
} else {
Expand Down Expand Up @@ -496,33 +496,37 @@ fn zeroed(return_type: Type, location: Location) -> IResult<Value> {
Ok(Value::Zeroed(Type::String(length_type)))
}
}
Type::FmtString(_, _) => {
let item = "format strings in a comptime context".into();
Err(InterpreterError::Unimplemented { item, location })
Type::FmtString(length_type, captures) => {
let length = length_type.evaluate_to_u32();
let typ = Type::FmtString(length_type, captures);
if let Some(length) = length {
Ok(Value::FormatString(Rc::new("\0".repeat(length as usize)), typ))
} else {
// Assume we can resolve the length later
Ok(Value::Zeroed(typ))
}
}
Type::Unit => Ok(Value::Unit),
Type::Tuple(fields) => {
Ok(Value::Tuple(try_vecmap(fields, |field| zeroed(field, location))?))
}
Type::Tuple(fields) => Ok(Value::Tuple(try_vecmap(fields, zeroed)?)),
Type::Struct(struct_type, generics) => {
let fields = struct_type.borrow().get_fields(&generics);
let mut values = HashMap::default();

for (field_name, field_type) in fields {
let field_value = zeroed(field_type, location)?;
let field_value = zeroed(field_type)?;
values.insert(Rc::new(field_name), field_value);
}

let typ = Type::Struct(struct_type, generics);
Ok(Value::Struct(values, typ))
}
Type::Alias(alias, generics) => zeroed(alias.borrow().get_type(&generics), location),
Type::Alias(alias, generics) => zeroed(alias.borrow().get_type(&generics)),
typ @ Type::Function(..) => {
// Using Value::Zeroed here is probably safer than using FuncId::dummy_id() or similar
Ok(Value::Zeroed(typ))
}
Type::MutableReference(element) => {
let element = zeroed(*element, location)?;
let element = zeroed(*element)?;
Ok(Value::Pointer(Shared::new(element), false))
}
Type::Quoted(QuotedType::TraitConstraint) => Ok(Value::TraitConstraint(TraitBound {
Expand Down
11 changes: 11 additions & 0 deletions compiler/noirc_frontend/src/hir/comptime/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub enum Value {
U32(u32),
U64(u64),
String(Rc<String>),
FormatString(Rc<String>, Type),
Function(FuncId, Type, Rc<TypeBindings>),
Closure(HirLambda, Vec<Value>, Type),
Tuple(Vec<Value>),
Expand Down Expand Up @@ -74,6 +75,7 @@ impl Value {
let length = Type::Constant(value.len() as u32);
Type::String(Box::new(length))
}
Value::FormatString(_, typ) => return Cow::Borrowed(typ),
Value::Function(_, typ, _) => return Cow::Borrowed(typ),
Value::Closure(_, _, typ) => return Cow::Borrowed(typ),
Value::Tuple(fields) => {
Expand Down Expand Up @@ -150,6 +152,10 @@ impl Value {
ExpressionKind::Literal(Literal::Integer((value as u128).into(), false))
}
Value::String(value) => ExpressionKind::Literal(Literal::Str(unwrap_rc(value))),
// Format strings are lowered as normal strings since they are already interpolated.
Value::FormatString(value, _) => {
ExpressionKind::Literal(Literal::Str(unwrap_rc(value)))
}
Value::Function(id, typ, bindings) => {
let id = interner.function_definition_id(id);
let impl_kind = ImplKind::NotATraitMethod;
Expand Down Expand Up @@ -280,6 +286,10 @@ impl Value {
HirExpression::Literal(HirLiteral::Integer((value as u128).into(), false))
}
Value::String(value) => HirExpression::Literal(HirLiteral::Str(unwrap_rc(value))),
// Format strings are lowered as normal strings since they are already interpolated.
Value::FormatString(value, _) => {
HirExpression::Literal(HirLiteral::Str(unwrap_rc(value)))
}
Value::Function(id, typ, bindings) => {
let id = interner.function_definition_id(id);
let impl_kind = ImplKind::NotATraitMethod;
Expand Down Expand Up @@ -424,6 +434,7 @@ impl Display for Value {
Value::U32(value) => write!(f, "{value}"),
Value::U64(value) => write!(f, "{value}"),
Value::String(value) => write!(f, "{value}"),
Value::FormatString(value, _) => write!(f, "{value}"),
Value::Function(..) => write!(f, "(function)"),
Value::Closure(_, _, _) => write!(f, "(closure)"),
Value::Tuple(fields) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "comptime_fmt_strings"
type = "bin"
authors = [""]
compiler_version = ">=0.32.0"

[dependencies]
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
fn main() {
// format strings are lowered as normal strings
let (s1, s2): (str<39>, str<4>) = comptime {
let x = 4;
let y = 5;

// Can't print these at compile-time here since printing to stdout while
// compiling breaks the test runner.
let s1 = f"x is {x}, fake interpolation: \{y}, y is {y}";
let s2 = std::unsafe::zeroed::<fmtstr<4, ()>>();
(s1, s2)
};
assert_eq(s1, "x is 4, fake interpolation: {y}, y is 5");
assert_eq(s2, "\0\0\0\0");
}

0 comments on commit fd7002c

Please sign in to comment.