diff --git a/ast/src/parsed/display.rs b/ast/src/parsed/display.rs index cf8d6d6cf8..6428f621f1 100644 --- a/ast/src/parsed/display.rs +++ b/ast/src/parsed/display.rs @@ -326,7 +326,7 @@ impl Display for Param { } pub fn quote(input: &str) -> String { - format!("\"{}\"", input.replace('\\', "\\\\").replace('"', "\\\"")) + format!("\"{}\"", input.escape_default()) } impl Display for PilStatement { @@ -448,7 +448,7 @@ impl Display for Expression { Expression::Reference(reference) => write!(f, "{reference}"), Expression::PublicReference(name) => write!(f, ":{name}"), Expression::Number(value) => write!(f, "{value}"), - Expression::String(value) => write!(f, "\"{value}\""), // TODO quote? + Expression::String(value) => write!(f, "\"{}\"", quote(value)), Expression::Tuple(items) => write!(f, "({})", format_expressions(items)), Expression::LambdaExpression(lambda) => write!(f, "{}", lambda), Expression::ArrayLiteral(array) => write!(f, "{array}"), diff --git a/parser/src/lib.rs b/parser/src/lib.rs index 1a19722f30..14aa9776af 100644 --- a/parser/src/lib.rs +++ b/parser/src/lib.rs @@ -67,6 +67,29 @@ pub fn parse_module<'a, T: FieldElement>( .map_err(|err| handle_parse_error(err, file_name, input)) } +/// Parse an escaped string - used in the grammar. +pub fn unescape_string(s: &str) -> String { + assert!(s.len() >= 2); + assert!(s.starts_with('"') && s.ends_with('"')); + let mut chars = s[1..s.len() - 1].chars(); + let mut result: String = Default::default(); + while let Some(c) = chars.next() { + result.push(if c == '\\' { + match chars.next().unwrap() { + 'n' => '\n', + 'r' => '\r', + 't' => '\t', + 'b' => 8 as char, + 'f' => 12 as char, + other => other, + } + } else { + c + }) + } + result +} + #[cfg(test)] mod test { use super::*; diff --git a/parser/src/powdr.lalrpop b/parser/src/powdr.lalrpop index fcbd46c99a..150f53dbcc 100644 --- a/parser/src/powdr.lalrpop +++ b/parser/src/powdr.lalrpop @@ -2,7 +2,7 @@ use std::str::FromStr; use powdr_ast::parsed::{*, asm::*}; use powdr_number::{AbstractNumberType, FieldElement}; use num_traits::Num; -use crate::ParserContext; +use crate::{ParserContext, unescape_string}; grammar(ctx: &ParserContext) where T: FieldElement; @@ -544,7 +544,7 @@ IfExpression: Box> = { StringLiteral: String = { - r#""[^"]*""# => <>[1..<>.len() - 1].to_string() + r#""[^\\"\n\r]*(\\[tnfbrx'"\\0-9][^\\"\n\r]*)*""# => unescape_string(<>) } Identifier: String = { diff --git a/pil-analyzer/src/evaluator.rs b/pil-analyzer/src/evaluator.rs index 97e63e1085..31e9c5a781 100644 --- a/pil-analyzer/src/evaluator.rs +++ b/pil-analyzer/src/evaluator.rs @@ -143,9 +143,10 @@ impl<'a, T: FieldElement, C: Custom> Value<'a, T, C> { } } -const BUILTINS: [(&str, BuiltinFunction); 2] = [ +const BUILTINS: [(&str, BuiltinFunction); 3] = [ ("std::array::len", BuiltinFunction::ArrayLen), ("std::check::panic", BuiltinFunction::Panic), + ("std::debug::print", BuiltinFunction::Print), ]; #[derive(Clone, Copy, PartialEq, Debug)] @@ -153,8 +154,11 @@ 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. + /// Does not return. Panic, + /// std::debug::print: string -> [], prints its argument on stdout. + /// Returns an empty array. + Print, } pub trait Custom: Display + fmt::Debug + Clone + PartialEq { @@ -326,6 +330,10 @@ mod internal { l.extend(std::mem::take(r)); Value::Array(std::mem::take(l)) } + (Value::String(l), BinaryOperator::Add, Value::String(r)) => { + l.push_str(r); + Value::String(std::mem::take(l)) + } (Value::Number(l), _, Value::Number(r)) => { Value::Number(evaluate_binary_operation(*l, *op, *r)) } @@ -430,6 +438,7 @@ mod internal { }) } + #[allow(clippy::print_stdout)] pub fn evaluate_builtin_function( b: BuiltinFunction, mut arguments: Vec>>, @@ -437,6 +446,7 @@ mod internal { let params = match b { BuiltinFunction::ArrayLen => 1, BuiltinFunction::Panic => 1, + BuiltinFunction::Print => 1, }; if arguments.len() != params { @@ -461,6 +471,15 @@ mod internal { }; Err(EvalError::FailedAssertion(msg))? } + BuiltinFunction::Print => { + 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(), + }; + print!("{msg}"); + Value::Array(Default::default()) + } }) } } diff --git a/std/debug.asm b/std/debug.asm new file mode 100644 index 0000000000..e7f90a10c6 --- /dev/null +++ b/std/debug.asm @@ -0,0 +1,7 @@ +/// This is a built-in function taking a string argument and printing it on stdout +/// when evaluated. +/// It returns an empty array so that it can be used at constraint level. +/// This symbol is not an empty array, the actual semantics are overridden. +let print = []; + +let println = [|msg| print(msg + "\n")][0]; \ No newline at end of file diff --git a/std/mod.asm b/std/mod.asm index 51b1acd716..fef91c1e8b 100644 --- a/std/mod.asm +++ b/std/mod.asm @@ -1,6 +1,7 @@ mod array; mod binary; mod check; +mod debug; mod hash; mod shift; mod split;