diff --git a/crates/aiken-lang/src/ast.rs b/crates/aiken-lang/src/ast.rs index ccd367f2..de7d5c97 100644 --- a/crates/aiken-lang/src/ast.rs +++ b/crates/aiken-lang/src/ast.rs @@ -205,6 +205,7 @@ fn str_to_keyword(word: &str) -> Option { "todo" => Some(Token::Todo), "type" => Some(Token::Type), "trace" => Some(Token::Trace), + "emit" => Some(Token::Emit), "test" => Some(Token::Test), // TODO: remove this in a future release "error" => Some(Token::Fail), @@ -2009,7 +2010,7 @@ pub enum Error { #[diagnostic(code("illegal::module_name"))] #[diagnostic(help(r#"You cannot use keywords as part of a module path name. As a quick reminder, here's a list of all the keywords (and thus, of invalid module path names): - as, expect, check, const, else, fn, if, is, let, opaque, pub, test, todo, trace, type, use, when"#))] + as, expect, check, const, else, fn, if, is, let, opaque, pub, test, todo, trace, emit, type, use, when"#))] KeywordInModuleName { name: String, keyword: String }, #[error("I realized you used '{}' as a module name, which is reserved (and not available).\n", diff --git a/crates/aiken-lang/src/expr.rs b/crates/aiken-lang/src/expr.rs index 1919c5b1..b05ab895 100644 --- a/crates/aiken-lang/src/expr.rs +++ b/crates/aiken-lang/src/expr.rs @@ -116,6 +116,13 @@ pub enum TypedExpr { text: Box, }, + Emit { + location: Span, + tipo: Rc, + then: Box, + text: Box, + }, + When { location: Span, tipo: Rc, @@ -196,6 +203,7 @@ impl TypedExpr { match self { Self::Var { constructor, .. } => constructor.tipo.clone(), Self::Trace { then, .. } => then.tipo(), + Self::Emit { then, .. } => then.tipo(), Self::Fn { tipo, .. } | Self::UInt { tipo, .. } | Self::ErrorTerm { tipo, .. } @@ -241,6 +249,7 @@ impl TypedExpr { TypedExpr::Fn { .. } | TypedExpr::UInt { .. } | TypedExpr::Trace { .. } + | TypedExpr::Emit { .. } | TypedExpr::List { .. } | TypedExpr::Call { .. } | TypedExpr::When { .. } @@ -283,6 +292,7 @@ impl TypedExpr { | Self::UInt { location, .. } | Self::Var { location, .. } | Self::Trace { location, .. } + | Self::Emit { location, .. } | Self::ErrorTerm { location, .. } | Self::When { location, .. } | Self::Call { location, .. } @@ -318,6 +328,7 @@ impl TypedExpr { Self::Fn { location, .. } | Self::UInt { location, .. } | Self::Trace { location, .. } + | Self::Emit { location, .. } | Self::Var { location, .. } | Self::ErrorTerm { location, .. } | Self::When { location, .. } @@ -360,6 +371,11 @@ impl TypedExpr { .find_node(byte_index) .or_else(|| then.find_node(byte_index)) .or(Some(Located::Expression(self))), + + TypedExpr::Emit { text, then, .. } => text + .find_node(byte_index) + .or_else(|| then.find_node(byte_index)) + .or(Some(Located::Expression(self))), TypedExpr::Pipeline { expressions, .. } | TypedExpr::Sequence { expressions, .. } => { expressions.iter().find_map(|e| e.find_node(byte_index)) @@ -534,6 +550,12 @@ pub enum UntypedExpr { text: Box, }, + Emit { + location: Span, + then: Box, + text: Box, + }, + TraceIfFalse { location: Span, value: Box, @@ -1245,6 +1267,7 @@ impl UntypedExpr { match self { Self::PipeLine { expressions, .. } => expressions.last().location(), Self::Trace { then, .. } => then.location(), + Self::Emit { then, .. } => then.location(), Self::TraceIfFalse { location, .. } | Self::Fn { location, .. } | Self::Var { location, .. } @@ -1284,7 +1307,7 @@ impl UntypedExpr { .map(|e| e.start_byte_index()) .unwrap_or(location.start), Self::PipeLine { expressions, .. } => expressions.first().start_byte_index(), - Self::Trace { location, .. } | Self::Assignment { location, .. } => location.start, + Self::Trace { location, .. } | Self::Emit{ location, .. } | Self::Assignment { location, .. } => location.start, _ => self.location().start, } } diff --git a/crates/aiken-lang/src/format.rs b/crates/aiken-lang/src/format.rs index 9fa8c67b..2bf52943 100644 --- a/crates/aiken-lang/src/format.rs +++ b/crates/aiken-lang/src/format.rs @@ -639,7 +639,7 @@ impl<'comments> Formatter<'comments> { ) -> Document<'a> { let args = wrap_args(args.iter().map(|e| (self.fn_arg(e), false))).group(); let body = match body { - UntypedExpr::Trace { .. } | UntypedExpr::When { .. } => { + UntypedExpr::Trace { .. } | UntypedExpr::Emit { .. } | UntypedExpr::When { .. } => { self.expr(body, true).force_break() } _ => self.expr(body, true), @@ -957,6 +957,10 @@ impl<'comments> Formatter<'comments> { kind, text, then, .. } => self.trace(kind, text, then), + UntypedExpr::Emit { + text, then, .. + } => self.emit(text, then), + UntypedExpr::When { subject, clauses, .. } => self.when(subject, clauses), @@ -1045,6 +1049,25 @@ impl<'comments> Formatter<'comments> { } } + pub fn emit<'a>( + &mut self, + text: &'a UntypedExpr, + then: &'a UntypedExpr, + ) -> Document<'a> { + let body = "emit" + .to_doc() + .append(" ") + .append(self.wrap_expr(text)) + .group(); + body + .append(if self.pop_empty_lines(then.start_byte_index()) { + lines(2) + } else { + line() + }) + .append(self.expr(then, true)) + } + pub fn pattern_constructor<'a>( &mut self, name: &'a str, @@ -1659,6 +1682,7 @@ impl<'comments> Formatter<'comments> { kind: TraceKind::Trace, .. } + | UntypedExpr::Emit { .. } | UntypedExpr::Sequence { .. } | UntypedExpr::Assignment { .. } => "{" .to_doc() @@ -1701,6 +1725,7 @@ impl<'comments> Formatter<'comments> { kind: TraceKind::Trace, .. } + | UntypedExpr::Emit { .. } | UntypedExpr::Sequence { .. } | UntypedExpr::Assignment { .. } => " {" .to_doc() diff --git a/crates/aiken-lang/src/gen_uplc.rs b/crates/aiken-lang/src/gen_uplc.rs index cd13c5c0..1bd51d04 100644 --- a/crates/aiken-lang/src/gen_uplc.rs +++ b/crates/aiken-lang/src/gen_uplc.rs @@ -530,6 +530,14 @@ impl<'a> CodeGenerator<'a> { self.build(then, module_build_name, &[]), ), + TypedExpr::Emit { + tipo, then, text, .. + } => AirTree::emit( + self.build(text, module_build_name, &[]), + tipo.clone(), + self.build(then, module_build_name, &[]), + ), + TypedExpr::When { tipo, subject, @@ -5310,6 +5318,15 @@ impl<'a> CodeGenerator<'a> { Some(term) } + Air::Emit { .. } => { + let text = arg_stack.pop().unwrap(); + + let term = arg_stack.pop().unwrap(); + + let term = term.delayed_trace(text); + + Some(term) + } Air::ErrorTerm { validator, .. } => { if validator { Some(Term::Error.apply(Term::Error.force())) diff --git a/crates/aiken-lang/src/gen_uplc/air.rs b/crates/aiken-lang/src/gen_uplc/air.rs index 9c9fb344..c86c29bd 100644 --- a/crates/aiken-lang/src/gen_uplc/air.rs +++ b/crates/aiken-lang/src/gen_uplc/air.rs @@ -198,6 +198,9 @@ pub enum Air { Trace { tipo: Rc, }, + Emit { + tipo: Rc, + }, NoOp, FieldsEmpty, ListEmpty, diff --git a/crates/aiken-lang/src/gen_uplc/tree.rs b/crates/aiken-lang/src/gen_uplc/tree.rs index 0cacc410..5863a1ae 100644 --- a/crates/aiken-lang/src/gen_uplc/tree.rs +++ b/crates/aiken-lang/src/gen_uplc/tree.rs @@ -354,6 +354,11 @@ pub enum AirTree { msg: Box, then: Box, }, + Emit { + tipo: Rc, + msg: Box, + then: Box, + }, // End Expressions } @@ -830,6 +835,13 @@ impl AirTree { then: then.into(), } } + pub fn emit(msg: AirTree, tipo: Rc, then: AirTree) -> AirTree { + AirTree::Emit { + tipo, + msg: msg.into(), + then: then.into(), + } + } pub fn no_op(then: AirTree) -> AirTree { AirTree::NoOp { then: then.into() } } @@ -1379,6 +1391,11 @@ impl AirTree { msg.create_air_vec(air_vec); then.create_air_vec(air_vec); } + AirTree::Emit { tipo, msg, then } => { + air_vec.push(Air::Emit { tipo: tipo.clone() }); + msg.create_air_vec(air_vec); + then.create_air_vec(air_vec); + } } } @@ -1400,7 +1417,8 @@ impl AirTree { | AirTree::Constr { tipo, .. } | AirTree::RecordUpdate { tipo, .. } | AirTree::ErrorTerm { tipo, .. } - | AirTree::Trace { tipo, .. } => tipo.clone(), + | AirTree::Trace { tipo, .. } + | AirTree::Emit { tipo, .. } => tipo.clone(), AirTree::Void => void(), AirTree::Var { constructor, .. } => constructor.tipo.clone(), AirTree::Fn { func_body, .. } => func_body.return_type(), @@ -1456,7 +1474,8 @@ impl AirTree { | AirTree::If { tipo, .. } | AirTree::Constr { tipo, .. } | AirTree::ErrorTerm { tipo, .. } - | AirTree::Trace { tipo, .. } => vec![tipo], + | AirTree::Trace { tipo, .. } + | AirTree::Emit { tipo, .. } => vec![tipo], AirTree::Var { constructor, .. } => { vec![constructor.tipo.borrow_mut()] } @@ -1940,6 +1959,23 @@ impl AirTree { apply_with_func_last, ); } + AirTree::Emit { msg, then, .. } => { + msg.do_traverse_tree_with( + tree_path, + current_depth + 1, + index_count.next_number(), + with, + apply_with_func_last, + ); + + then.do_traverse_tree_with( + tree_path, + current_depth + 1, + index_count.next_number(), + with, + apply_with_func_last, + ); + } AirTree::DefineFunc { func_body, then, .. } => { @@ -2284,6 +2320,15 @@ impl AirTree { panic!("Tree Path index outside tree children nodes") } } + AirTree::Emit { msg, then, .. } => { + if *index == 0 { + msg.as_mut().do_find_air_tree_node(tree_path_iter) + } else if *index == 1 { + then.as_mut().do_find_air_tree_node(tree_path_iter) + } else { + panic!("Tree Path index outside tree children nodes") + } + } _ => { panic!("A tree node with no children was encountered with a longer tree path.") } diff --git a/crates/aiken-lang/src/parser/expr/fail_todo_trace.rs b/crates/aiken-lang/src/parser/expr/fail_todo_trace.rs index cdec3694..1e30893a 100644 --- a/crates/aiken-lang/src/parser/expr/fail_todo_trace.rs +++ b/crates/aiken-lang/src/parser/expr/fail_todo_trace.rs @@ -36,6 +36,14 @@ pub fn parser<'a>( then: Box::new(then_.unwrap_or_else(|| UntypedExpr::todo(None, span))), text: Box::new(text), }), + just(Token::Emit) + .ignore_then(choice((string::hybrid(), expression.clone()))) + .then(sequence.clone().or_not()) + .map_with_span(|(text, then_), span| UntypedExpr::Emit { + location: span, + then: Box::new(then_.unwrap_or_else(|| UntypedExpr::todo(None, span))), + text: Box::new(text), + }), )) } diff --git a/crates/aiken-lang/src/parser/lexer.rs b/crates/aiken-lang/src/parser/lexer.rs index c1fc1282..c9bed8ba 100644 --- a/crates/aiken-lang/src/parser/lexer.rs +++ b/crates/aiken-lang/src/parser/lexer.rs @@ -220,6 +220,7 @@ pub fn lexer() -> impl Parser, Error = ParseError> { let keyword = text::ident().map(|s: String| match s.as_str() { "trace" => Token::Trace, + "emit" => Token::Emit, // TODO: remove this in a future release "error" => Token::Fail, "fail" => Token::Fail, diff --git a/crates/aiken-lang/src/parser/token.rs b/crates/aiken-lang/src/parser/token.rs index 6169749f..04c821df 100644 --- a/crates/aiken-lang/src/parser/token.rs +++ b/crates/aiken-lang/src/parser/token.rs @@ -89,6 +89,7 @@ pub enum Token { Type, When, Trace, + Emit, Validator, Via, } @@ -175,6 +176,7 @@ impl fmt::Display for Token { Token::Pub => "pub", Token::Todo => "todo", Token::Trace => "trace", + Token::Emit => "emit", Token::Type => "type", Token::Test => "test", Token::Fail => "fail", diff --git a/crates/aiken-lang/src/tipo/expr.rs b/crates/aiken-lang/src/tipo/expr.rs index 7aa932f9..3251dce6 100644 --- a/crates/aiken-lang/src/tipo/expr.rs +++ b/crates/aiken-lang/src/tipo/expr.rs @@ -281,6 +281,22 @@ impl<'a, 'b> ExprTyper<'a, 'b> { kind, } => self.infer_trace(kind, *then, location, *text), + UntypedExpr::Emit { + location, + then, + text, + } => { + let text = self.infer(*text)?; + self.unify(string(), text.tipo(), text.location(), false)?; + let then = self.infer(*then)?; + Ok(TypedExpr::Emit { + location, + tipo: then.tipo(), + then: Box::new(then), + text: Box::new(text), + }) + } + UntypedExpr::When { location, subject, @@ -2324,7 +2340,7 @@ fn assert_no_assignment(expr: &UntypedExpr) -> Result<(), Error> { location: expr.location(), expr: *value.clone(), }), - UntypedExpr::Trace { then, .. } => assert_no_assignment(then), + UntypedExpr::Trace { then, .. } | UntypedExpr::Emit { then, .. } => assert_no_assignment(then), UntypedExpr::Fn { .. } | UntypedExpr::BinOp { .. } | UntypedExpr::ByteArray { .. } diff --git a/crates/aiken-project/src/test_framework.rs b/crates/aiken-project/src/test_framework.rs index 90e0c4ae..f6dfe3d5 100644 --- a/crates/aiken-project/src/test_framework.rs +++ b/crates/aiken-project/src/test_framework.rs @@ -1085,6 +1085,7 @@ impl TryFrom for Assertion { } TypedExpr::Trace { then, .. } => (*then).try_into(), + TypedExpr::Emit { then, .. } => (*then).try_into(), TypedExpr::Sequence { expressions, .. } | TypedExpr::Pipeline { expressions, .. } => { if let Ok(Assertion {