Skip to content

Commit

Permalink
feat: Add array_to_str_lossy (#5613)
Browse files Browse the repository at this point in the history
# Description

## Problem\*

Resolves <!-- Link to GitHub Issue -->

## Summary\*

I noticed we had a way to convert from strings to byte arrays but no way
to do the reverse.

## Additional Context

Draft because I'm unsure how to implement this in acir-gen where we
can't know the actual value of the array.

## Documentation\*

Check one:
- [ ] No documentation needed.
- [x] 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 Aug 8, 2024
1 parent a6ce7f4 commit af5acf4
Show file tree
Hide file tree
Showing 13 changed files with 101 additions and 4 deletions.
17 changes: 16 additions & 1 deletion compiler/noirc_evaluator/src/ssa/acir_gen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2714,7 +2714,22 @@ impl<'a> Context<'a> {
.get_or_create_witness_var(input)
.map(|val| self.convert_vars_to_values(vec![val], dfg, result_ids))?)
}
_ => todo!("expected a black box function"),
Intrinsic::ArrayAsStrUnchecked => Ok(vec![self.convert_value(arguments[0], dfg)]),
Intrinsic::AssertConstant => {
unreachable!("Expected assert_constant to be removed by this point")
}
Intrinsic::StaticAssert => {
unreachable!("Expected static_assert to be removed by this point")
}
Intrinsic::StrAsBytes => unreachable!("Expected as_bytes to be removed by this point"),
Intrinsic::FromField => unreachable!("Expected from_field to be removed by this point"),
Intrinsic::AsField => unreachable!("Expected as_field to be removed by this point"),
Intrinsic::IsUnconstrained => {
unreachable!("Expected is_unconstrained to be removed by this point")
}
Intrinsic::DerivePedersenGenerators => {
unreachable!("DerivePedersenGenerators can only be called with constants")
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ impl Context {
| Intrinsic::AsWitness
| Intrinsic::IsUnconstrained => {}
Intrinsic::ArrayLen
| Intrinsic::ArrayAsStrUnchecked
| Intrinsic::AsField
| Intrinsic::AsSlice
| Intrinsic::BlackBox(..)
Expand Down
4 changes: 4 additions & 0 deletions compiler/noirc_evaluator/src/ssa/ir/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ pub(crate) type InstructionId = Id<Instruction>;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub(crate) enum Intrinsic {
ArrayLen,
ArrayAsStrUnchecked,
AsSlice,
AssertConstant,
StaticAssert,
Expand All @@ -76,6 +77,7 @@ impl std::fmt::Display for Intrinsic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Intrinsic::ArrayLen => write!(f, "array_len"),
Intrinsic::ArrayAsStrUnchecked => write!(f, "array_as_str_unchecked"),
Intrinsic::AsSlice => write!(f, "as_slice"),
Intrinsic::AssertConstant => write!(f, "assert_constant"),
Intrinsic::StaticAssert => write!(f, "static_assert"),
Expand Down Expand Up @@ -116,6 +118,7 @@ impl Intrinsic {
Intrinsic::ToBits(_) | Intrinsic::ToRadix(_) => true,

Intrinsic::ArrayLen
| Intrinsic::ArrayAsStrUnchecked
| Intrinsic::AsSlice
| Intrinsic::SlicePushBack
| Intrinsic::SlicePushFront
Expand Down Expand Up @@ -144,6 +147,7 @@ impl Intrinsic {
pub(crate) fn lookup(name: &str) -> Option<Intrinsic> {
match name {
"array_len" => Some(Intrinsic::ArrayLen),
"array_as_str_unchecked" => Some(Intrinsic::ArrayAsStrUnchecked),
"as_slice" => Some(Intrinsic::AsSlice),
"assert_constant" => Some(Intrinsic::AssertConstant),
"static_assert" => Some(Intrinsic::StaticAssert),
Expand Down
2 changes: 2 additions & 0 deletions compiler/noirc_evaluator/src/ssa/ir/instruction/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ pub(super) fn simplify_call(
SimplifyResult::None
}
}
// Strings are already arrays of bytes in SSA
Intrinsic::ArrayAsStrUnchecked => SimplifyResult::SimplifiedTo(arguments[0]),
Intrinsic::AsSlice => {
let array = dfg.get_array_constant(arguments[0]);
if let Some((array, array_type)) = array {
Expand Down
5 changes: 5 additions & 0 deletions compiler/noirc_evaluator/src/ssa/ir/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ impl Type {
Type::unsigned(8)
}

/// Creates the str<N> type, of the given length N
pub(crate) fn str(length: usize) -> Type {
Type::Array(Rc::new(vec![Type::char()]), length)
}

/// Creates the native field type.
pub(crate) fn field() -> Type {
Type::Numeric(NumericType::NativeField)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ impl Context {
| Intrinsic::SliceRemove => true,

Intrinsic::ArrayLen
| Intrinsic::ArrayAsStrUnchecked
| Intrinsic::AssertConstant
| Intrinsic::StaticAssert
| Intrinsic::ApplyRangeConstraint
Expand Down
1 change: 1 addition & 0 deletions compiler/noirc_evaluator/src/ssa/opt/remove_if_else.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ fn slice_capacity_change(
| Intrinsic::StaticAssert
| Intrinsic::ApplyRangeConstraint
| Intrinsic::ArrayLen
| Intrinsic::ArrayAsStrUnchecked
| Intrinsic::StrAsBytes
| Intrinsic::BlackBox(_)
| Intrinsic::FromField
Expand Down
2 changes: 1 addition & 1 deletion compiler/noirc_evaluator/src/ssa/ssa_gen/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ impl<'a> FunctionContext<'a> {
ast::Type::Integer(Signedness::Signed, bits) => Type::signed((*bits).into()),
ast::Type::Integer(Signedness::Unsigned, bits) => Type::unsigned((*bits).into()),
ast::Type::Bool => Type::unsigned(1),
ast::Type::String(len) => Type::Array(Rc::new(vec![Type::char()]), *len as usize),
ast::Type::String(len) => Type::str(*len as usize),
ast::Type::FmtString(_, _) => {
panic!("convert_non_tuple_type called on a fmt string: {typ}")
}
Expand Down
16 changes: 16 additions & 0 deletions compiler/noirc_frontend/src/hir/comptime/interpreter/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ use crate::{
QuotedType, Shared, Type,
};

use self::builtin_helpers::{get_array, get_u8};

use super::Interpreter;

pub(crate) mod builtin_helpers;
Expand All @@ -37,6 +39,7 @@ impl<'local, 'context> Interpreter<'local, 'context> {
) -> IResult<Value> {
let interner = &mut self.elaborator.interner;
match name {
"array_as_str_unchecked" => array_as_str_unchecked(interner, arguments, location),
"array_len" => array_len(interner, arguments, location),
"as_slice" => as_slice(interner, arguments, location),
"is_unconstrained" => Ok(Value::Bool(true)),
Expand Down Expand Up @@ -112,6 +115,19 @@ fn array_len(
}
}

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

let array = get_array(interner, arguments.pop().unwrap().0, location)?.0;
let string_bytes = try_vecmap(array, |byte| get_u8(byte, location))?;
let string = String::from_utf8_lossy(&string_bytes).into_owned();
Ok(Value::String(Rc::new(string)))
}

fn as_slice(
interner: &NodeInterner,
arguments: Vec<(Value, Location)>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,17 @@ pub(crate) fn get_field(value: Value, location: Location) -> IResult<FieldElemen
}
}

pub(crate) fn get_u8(value: Value, location: Location) -> IResult<u8> {
match value {
Value::U8(value) => Ok(value),
value => {
let expected = Type::Integer(Signedness::Unsigned, IntegerBitSize::Eight);
let actual = value.get_type().into_owned();
Err(InterpreterError::TypeMismatch { expected, actual, location })
}
}
}

pub(crate) fn get_u32(value: Value, location: Location) -> IResult<u32> {
match value {
Value::U32(value) => Ok(value),
Expand Down
20 changes: 20 additions & 0 deletions docs/docs/noir/concepts/data_types/arrays.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,23 @@ fn main() {
}

```

### as_str_unchecked

Converts a byte array of type `[u8; N]` to a string. Note that this performs no UTF-8 validation -
the given array is interpreted as-is as a string.

```rust
impl<let N: u32> [u8; N] {
pub fn as_str_unchecked(self) -> str<N>
}
```

example:

```rust
fn main() {
let hi = [104, 105].as_str_unchecked();
assert_eq(hi, "hi");
}
```
17 changes: 15 additions & 2 deletions noir_stdlib/src/array.nr
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::cmp::Ord;
use crate::option::Option;
use crate::convert::From;

// TODO: Once we fully move to the new SSA pass this module can be removed and replaced
// by the methods in the `slice` module
impl<T, let N: u32> [T; N] {
#[builtin(array_len)]
pub fn len(self) -> u32 {}
Expand Down Expand Up @@ -108,6 +108,13 @@ impl<T, let N: u32> [T; N] {
}
}

impl<let N: u32> [u8; N] {
/// Convert a sequence of bytes as-is into a string.
/// This function performs no UTF-8 validation or similar.
#[builtin(array_as_str_unchecked)]
pub fn as_str_unchecked(self) -> str<N> {}
}

// helper function used to look up the position of a value in an array of Field
// Note that function returns 0 if the value is not found
unconstrained fn find_index<let N: u32>(a: [u32; N], find: u32) -> u32 {
Expand All @@ -119,3 +126,9 @@ unconstrained fn find_index<let N: u32>(a: [u32; N], find: u32) -> u32 {
}
result
}

impl<let N: u32> From<str<N>> for [u8; N] {
fn from(s: str<N>) -> Self {
s.as_bytes()
}
}
8 changes: 8 additions & 0 deletions noir_stdlib/src/string.nr
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::collections::vec::Vec;
use crate::convert::From;

impl<let N: u32> str<N> {
/// Converts the given string into a byte array
#[builtin(str_as_bytes)]
Expand All @@ -9,3 +11,9 @@ impl<let N: u32> str<N> {
Vec::from_slice(self.as_bytes().as_slice())
}
}

impl<let N: u32> From<[u8; N]> for str<N> {
fn from(bytes: [u8; N]) -> Self {
bytes.as_str_unchecked()
}
}

0 comments on commit af5acf4

Please sign in to comment.