Skip to content

Commit

Permalink
feat: Implement print without newline (#3650)
Browse files Browse the repository at this point in the history
# Description

## Problem\*

Resolves  **#3575
Resolves **#2912 (duplicate)

Continuation of #3576
## Summary\*

Refactors `println_oracle` method into `print` with boolean indicator,
allowing `print` to coexist with `println` under the same `ForeignCall`
string symbol.
Updates appropriate tests, and logging.md documentation.
Add a subchapter to `03_string.md` documentation for the `fmtstr` type

## Additional Context



## Documentation\*

Check one:
- [ ] No documentation needed.
- [x] Documentation included in this PR.
- [ ] **[Exceptional Case]** 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: jfecher <jfecher11@gmail.com>
  • Loading branch information
grasshopper47 and jfecher authored Dec 5, 2023
1 parent 95d7ce2 commit 9827dfe
Show file tree
Hide file tree
Showing 10 changed files with 97 additions and 33 deletions.
7 changes: 4 additions & 3 deletions compiler/noirc_frontend/src/monomorphization/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -889,10 +889,11 @@ impl<'interner> Monomorphizer<'interner> {

if let ast::Expression::Ident(ident) = original_func.as_ref() {
if let Definition::Oracle(name) = &ident.definition {
if name.as_str() == "println" {
if name.as_str() == "print" {
// Oracle calls are required to be wrapped in an unconstrained function
// Thus, the only argument to the `println` oracle is expected to always be an ident
self.append_printable_type_info(&hir_arguments[0], &mut arguments);
// The first argument to the `print` oracle is a bool, indicating a newline to be inserted at the end of the input
// The second argument is expected to always be an ident
self.append_printable_type_info(&hir_arguments[1], &mut arguments);
}
}
}
Expand Down
29 changes: 22 additions & 7 deletions docs/docs/standard_library/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ keywords:
[
noir logging,
println statement,
print statement,
debugging in noir,
noir std library,
logging tutorial,
Expand All @@ -17,14 +18,13 @@ keywords:
]
---

The standard library provides a familiar `println` statement you can use. Despite being a limited
implementation of rust's `println!` macro, this construct can be useful for debugging.
The standard library provides two familiar statements you can use: `println` and `print`. Despite being a limited implementation of rust's `println!` and `print!` macros, these constructs can be useful for debugging.

You can print the output of println statements in your Noir code by using the `nargo execute` command or the `--show-output` flag when using `nargo test` (provided there are println statements in your tests).
You can print the output of both statements in your Noir code by using the `nargo execute` command or the `--show-output` flag when using `nargo test` (provided there are print statements in your tests).

It is recommended to use `nargo execute` if you want to debug failing constrains with `println` statements. This is due to every input in a test being a constant rather than a witness, so we issue an error during compilation while we only print during execution (which comes after compilation). `println` will not work for failed constraints caught at compile time.
It is recommended to use `nargo execute` if you want to debug failing constrains with `println` or `print` statements. This is due to every input in a test being a constant rather than a witness, so we issue an error during compilation while we only print during execution (which comes after compilation). Neither `println`, nor `print` are callable for failed constraints caught at compile time.

The `println` statement is unconstrained, so it works for outputting integers, fields, strings, and even structs or expressions. For example:
Both `print` and `println` are generic functions which can work on integers, fields, strings, and even structs or expressions. Note however, that slices are currently unsupported. For example:

```rust
use dep::std;
Expand All @@ -40,10 +40,9 @@ fn main(age : Field, height : Field) {
std::println(age + height);
std::println("Hello world!");
}

```

You can print multiple different types in the same statement and string as well as a new "fmtstr" type. A `fmtstr` can be specified in the same way as a normal string it just should be prepended with an "f" character:
You can print different types in the same statement (including strings) with a type called `fmtstr`. It can be specified in the same way as a normal string, just prepended with an "f" character:

```rust
let fmt_str = f"i: {i}, j: {j}";
Expand All @@ -59,4 +58,20 @@ You can print multiple different types in the same statement and string as well

let foo = fooStruct { my_struct: s, foo: 15 };
std::println(f"s: {s}, foo: {foo}");

std::println(15); // prints 0x0f, implicit Field
std::println(-1 as u8); // prints 255
std::println(-1 as i8); // prints -1
```

Examples shown above are interchangeable between the two `print` statements:

```rust
let person = Person { age : age, height : height };

std::println(person);
std::print(person);

std::println("Hello world!"); // Prints with a newline at the end of the input
std::print("Hello world!"); // Prints the input and keeps cursor on the same line
```
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ keywords:

The string type is a fixed length value defined with `str<N>`.

You can use strings in `assert()` functions or print them with
`std::println()`. See more about [Logging](../../standard_library/logging).
You can use strings in `assert()` functions or print them with `std::println()`. See more about [Logging](../../standard_library/logging).

```rust
use dep::std;
Expand All @@ -42,7 +41,7 @@ fn main() {
}
```

## Escape characters
## Escape Characters

You can use escape characters for your strings:

Expand All @@ -58,6 +57,32 @@ You can use escape characters for your strings:
Example:

```rust
let s = "Hello \"world" // prints "Hello "world"
let s = "Hello \"world"; // prints "Hello "world"
let s = "hey \tyou"; // prints "hey you"
```

## Formatted Strings

You can prepend a string with the singular `f` token to create a formatted string. This is useful when logging, as it allows injection of local variables:

```rust
let var = 15;
std::println(f"var {var}") // prints "var 0x0F"

let var = -1 as u8;
std::println(f"var {var}") // prints "var 255"

let var : i8 = -1;
std::println(f"var {var}") // prints "var -1"

// prints "Hello
//world"
std::println(f"Hello
world");

std::println(f"hey \tyou"); // prints "hey \tyou"
```

A type can be specified to print numbers either as hex via `Field`, unsigned via `u*` types and signed via `i*` types.

Note that escaped characters in formatted strings `fmtstr` will be outputted as defined, i.e. "\n" will be printed `\n`, not as a new line. You can add a newline or other whitespace by creating a multiline string as in the example above.
12 changes: 8 additions & 4 deletions noir_stdlib/src/lib.nr
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@ mod option;
mod string;
mod test;
// Oracle calls are required to be wrapped in an unconstrained function
// Thus, the only argument to the `println` oracle is expected to always be an ident
#[oracle(println)]
unconstrained fn println_oracle<T>(_input: T) {}
// Thus, the only argument to the `println` oracle is expected to always be an ident
#[oracle(print)]
unconstrained fn print_oracle<T>(_with_newline: bool, _input: T) {}

unconstrained pub fn print<T>(input: T) {
print_oracle(false, input);
}

unconstrained pub fn println<T>(input: T) {
println_oracle(input);
print_oracle(true, input);
}

#[foreign(recursive_aggregation)]
Expand Down
12 changes: 10 additions & 2 deletions test_programs/execution_success/strings/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,26 @@ fn main(message: pub str<11>, y: Field, hex_as_string: str<4>, hex_as_field: Fie
let x = 10;
let z = x * 5;
std::println(10);
std::print(10);

std::println(z); // x * 5 in println not yet supported
std::print(z);
std::println(x);
std::print(x);

let array = [1, 2, 3, 5, 8];
assert(y == 5); // Change to y != 5 to see how the later print statements are not called
std::println(array);
std::print(array);

bad_message = "hell\0\"world";
std::println(bad_message);
std::print(bad_message);
assert(message != bad_message);

let hash = std::hash::pedersen_commitment([x]);
std::println(hash);
std::print(hash);

assert(hex_as_string == "0x41");
// assert(hex_as_string != 0x41); This will fail with a type mismatch between str[4] and Field
Expand All @@ -36,6 +42,10 @@ fn test_prints_strings() {

std::println(message);
std::println("goodbye world");

std::print(message);
std::print("\n");
std::print("goodbye world\n");
}

#[test]
Expand All @@ -52,8 +62,6 @@ fn test_prints_array() {
}

fn failed_constraint(hex_as_field: Field) {
// TODO(#2116): Note that `println` will not work if a failed constraint can be
// evaluated at compile time.
// When this method is called from a test method or with constant values
// a `Failed constraint` compile error will be caught before this `println`
// is executed as the input will be a constant.
Expand Down
23 changes: 14 additions & 9 deletions tooling/nargo/src/ops/foreign_calls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub trait ForeignCallExecutor {
/// This enumeration represents the Brillig foreign calls that are natively supported by nargo.
/// After resolution of a foreign call, nargo will restart execution of the ACVM
pub(crate) enum ForeignCall {
Println,
Print,
CreateMock,
SetMockParams,
SetMockReturns,
Expand All @@ -31,7 +31,7 @@ impl std::fmt::Display for ForeignCall {
impl ForeignCall {
pub(crate) fn name(&self) -> &'static str {
match self {
ForeignCall::Println => "println",
ForeignCall::Print => "print",
ForeignCall::CreateMock => "create_mock",
ForeignCall::SetMockParams => "set_mock_params",
ForeignCall::SetMockReturns => "set_mock_returns",
Expand All @@ -42,7 +42,7 @@ impl ForeignCall {

pub(crate) fn lookup(op_name: &str) -> Option<ForeignCall> {
match op_name {
"println" => Some(ForeignCall::Println),
"print" => Some(ForeignCall::Print),
"create_mock" => Some(ForeignCall::CreateMock),
"set_mock_params" => Some(ForeignCall::SetMockParams),
"set_mock_returns" => Some(ForeignCall::SetMockReturns),
Expand Down Expand Up @@ -92,7 +92,7 @@ pub struct DefaultForeignCallExecutor {
last_mock_id: usize,
/// The registered mocks
mocked_responses: Vec<MockedCall>,
/// Whether to print [`ForeignCall::Println`] output.
/// Whether to print [`ForeignCall::Print`] output.
show_output: bool,
}

Expand Down Expand Up @@ -120,9 +120,14 @@ impl DefaultForeignCallExecutor {
decode_string_value(&fields)
}

fn execute_println(foreign_call_inputs: &[ForeignCallParam]) -> Result<(), ForeignCallError> {
let display_values: PrintableValueDisplay = foreign_call_inputs.try_into()?;
println!("{display_values}");
fn execute_print(foreign_call_inputs: &[ForeignCallParam]) -> Result<(), ForeignCallError> {
let skip_newline = foreign_call_inputs[0].unwrap_value().is_zero();
let display_values: PrintableValueDisplay = foreign_call_inputs
.split_first()
.ok_or(ForeignCallError::MissingForeignCallInputs)?
.1
.try_into()?;
print!("{display_values}{}", if skip_newline { "" } else { "\n" });
Ok(())
}
}
Expand All @@ -134,9 +139,9 @@ impl ForeignCallExecutor for DefaultForeignCallExecutor {
) -> Result<ForeignCallResult, ForeignCallError> {
let foreign_call_name = foreign_call.function.as_str();
match ForeignCall::lookup(foreign_call_name) {
Some(ForeignCall::Println) => {
Some(ForeignCall::Print) => {
if self.show_output {
Self::execute_println(&foreign_call.inputs)?;
Self::execute_print(&foreign_call.inputs)?;
}
Ok(ForeignCallResult { values: vec![] })
}
Expand Down
1 change: 1 addition & 0 deletions tooling/nargo_fmt/tests/expected/print.nr
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use dep::std;

fn main() {
std::print("Hello world");
std::println("Hello world");
}
3 changes: 2 additions & 1 deletion tooling/nargo_fmt/tests/expected/print2.nr
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use dep::std;

fn main( ) {
fn main() {
std::print("Hello world");
std::println("Hello world");
}
5 changes: 4 additions & 1 deletion tooling/nargo_fmt/tests/input/print.nr
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
use dep::std;

fn main() { std::println("Hello world"); }
fn main() {
std::print("Hello world");
std::println("Hello world");
}
5 changes: 3 additions & 2 deletions tooling/nargo_fmt/tests/input/print2.nr
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use dep::std;

fn main( ) {
std::println("Hello world");
fn main() {
std::print("Hello world");
std::println("Hello world");
}

0 comments on commit 9827dfe

Please sign in to comment.