diff --git a/CHANGELOG.md b/CHANGELOG.md index f4a2b02da..4218628ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,41 @@ - **aiken-lang**: remove warning on discarded expect, allowing to keep 'side-effects' when necessary. See #967. @KtorZ +- **aiken-lang**: rework traces to be (1) variadic, (2) generic in its arguments and (3) structured. @KtorZ + + In more details: + 1. Enables the `trace` keyword to take one, two or any argument really separated by comma after the first. For example: + + ```ak + trace @"a classic trace" + + // .. + + trace @"condition_1": @"foo" + + // ... + + trace @"condition_2": @"foo", @"bar" + ``` + + 2. Enables the `trace` keyword to not only take strings as arguments; but any + data-type that is serialisable (i.e. that can be cast to Data). It is fundamentally identical to calling the [`cbor.diagnostic`](https://aiken-lang.github.io/stdlib/aiken/cbor.html#diagnostic) function from the standard lib; except that this is done and glued with the rest of the trace automatically. + + ```ak + trace @"condition_1": [1, 2, 3] + + // ... + + let my_var = Some("foo") + trace my_var + ``` + + 3. Changes the behavior of the `--trace-level compact` mode to now: + - remove trace-if-false (`?` operator) traces entirely in this mode; + - only keep the label (first trace argument) and error when it isn't a string. + + See also [#978](https://github.com/aiken-lang/aiken/pull/978). + ## v1.0.29-alpha - 2024-06-06 ### Added diff --git a/crates/aiken-lang/src/ast.rs b/crates/aiken-lang/src/ast.rs index 20e7b373d..97c48267c 100644 --- a/crates/aiken-lang/src/ast.rs +++ b/crates/aiken-lang/src/ast.rs @@ -1957,6 +1957,10 @@ pub enum TraceLevel { } impl Tracing { + pub fn verbose() -> Self { + Tracing::All(TraceLevel::Verbose) + } + pub fn silent() -> Self { Tracing::All(TraceLevel::Silent) } diff --git a/crates/aiken-lang/src/builtins.rs b/crates/aiken-lang/src/builtins.rs index 0975965c6..3d9858cfe 100644 --- a/crates/aiken-lang/src/builtins.rs +++ b/crates/aiken-lang/src/builtins.rs @@ -1,4 +1,5 @@ use crate::{ + aiken_fn, ast::{ Annotation, ArgName, CallArg, DataTypeKey, Function, FunctionAccessKey, ModuleKind, OnTestFailure, Span, TypedArg, TypedDataType, TypedFunction, UnOp, @@ -268,6 +269,102 @@ pub fn prelude(id_gen: &IdGenerator) -> TypeInfo { ), ); + // enumerate + let enumerate_a = generic_var(id_gen.next()); + let enumerate_b = generic_var(id_gen.next()); + prelude.values.insert( + "enumerate".to_string(), + ValueConstructor::public( + function( + vec![ + list(enumerate_a.clone()), + enumerate_b.clone(), + function( + vec![enumerate_a.clone(), enumerate_b.clone()], + enumerate_b.clone(), + ), + function( + vec![enumerate_a.clone(), enumerate_b.clone()], + enumerate_b.clone(), + ), + ], + enumerate_b, + ), + ValueConstructorVariant::ModuleFn { + name: "enumerate".to_string(), + field_map: None, + module: "".to_string(), + arity: 4, + location: Span::empty(), + builtin: None, + }, + ), + ); + + // encode_base16 + prelude.values.insert( + "encode_base16".to_string(), + ValueConstructor::public( + function(vec![byte_array(), int(), byte_array()], byte_array()), + ValueConstructorVariant::ModuleFn { + name: "encode_base16".to_string(), + field_map: None, + module: "".to_string(), + arity: 3, + location: Span::empty(), + builtin: None, + }, + ), + ); + + // from_int + prelude.values.insert( + "from_int".to_string(), + ValueConstructor::public( + function(vec![int(), byte_array()], byte_array()), + ValueConstructorVariant::ModuleFn { + name: "from_int".to_string(), + field_map: None, + module: "".to_string(), + arity: 2, + location: Span::empty(), + builtin: None, + }, + ), + ); + + // do_from_int + prelude.values.insert( + "do_from_int".to_string(), + ValueConstructor::public( + function(vec![int(), byte_array()], byte_array()), + ValueConstructorVariant::ModuleFn { + name: "do_from_int".to_string(), + field_map: None, + module: "".to_string(), + arity: 2, + location: Span::empty(), + builtin: None, + }, + ), + ); + + // diagnostic + prelude.values.insert( + "diagnostic".to_string(), + ValueConstructor::public( + function(vec![data(), byte_array()], byte_array()), + ValueConstructorVariant::ModuleFn { + name: "diagnostic".to_string(), + field_map: None, + module: "".to_string(), + arity: 2, + location: Span::empty(), + builtin: None, + }, + ), + ); + // always let always_a_var = generic_var(id_gen.next()); let always_b_var = generic_var(id_gen.next()); @@ -919,7 +1016,10 @@ pub fn from_default_function(builtin: DefaultFunction, id_gen: &IdGenerator) -> ) } -pub fn prelude_functions(id_gen: &IdGenerator) -> IndexMap { +pub fn prelude_functions( + id_gen: &IdGenerator, + module_types: &HashMap, +) -> IndexMap { let mut functions = IndexMap::new(); // /// Negate the argument. Useful for map/fold and pipelines. @@ -1239,6 +1339,230 @@ pub fn prelude_functions(id_gen: &IdGenerator) -> IndexMap, + zero: b, + with: fn(a, b) -> b, + last: fn(a, b) -> b, + ) -> b { + when self is { + [] -> zero + [x] -> last(x, zero) + [x, ..xs] -> with(x, enumerate(xs, zero, with, last)) + } + } + "# + ), + ); + + functions.insert( + FunctionAccessKey { + module_name: "".to_string(), + function_name: "encode_base16".to_string(), + }, + aiken_fn!( + &module_types, + &id_gen, + r#" + use aiken/builtin + + fn encode_base16(bytes: ByteArray, ix: Int, builder: ByteArray) -> ByteArray { + if ix < 0 { + builder + } else { + let byte = builtin.index_bytearray(bytes, ix) + let msb = byte / 16 + let lsb = byte % 16 + let builder = + builtin.cons_bytearray( + msb + if msb < 10 { + 48 + } else { + 55 + }, + builtin.cons_bytearray( + lsb + if lsb < 10 { + 48 + } else { + 55 + }, + builder, + ), + ) + encode_base16(bytes, ix - 1, builder) + } + } + "# + ), + ); + + functions.insert( + FunctionAccessKey { + module_name: "".to_string(), + function_name: "do_from_int".to_string(), + }, + aiken_fn!( + &module_types, + &id_gen, + r#" + use aiken/builtin + + fn do_from_int(i: Int, digits: ByteArray) -> ByteArray { + if i <= 0 { + digits + } else { + do_from_int( + builtin.quotient_integer(i, 10), + builtin.cons_bytearray(builtin.remainder_integer(i, 10) + 48, digits), + ) + } + } + "# + ), + ); + + functions.insert( + FunctionAccessKey { + module_name: "".to_string(), + function_name: "from_int".to_string(), + }, + aiken_fn!( + &module_types, + &id_gen, + r#" + use aiken/builtin + + /// Encode an integer into UTF-8. + fn from_int(i: Int, digits: ByteArray) -> ByteArray { + if i == 0 { + builtin.append_bytearray(#"30", digits) + } else if i < 0 { + builtin.append_bytearray(#"2d", from_int(-i, digits)) + } else { + do_from_int( + builtin.quotient_integer(i, 10), + builtin.cons_bytearray(builtin.remainder_integer(i, 10) + 48, digits), + ) + } + } + "# + ), + ); + + functions.insert( + FunctionAccessKey { + module_name: "".to_string(), + function_name: "diagnostic".to_string(), + }, + aiken_fn!( + &module_types, + &id_gen, + r#" + use aiken/builtin + + fn diagnostic(self: Data, builder: ByteArray) -> ByteArray { + builtin.choose_data( + self, + { + let Pair(constr, fields) = builtin.un_constr_data(self) + + let builder = + when fields is { + [] -> builtin.append_bytearray(#"5b5d29", builder) + _ -> { + let bytes = + enumerate( + fields, + builtin.append_bytearray(#"5d29", builder), + fn(e: Data, st: ByteArray) { + diagnostic(e, builtin.append_bytearray(#"2c20", st)) + }, + fn(e: Data, st: ByteArray) { diagnostic(e, st) }, + ) + builtin.append_bytearray(#"5b5f20", bytes) + } + } + + let constr_tag = + if constr < 7 { + 121 + constr + } else if constr < 128 { + 1280 + constr - 7 + } else { + fail @"What are you doing? No I mean, seriously." + } + + builder + |> builtin.append_bytearray(#"28", _) + |> from_int(constr_tag, _) + }, + { + let elems = builtin.un_map_data(self) + when elems is { + [] -> builtin.append_bytearray(#"7b7d", builder) + _ -> { + let bytes = + enumerate( + elems, + builtin.append_bytearray(#"207d", builder), + fn(e: Pair, st: ByteArray) { + let value = diagnostic(e.2nd, builtin.append_bytearray(#"2c20", st)) + diagnostic(e.1st, builtin.append_bytearray(#"3a20", value)) + }, + fn(e: Pair, st: ByteArray) { + let value = diagnostic(e.2nd, st) + diagnostic(e.1st, builtin.append_bytearray(#"3a20", value)) + }, + ) + builtin.append_bytearray(#"7b5f20", bytes) + } + } + }, + { + let elems = builtin.un_list_data(self) + when elems is { + [] -> builtin.append_bytearray(#"5b5d", builder) + _ -> { + let bytes = + enumerate( + elems, + builtin.append_bytearray(#"5d", builder), + fn(e: Data, st: ByteArray) { + diagnostic(e, builtin.append_bytearray(#"2c20", st)) + }, + fn(e: Data, st: ByteArray) { diagnostic(e, st) }, + ) + builtin.append_bytearray(#"5b5f20", bytes) + } + } + }, + self + |> builtin.un_i_data + |> from_int(builder), + { + let bytes = builtin.un_b_data(self) + bytes + |> encode_base16( + builtin.length_of_bytearray(bytes) - 1, + builtin.append_bytearray(#"27", builder), + ) + |> builtin.append_bytearray(#"6827", _) + }, + ) + } + "# + ), + ); + functions } diff --git a/crates/aiken-lang/src/expr.rs b/crates/aiken-lang/src/expr.rs index 3571a4128..bc1505c4e 100644 --- a/crates/aiken-lang/src/expr.rs +++ b/crates/aiken-lang/src/expr.rs @@ -548,7 +548,8 @@ pub enum UntypedExpr { kind: TraceKind, location: Span, then: Box, - text: Box, + label: Box, + arguments: Vec, }, TraceIfFalse { @@ -1134,10 +1135,11 @@ impl UntypedExpr { location, kind: TraceKind::Todo, then: Box::new(UntypedExpr::ErrorTerm { location }), - text: Box::new(reason.unwrap_or_else(|| UntypedExpr::String { + label: Box::new(reason.unwrap_or_else(|| UntypedExpr::String { location, value: DEFAULT_TODO_STR.to_string(), })), + arguments: Vec::new(), } } @@ -1147,7 +1149,8 @@ impl UntypedExpr { location, kind: TraceKind::Error, then: Box::new(UntypedExpr::ErrorTerm { location }), - text: Box::new(reason), + label: Box::new(reason), + arguments: Vec::new(), } } else { UntypedExpr::ErrorTerm { location } diff --git a/crates/aiken-lang/src/format.rs b/crates/aiken-lang/src/format.rs index 4bcded89f..debb1d5c7 100644 --- a/crates/aiken-lang/src/format.rs +++ b/crates/aiken-lang/src/format.rs @@ -969,8 +969,12 @@ impl<'comments> Formatter<'comments> { } => self.assignment(patterns, value, *kind), UntypedExpr::Trace { - kind, text, then, .. - } => self.trace(kind, text, then), + kind, + label, + then, + arguments, + .. + } => self.trace(kind, label, arguments, then), UntypedExpr::When { subject, clauses, .. @@ -1037,26 +1041,34 @@ impl<'comments> Formatter<'comments> { pub fn trace<'a>( &mut self, kind: &'a TraceKind, - text: &'a UntypedExpr, + label: &'a UntypedExpr, + arguments: &'a [UntypedExpr], then: &'a UntypedExpr, ) -> Document<'a> { - let (keyword, default_text) = match kind { + let (keyword, default_label) = match kind { TraceKind::Trace => ("trace", None), TraceKind::Error => ("fail", Some(DEFAULT_ERROR_STR.to_string())), TraceKind::Todo => ("todo", Some(DEFAULT_TODO_STR.to_string())), }; - let body = match text { - UntypedExpr::String { value, .. } if Some(value) == default_text.as_ref() => { + let mut body = match label { + UntypedExpr::String { value, .. } if Some(value) == default_label.as_ref() => { keyword.to_doc() } _ => keyword .to_doc() .append(" ") - .append(self.wrap_expr(text)) + .append(self.wrap_expr(label)) .group(), }; + for (ix, arg) in arguments.iter().enumerate() { + body = body + .append(if ix == 0 { ": " } else { ", " }) + .append(self.wrap_expr(arg)) + .group(); + } + match kind { TraceKind::Error | TraceKind::Todo => body, TraceKind::Trace => body @@ -1095,7 +1107,6 @@ impl<'comments> Formatter<'comments> { if args.is_empty() && spread_location.is_some() { if is_record { name.append(" { .. }") - // TODO: not possible } else { name.append("(..)") } diff --git a/crates/aiken-lang/src/gen_uplc.rs b/crates/aiken-lang/src/gen_uplc.rs index f80b07e6d..707ff3e56 100644 --- a/crates/aiken-lang/src/gen_uplc.rs +++ b/crates/aiken-lang/src/gen_uplc.rs @@ -17,7 +17,7 @@ use crate::{ Span, TraceLevel, Tracing, TypedArg, TypedClause, TypedDataType, TypedFunction, TypedPattern, TypedValidator, UnOp, }, - builtins::{bool, data, int, list, void}, + builtins::{bool, data, int, list, void, PRELUDE}, expr::TypedExpr, gen_uplc::{ air::ExpectLevel, @@ -43,7 +43,6 @@ use itertools::Itertools; use petgraph::{algo, Graph}; use std::{collections::HashMap, rc::Rc}; use tree::Fields; - use uplc::{ ast::{Constant as UplcConstant, Name, NamedDeBruijn, Program, Term, Type as UplcType}, builder::{CONSTR_FIELDS_EXPOSER, CONSTR_INDEX_EXPOSER, EXPECT_ON_LIST}, @@ -749,7 +748,19 @@ impl<'a> CodeGenerator<'a> { } ModuleValueConstructor::Fn { name, module, .. } => { let func = self.functions.get(&FunctionAccessKey { - module_name: module_name.clone(), + // NOTE: This is needed because we register prelude functions under an + // empty module name. This is to facilitate their access when used + // directly. Note that, if we weren't doing this particular + // transformation, we would need to do the other direction anyway: + // + // if module_name.is_empty() { PRELUDE.to_string() } else { module_name.clone() } + // + // So either way, we need to take care of this. + module_name: if module_name == PRELUDE { + String::new() + } else { + module_name.clone() + }, function_name: name.clone(), }); diff --git a/crates/aiken-lang/src/lib.rs b/crates/aiken-lang/src/lib.rs index 5f52ceb78..2910c3f31 100644 --- a/crates/aiken-lang/src/lib.rs +++ b/crates/aiken-lang/src/lib.rs @@ -31,5 +31,36 @@ impl IdGenerator { } } +#[macro_export] +macro_rules! aiken_fn { + ($module_types:expr, $id_gen:expr, $src:expr) => {{ + let (untyped_module, _) = $crate::parser::module($src, $crate::ast::ModuleKind::Lib) + .expect("failed to parse module."); + + let module_name = ""; + + let mut warnings = vec![]; + + let typed_module = untyped_module + .infer( + $id_gen, + $crate::ast::ModuleKind::Lib, + module_name, + $module_types, + $crate::ast::Tracing::silent(), + &mut warnings, + ) + .unwrap(); + + if let Some($crate::ast::Definition::Fn(typed_fn)) = + typed_module.definitions.into_iter().last() + { + typed_fn + } else { + unreachable!() + } + }}; +} + #[cfg(test)] mod tests; diff --git a/crates/aiken-lang/src/parser/definition/snapshots/function_empty.snap b/crates/aiken-lang/src/parser/definition/snapshots/function_empty.snap index 036ca2be2..4f772cf60 100644 --- a/crates/aiken-lang/src/parser/definition/snapshots/function_empty.snap +++ b/crates/aiken-lang/src/parser/definition/snapshots/function_empty.snap @@ -11,10 +11,11 @@ Fn( then: ErrorTerm { location: 0..15, }, - text: String { + label: String { location: 0..15, value: "aiken::todo", }, + arguments: [], }, doc: None, location: 0..12, diff --git a/crates/aiken-lang/src/parser/definition/snapshots/function_non_public.snap b/crates/aiken-lang/src/parser/definition/snapshots/function_non_public.snap index 2e90a1ed7..c46c20457 100644 --- a/crates/aiken-lang/src/parser/definition/snapshots/function_non_public.snap +++ b/crates/aiken-lang/src/parser/definition/snapshots/function_non_public.snap @@ -11,10 +11,11 @@ Fn( then: ErrorTerm { location: 0..11, }, - text: String { + label: String { location: 0..11, value: "aiken::todo", }, + arguments: [], }, doc: None, location: 0..8, diff --git a/crates/aiken-lang/src/parser/error.rs b/crates/aiken-lang/src/parser/error.rs index 0212e042e..2877c7ba1 100644 --- a/crates/aiken-lang/src/parser/error.rs +++ b/crates/aiken-lang/src/parser/error.rs @@ -9,6 +9,30 @@ use std::collections::HashSet; #[derive(Debug, Clone, Diagnostic, thiserror::Error)] #[error("{kind}\n")] +#[diagnostic( + help( + "{}", + match kind { + ErrorKind::Unexpected(..) if !expected.is_empty() => { + format!( + "I am looking for one of the following patterns:\n{}", + expected + .iter() + .map(|x| format!( + "→ {}", + x.to_aiken() + .if_supports_color(Stdout, |s| s.purple()) + )) + .collect::>() + .join("\n") + ) + }, + _ => { + kind.help().map(|x| x.to_string()).unwrap_or_default() + } + } + ) +)] pub struct ParseError { pub kind: ErrorKind, #[label] @@ -28,6 +52,16 @@ impl ParseError { self } + pub fn expected_but_got(expected: Pattern, got: Pattern, span: Span) -> Self { + Self { + kind: ErrorKind::Unexpected(got), + expected: HashSet::from_iter([expected]), + span, + while_parsing: None, + label: None, + } + } + pub fn invalid_assignment_right_hand_side(span: Span) -> Self { Self { kind: ErrorKind::UnfinishedAssignmentRightHandSide, @@ -163,7 +197,7 @@ pub enum ErrorKind { UnexpectedEnd, #[error("{0}")] - #[diagnostic(help("{}", .0.help().unwrap_or_else(|| Box::new(""))))] + #[diagnostic(help("{}", .0.help().unwrap_or_else(|| Box::new("")))) ] Unexpected(Pattern), #[error("I discovered an invalid tuple index.")] @@ -221,7 +255,9 @@ pub enum ErrorKind { HybridNotationInByteArray, #[error("I failed to understand a when clause guard.")] - #[diagnostic(url("https://aiken-lang.org/language-tour/control-flow#checking-equality-and-ordering-in-patterns"))] + #[diagnostic(url( + "https://aiken-lang.org/language-tour/control-flow#checking-equality-and-ordering-in-patterns" + ))] #[diagnostic(help("{}", formatdoc! { r#"Clause guards are not as capable as standard expressions. While you can combine multiple clauses using '{operator_or}' and '{operator_and}', you can't do any arithmetic in there. They are mainly meant to compare pattern variables to some known constants using simple binary operators. @@ -278,15 +314,6 @@ pub enum Pattern { #[error("I found an unexpected token '{0}'.")] #[diagnostic(help("Try removing it!"))] Token(Token), - #[error("I found an unexpected literal value.")] - #[diagnostic(help("Try removing it!"))] - Literal, - #[error("I found an unexpected type name.")] - #[diagnostic(help("Try removing it!"))] - TypeIdent, - #[error("I found an unexpected identifier.")] - #[diagnostic(help("Try removing it!"))] - TermIdent, #[error("I found an unexpected end of input.")] End, #[error("I found a malformed list spread pattern.")] @@ -295,11 +322,6 @@ pub enum Pattern { #[error("I found an out-of-bound byte literal.")] #[diagnostic(help("Bytes must be between 0-255."))] Byte, - #[error("I found an unexpected pattern.")] - #[diagnostic(help( - "If no label is provided then only variables\nmatching a field name are allowed." - ))] - RecordPunning, #[error("I found an unexpected label.")] #[diagnostic(help("You can only use labels surrounded by curly braces"))] Label, @@ -308,11 +330,27 @@ pub enum Pattern { Discard, } +impl Pattern { + fn to_aiken(&self) -> String { + use Pattern::*; + match self { + Token(tok) => tok.to_string(), + Char(c) => c.to_string(), + End => "".to_string(), + Match => "A pattern (a discard, a var, etc...)".to_string(), + Byte => "A byte between [0; 255]".to_string(), + Label => "A label".to_string(), + Discard => "_".to_string(), + } + } +} + impl From for Pattern { fn from(c: char) -> Self { Self::Char(c) } } + impl From for Pattern { fn from(tok: Token) -> Self { Self::Token(tok) 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 618daaaea..aed2d6f31 100644 --- a/crates/aiken-lang/src/parser/expr/fail_todo_trace.rs +++ b/crates/aiken-lang/src/parser/expr/fail_todo_trace.rs @@ -2,7 +2,7 @@ use crate::{ ast::TraceKind, expr::UntypedExpr, parser::{ - error::ParseError, + error::{ParseError, Pattern}, expr::{string, when::clause}, token::Token, }, @@ -28,13 +28,36 @@ pub fn parser<'a>( .map_with_span(UntypedExpr::fail), just(Token::Trace) .ignore_then(choice((string::hybrid(), expression.clone()))) + .then( + choice((just(Token::Colon), just(Token::Comma))) + .then( + choice((string::hybrid(), expression.clone())) + .separated_by(just(Token::Comma)), + ) + .validate(|(token, arguments), span, emit| { + if token != Token::Colon { + emit(ParseError::expected_but_got( + Pattern::Token(Token::Colon), + Pattern::Token(token), + span.map(|start, _end| (start, start + 1)), + )) + } + + arguments + }) + .or_not() + .map(|opt| opt.unwrap_or_default()), + ) .then(sequence.clone().or_not()) - .map_with_span(|(text, then_), span| UntypedExpr::Trace { - kind: TraceKind::Trace, - location: span, - then: Box::new(then_.unwrap_or_else(|| UntypedExpr::todo(None, span))), - text: Box::new(text), - }), + .map_with_span( + |((label, arguments), continuation), span| UntypedExpr::Trace { + kind: TraceKind::Trace, + location: span, + then: Box::new(continuation.unwrap_or_else(|| UntypedExpr::todo(None, span))), + label: Box::new(label), + arguments, + }, + ), )) } @@ -114,6 +137,26 @@ mod tests { ); } + #[test] + fn trace_string() { + assert_expr!( + r#" + trace @"foo" + a + "# + ); + } + + #[test] + fn trace_bytearray() { + assert_expr!( + r#" + trace "foo" + a + "# + ); + } + #[test] fn trace_expr() { assert_expr!( @@ -132,4 +175,22 @@ mod tests { "# ); } + + #[test] + fn trace_labelled() { + assert_expr!( + r#" + trace foo: "bar" + "# + ); + } + + #[test] + fn trace_variadic() { + assert_expr!( + r#" + trace "foo": @"bar", baz + "# + ); + } } diff --git a/crates/aiken-lang/src/parser/expr/snapshots/error_basic.snap b/crates/aiken-lang/src/parser/expr/snapshots/error_basic.snap index 2a1d8c0de..75fca4bdb 100644 --- a/crates/aiken-lang/src/parser/expr/snapshots/error_basic.snap +++ b/crates/aiken-lang/src/parser/expr/snapshots/error_basic.snap @@ -8,8 +8,9 @@ Trace { then: ErrorTerm { location: 0..11, }, - text: String { + label: String { location: 5..11, value: "foo", }, + arguments: [], } diff --git a/crates/aiken-lang/src/parser/expr/snapshots/error_sugar.snap b/crates/aiken-lang/src/parser/expr/snapshots/error_sugar.snap index 1cb7d47e8..c106702aa 100644 --- a/crates/aiken-lang/src/parser/expr/snapshots/error_sugar.snap +++ b/crates/aiken-lang/src/parser/expr/snapshots/error_sugar.snap @@ -8,8 +8,9 @@ Trace { then: ErrorTerm { location: 0..10, }, - text: String { + label: String { location: 5..10, value: "foo", }, + arguments: [], } diff --git a/crates/aiken-lang/src/parser/expr/snapshots/fail_expr.snap b/crates/aiken-lang/src/parser/expr/snapshots/fail_expr.snap index 2e7d8190a..64277982c 100644 --- a/crates/aiken-lang/src/parser/expr/snapshots/fail_expr.snap +++ b/crates/aiken-lang/src/parser/expr/snapshots/fail_expr.snap @@ -8,7 +8,7 @@ Trace { then: ErrorTerm { location: 0..67, }, - text: Call { + label: Call { arguments: [ CallArg { label: None, @@ -51,4 +51,5 @@ Trace { }, location: 5..67, }, + arguments: [], } diff --git a/crates/aiken-lang/src/parser/expr/snapshots/if_soft_cast_discard_assign.snap b/crates/aiken-lang/src/parser/expr/snapshots/if_soft_cast_discard_assign.snap index aab73ee8d..a4f0bdada 100644 --- a/crates/aiken-lang/src/parser/expr/snapshots/if_soft_cast_discard_assign.snap +++ b/crates/aiken-lang/src/parser/expr/snapshots/if_soft_cast_discard_assign.snap @@ -20,10 +20,11 @@ If { then: ErrorTerm { location: 20..24, }, - text: String { + label: String { location: 20..24, value: "aiken::todo", }, + arguments: [], }, is: Some( AssignmentPattern { @@ -51,9 +52,10 @@ If { then: ErrorTerm { location: 36..40, }, - text: String { + label: String { location: 36..40, value: "aiken::todo", }, + arguments: [], }, } diff --git a/crates/aiken-lang/src/parser/expr/snapshots/if_soft_cast_not_var_condition.snap b/crates/aiken-lang/src/parser/expr/snapshots/if_soft_cast_not_var_condition.snap index e8fd4f35e..0bd080ce1 100644 --- a/crates/aiken-lang/src/parser/expr/snapshots/if_soft_cast_not_var_condition.snap +++ b/crates/aiken-lang/src/parser/expr/snapshots/if_soft_cast_not_var_condition.snap @@ -20,10 +20,11 @@ If { then: ErrorTerm { location: 31..35, }, - text: String { + label: String { location: 31..35, value: "aiken::todo", }, + arguments: [], }, is: Some( AssignmentPattern { @@ -68,9 +69,10 @@ If { then: ErrorTerm { location: 47..51, }, - text: String { + label: String { location: 47..51, value: "aiken::todo", }, + arguments: [], }, } diff --git a/crates/aiken-lang/src/parser/expr/snapshots/todo_basic.snap b/crates/aiken-lang/src/parser/expr/snapshots/todo_basic.snap index 9470a1a8f..4b5a651e3 100644 --- a/crates/aiken-lang/src/parser/expr/snapshots/todo_basic.snap +++ b/crates/aiken-lang/src/parser/expr/snapshots/todo_basic.snap @@ -8,8 +8,9 @@ Trace { then: ErrorTerm { location: 0..11, }, - text: String { + label: String { location: 5..11, value: "foo", }, + arguments: [], } diff --git a/crates/aiken-lang/src/parser/expr/snapshots/todo_empty.snap b/crates/aiken-lang/src/parser/expr/snapshots/todo_empty.snap index bc1fc0d11..16d45e791 100644 --- a/crates/aiken-lang/src/parser/expr/snapshots/todo_empty.snap +++ b/crates/aiken-lang/src/parser/expr/snapshots/todo_empty.snap @@ -8,8 +8,9 @@ Trace { then: ErrorTerm { location: 0..4, }, - text: String { + label: String { location: 0..4, value: "aiken::todo", }, + arguments: [], } diff --git a/crates/aiken-lang/src/parser/expr/snapshots/todo_expr.snap b/crates/aiken-lang/src/parser/expr/snapshots/todo_expr.snap index 9767c66a8..3dd7a2455 100644 --- a/crates/aiken-lang/src/parser/expr/snapshots/todo_expr.snap +++ b/crates/aiken-lang/src/parser/expr/snapshots/todo_expr.snap @@ -8,7 +8,7 @@ Trace { then: ErrorTerm { location: 0..32, }, - text: Call { + label: Call { arguments: [ CallArg { label: None, @@ -49,4 +49,5 @@ Trace { }, location: 5..32, }, + arguments: [], } diff --git a/crates/aiken-lang/src/parser/expr/snapshots/todo_sugar.snap b/crates/aiken-lang/src/parser/expr/snapshots/todo_sugar.snap index 6e9bf4c08..ff88606c9 100644 --- a/crates/aiken-lang/src/parser/expr/snapshots/todo_sugar.snap +++ b/crates/aiken-lang/src/parser/expr/snapshots/todo_sugar.snap @@ -8,8 +8,9 @@ Trace { then: ErrorTerm { location: 0..10, }, - text: String { + label: String { location: 5..10, value: "foo", }, + arguments: [], } diff --git a/crates/aiken-lang/src/parser/expr/snapshots/trace_bytearray.snap b/crates/aiken-lang/src/parser/expr/snapshots/trace_bytearray.snap new file mode 100644 index 000000000..63c4af229 --- /dev/null +++ b/crates/aiken-lang/src/parser/expr/snapshots/trace_bytearray.snap @@ -0,0 +1,17 @@ +--- +source: crates/aiken-lang/src/parser/expr/fail_todo_trace.rs +description: "Code:\n\ntrace \"foo\"\na\n" +--- +Trace { + kind: Trace, + location: 0..13, + then: Var { + location: 12..13, + name: "a", + }, + label: String { + location: 6..11, + value: "foo", + }, + arguments: [], +} diff --git a/crates/aiken-lang/src/parser/expr/snapshots/trace_expr.snap b/crates/aiken-lang/src/parser/expr/snapshots/trace_expr.snap index 783fa6e14..b64f649e0 100644 --- a/crates/aiken-lang/src/parser/expr/snapshots/trace_expr.snap +++ b/crates/aiken-lang/src/parser/expr/snapshots/trace_expr.snap @@ -9,7 +9,7 @@ Trace { location: 34..35, name: "a", }, - text: Call { + label: Call { arguments: [ CallArg { label: None, @@ -50,4 +50,5 @@ Trace { }, location: 6..33, }, + arguments: [], } diff --git a/crates/aiken-lang/src/parser/expr/snapshots/trace_expr_todo.snap b/crates/aiken-lang/src/parser/expr/snapshots/trace_expr_todo.snap index c0ecd7ace..ae65d4dfa 100644 --- a/crates/aiken-lang/src/parser/expr/snapshots/trace_expr_todo.snap +++ b/crates/aiken-lang/src/parser/expr/snapshots/trace_expr_todo.snap @@ -1,6 +1,6 @@ --- source: crates/aiken-lang/src/parser/expr/fail_todo_trace.rs -description: "Code:\n\ntrace some_var \n" +description: "Code:\n\ntrace some_var\n" --- Trace { kind: Trace, @@ -11,13 +11,15 @@ Trace { then: ErrorTerm { location: 0..14, }, - text: String { + label: String { location: 0..14, value: "aiken::todo", }, + arguments: [], }, - text: Var { + label: Var { location: 6..14, name: "some_var", }, + arguments: [], } diff --git a/crates/aiken-lang/src/parser/expr/snapshots/trace_labelled.snap b/crates/aiken-lang/src/parser/expr/snapshots/trace_labelled.snap new file mode 100644 index 000000000..3af8fa07f --- /dev/null +++ b/crates/aiken-lang/src/parser/expr/snapshots/trace_labelled.snap @@ -0,0 +1,30 @@ +--- +source: crates/aiken-lang/src/parser/expr/fail_todo_trace.rs +description: "Code:\n\ntrace foo: \"bar\"\n" +--- +Trace { + kind: Trace, + location: 0..16, + then: Trace { + kind: Todo, + location: 0..16, + then: ErrorTerm { + location: 0..16, + }, + label: String { + location: 0..16, + value: "aiken::todo", + }, + arguments: [], + }, + label: Var { + location: 6..9, + name: "foo", + }, + arguments: [ + String { + location: 11..16, + value: "bar", + }, + ], +} diff --git a/crates/aiken-lang/src/parser/expr/snapshots/trace_string.snap b/crates/aiken-lang/src/parser/expr/snapshots/trace_string.snap new file mode 100644 index 000000000..44459623a --- /dev/null +++ b/crates/aiken-lang/src/parser/expr/snapshots/trace_string.snap @@ -0,0 +1,17 @@ +--- +source: crates/aiken-lang/src/parser/expr/fail_todo_trace.rs +description: "Code:\n\ntrace @\"foo\"\na\n" +--- +Trace { + kind: Trace, + location: 0..14, + then: Var { + location: 13..14, + name: "a", + }, + label: String { + location: 6..12, + value: "foo", + }, + arguments: [], +} diff --git a/crates/aiken-lang/src/parser/expr/snapshots/trace_variadic.snap b/crates/aiken-lang/src/parser/expr/snapshots/trace_variadic.snap new file mode 100644 index 000000000..71ee71502 --- /dev/null +++ b/crates/aiken-lang/src/parser/expr/snapshots/trace_variadic.snap @@ -0,0 +1,34 @@ +--- +source: crates/aiken-lang/src/parser/expr/fail_todo_trace.rs +description: "Code:\n\ntrace \"foo\": @\"bar\", baz\n" +--- +Trace { + kind: Trace, + location: 0..24, + then: Trace { + kind: Todo, + location: 0..24, + then: ErrorTerm { + location: 0..24, + }, + label: String { + location: 0..24, + value: "aiken::todo", + }, + arguments: [], + }, + label: String { + location: 6..11, + value: "foo", + }, + arguments: [ + String { + location: 13..19, + value: "bar", + }, + Var { + location: 21..24, + name: "baz", + }, + ], +} diff --git a/crates/aiken-lang/src/parser/expr/when/snapshots/when_clause_double_todo.snap b/crates/aiken-lang/src/parser/expr/when/snapshots/when_clause_double_todo.snap index 186428889..7fe8c880d 100644 --- a/crates/aiken-lang/src/parser/expr/when/snapshots/when_clause_double_todo.snap +++ b/crates/aiken-lang/src/parser/expr/when/snapshots/when_clause_double_todo.snap @@ -32,10 +32,11 @@ When { then: ErrorTerm { location: 28..32, }, - text: String { + label: String { location: 28..32, value: "aiken::todo", }, + arguments: [], }, }, UntypedClause { @@ -61,10 +62,11 @@ When { then: ErrorTerm { location: 47..51, }, - text: String { + label: String { location: 47..51, value: "aiken::todo", }, + arguments: [], }, }, ], diff --git a/crates/aiken-lang/src/parser/expr/when/snapshots/when_clause_todo.snap b/crates/aiken-lang/src/parser/expr/when/snapshots/when_clause_todo.snap index bd0197bca..a437e8805 100644 --- a/crates/aiken-lang/src/parser/expr/when/snapshots/when_clause_todo.snap +++ b/crates/aiken-lang/src/parser/expr/when/snapshots/when_clause_todo.snap @@ -54,10 +54,11 @@ When { then: ErrorTerm { location: 47..68, }, - text: String { + label: String { location: 52..68, value: "unimplemented", }, + arguments: [], }, }, ], diff --git a/crates/aiken-lang/src/parser/token.rs b/crates/aiken-lang/src/parser/token.rs index 8f1fdbfaa..78c754ae3 100644 --- a/crates/aiken-lang/src/parser/token.rs +++ b/crates/aiken-lang/src/parser/token.rs @@ -183,6 +183,6 @@ impl fmt::Display for Token { Token::Validator => "validator", Token::Via => "via", }; - write!(f, "\"{s}\"") + write!(f, "{s}") } } diff --git a/crates/aiken-lang/src/tests/check.rs b/crates/aiken-lang/src/tests/check.rs index b157d0ce9..58d971b4f 100644 --- a/crates/aiken-lang/src/tests/check.rs +++ b/crates/aiken-lang/src/tests/check.rs @@ -18,6 +18,7 @@ fn check_module( ast: UntypedModule, extra: Vec<(String, UntypedModule)>, kind: ModuleKind, + tracing: Tracing, ) -> Result<(Vec, TypedModule), (Vec, Error)> { let id_gen = IdGenerator::new(); @@ -47,7 +48,7 @@ fn check_module( kind, "test/project", &module_types, - Tracing::All(TraceLevel::Verbose), + tracing, &mut warnings, ); @@ -57,20 +58,27 @@ fn check_module( } fn check(ast: UntypedModule) -> Result<(Vec, TypedModule), (Vec, Error)> { - check_module(ast, Vec::new(), ModuleKind::Lib) + check_module(ast, Vec::new(), ModuleKind::Lib, Tracing::verbose()) +} + +fn check_with_verbosity( + ast: UntypedModule, + level: TraceLevel, +) -> Result<(Vec, TypedModule), (Vec, Error)> { + check_module(ast, Vec::new(), ModuleKind::Lib, Tracing::All(level)) } fn check_with_deps( ast: UntypedModule, extra: Vec<(String, UntypedModule)>, ) -> Result<(Vec, TypedModule), (Vec, Error)> { - check_module(ast, extra, ModuleKind::Lib) + check_module(ast, extra, ModuleKind::Lib, Tracing::verbose()) } fn check_validator( ast: UntypedModule, ) -> Result<(Vec, TypedModule), (Vec, Error)> { - check_module(ast, Vec::new(), ModuleKind::Validator) + check_module(ast, Vec::new(), ModuleKind::Validator, Tracing::verbose()) } #[test] @@ -1288,8 +1296,32 @@ fn trace_non_strings() { True } "#; + assert!(check(parse(source_code)).is_ok()) +} + +#[test] +fn trace_string_label_compact() { + let source_code = r#" + test foo() { + trace @"foo": [1,2,3] + True + } + "#; + + assert!(check(parse(source_code)).is_ok()) +} + +#[test] +fn trace_non_string_label_compact() { + let source_code = r#" + test foo() { + trace(14 + 42) + True + } + "#; + assert!(matches!( - check(parse(source_code)), + check_with_verbosity(parse(source_code), TraceLevel::Compact), Err((_, Error::CouldNotUnify { .. })) )) } diff --git a/crates/aiken-lang/src/tests/format.rs b/crates/aiken-lang/src/tests/format.rs index 5fed0458b..ae567e674 100644 --- a/crates/aiken-lang/src/tests/format.rs +++ b/crates/aiken-lang/src/tests/format.rs @@ -985,3 +985,18 @@ fn format_validator_pattern() { "# ); } + +#[test] +fn format_variadic_trace() { + assert_format!( + r#" + fn foo() { + trace @"foo": @"bar" + trace "foo": "bar" + trace @"foo": "bar", @"baz" + trace bar: @"baz" + Void + } + "# + ); +} diff --git a/crates/aiken-lang/src/tests/snapshots/format_variadic_trace.snap b/crates/aiken-lang/src/tests/snapshots/format_variadic_trace.snap new file mode 100644 index 000000000..6d902e24e --- /dev/null +++ b/crates/aiken-lang/src/tests/snapshots/format_variadic_trace.snap @@ -0,0 +1,11 @@ +--- +source: crates/aiken-lang/src/tests/format.rs +description: "Code:\n\nfn foo() {\n trace @\"foo\": @\"bar\"\n trace \"foo\": \"bar\"\n trace @\"foo\": \"bar\", @\"baz\"\n trace bar: @\"baz\"\n Void\n}\n" +--- +fn foo() { + trace @"foo": @"bar" + trace @"foo": @"bar" + trace @"foo": @"bar", @"baz" + trace bar: @"baz" + Void +} diff --git a/crates/aiken-lang/src/tipo.rs b/crates/aiken-lang/src/tipo.rs index f29257c3a..7b8274cf2 100644 --- a/crates/aiken-lang/src/tipo.rs +++ b/crates/aiken-lang/src/tipo.rs @@ -12,10 +12,10 @@ use itertools::Itertools; use std::{cell::RefCell, collections::HashMap, ops::Deref, rc::Rc}; use uplc::{ast::Type as UplcType, builtins::DefaultFunction}; -mod environment; +pub(crate) mod environment; pub mod error; mod exhaustive; -mod expr; +pub(crate) mod expr; pub mod fields; mod hydrator; mod infer; diff --git a/crates/aiken-lang/src/tipo/expr.rs b/crates/aiken-lang/src/tipo/expr.rs index 102afa142..83fc492ca 100644 --- a/crates/aiken-lang/src/tipo/expr.rs +++ b/crates/aiken-lang/src/tipo/expr.rs @@ -19,12 +19,13 @@ use crate::{ UntypedRecordUpdateArg, }, builtins::{ - bool, byte_array, function, g1_element, g2_element, int, list, pair, string, tuple, void, + bool, byte_array, data, from_default_function, function, g1_element, g2_element, int, list, + pair, string, tuple, void, BUILTIN, }, expr::{FnStyle, TypedExpr, UntypedExpr}, format, - line_numbers::LineNumbers, - tipo::{fields::FieldMap, PatternConstructor, TypeVar}, + tipo::{fields::FieldMap, DefaultFunction, PatternConstructor, TypeVar}, + IdGenerator, }; use std::{ cmp::Ordering, @@ -39,7 +40,6 @@ pub(crate) fn infer_function( module_name: &str, hydrators: &mut HashMap, environment: &mut Environment<'_>, - lines: &LineNumbers, tracing: Tracing, ) -> Result, TypedExpr, TypedArg>, Error> { if let Some(typed_fun) = environment.inferred_functions.get(&fun.name) { @@ -94,7 +94,7 @@ pub(crate) fn infer_function( let preregistered_fn = environment .get_variable(name) - .expect("Could not find preregistered type for function"); + .unwrap_or_else(|| panic!("Could not find preregistered type for function: {name}")); let field_map = preregistered_fn.field_map().cloned(); @@ -120,7 +120,7 @@ pub(crate) fn infer_function( .remove(name) .unwrap_or_else(|| panic!("Could not find hydrator for fn {name}")); - let mut expr_typer = ExprTyper::new(environment, lines, tracing); + let mut expr_typer = ExprTyper::new(environment, tracing); expr_typer.hydrator = hydrator; expr_typer.not_yet_inferred = BTreeSet::from_iter(hydrators.keys().cloned()); @@ -153,12 +153,11 @@ pub(crate) fn infer_function( environment.current_module, hydrators, environment, - lines, tracing, )?; // Then, try again the entire function definition. - return infer_function(fun, module_name, hydrators, environment, lines, tracing); + return infer_function(fun, module_name, hydrators, environment, tracing); } let (arguments, body, return_type) = inferred?; @@ -221,8 +220,6 @@ pub(crate) fn infer_function( #[derive(Debug)] pub(crate) struct ExprTyper<'a, 'b> { - pub(crate) lines: &'a LineNumbers, - pub(crate) environment: &'a mut Environment<'b>, // We tweak the tracing behavior during type-check. Traces are either kept or left out of the @@ -242,18 +239,13 @@ pub(crate) struct ExprTyper<'a, 'b> { } impl<'a, 'b> ExprTyper<'a, 'b> { - pub fn new( - environment: &'a mut Environment<'b>, - lines: &'a LineNumbers, - tracing: Tracing, - ) -> Self { + pub fn new(environment: &'a mut Environment<'b>, tracing: Tracing) -> Self { Self { hydrator: Hydrator::new(), not_yet_inferred: BTreeSet::new(), environment, tracing, ungeneralised_function_used: false, - lines, } } @@ -516,9 +508,11 @@ impl<'a, 'b> ExprTyper<'a, 'b> { UntypedExpr::Trace { location, then, - text, + label, + arguments, kind, - } => self.infer_trace(kind, *then, location, *text), + .. + } => self.infer_trace(kind, *then, location, *label, arguments), UntypedExpr::When { location, @@ -677,25 +671,16 @@ impl<'a, 'b> ExprTyper<'a, 'b> { .to_pretty_string(999) ), }), - TraceLevel::Compact => Some(TypedExpr::String { - location, - tipo: string(), - value: self - .lines - .line_and_column_number(location.start) - .expect("Spans are within bounds.") - .to_string(), - }), - TraceLevel::Silent => None, + TraceLevel::Compact | TraceLevel::Silent => None, }; let typed_value = self.infer(value)?; self.unify(bool(), typed_value.tipo(), typed_value.location(), false)?; - match self.tracing.trace_level(false) { - TraceLevel::Silent => Ok(typed_value), - TraceLevel::Verbose | TraceLevel::Compact => Ok(TypedExpr::If { + match text { + None => Ok(typed_value), + Some(text) => Ok(TypedExpr::If { location, branches: vec1::vec1![IfBranch { condition: typed_value, @@ -706,7 +691,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { final_else: Box::new(TypedExpr::Trace { location, tipo: bool(), - text: Box::new(text.expect("TraceLevel::Silent excluded from pattern-guard")), + text: Box::new(text), then: Box::new(var_false), }), tipo: bool(), @@ -1793,7 +1778,7 @@ impl<'a, 'b> ExprTyper<'a, 'b> { }) } - fn infer_fn( + pub fn infer_fn( &mut self, args: Vec, expected_args: &[Rc], @@ -2403,15 +2388,29 @@ impl<'a, 'b> ExprTyper<'a, 'b> { TypedExpr::ErrorTerm { location, tipo } } + fn infer_trace_arg(&mut self, arg: UntypedExpr) -> Result { + let typed_arg = self.infer(arg)?; + match self.unify(string(), typed_arg.tipo(), typed_arg.location(), false) { + Err(_) => { + self.unify(data(), typed_arg.tipo(), typed_arg.location(), true)?; + Ok(diagnose_expr(typed_arg)) + } + Ok(()) => Ok(typed_arg), + } + } + fn infer_trace( &mut self, kind: TraceKind, then: UntypedExpr, location: Span, - text: UntypedExpr, + label: UntypedExpr, + arguments: Vec, ) -> Result { - let text = self.infer(text)?; - self.unify(string(), text.tipo(), text.location(), false)?; + let typed_arguments = arguments + .into_iter() + .map(|arg| self.infer_trace_arg(arg)) + .collect::, Error>>()?; let then = self.infer(then)?; let tipo = then.tipo(); @@ -2425,26 +2424,42 @@ impl<'a, 'b> ExprTyper<'a, 'b> { match self.tracing.trace_level(false) { TraceLevel::Silent => Ok(then), - TraceLevel::Compact => Ok(TypedExpr::Trace { - location, - tipo, - then: Box::new(then), - text: Box::new(TypedExpr::String { + TraceLevel::Compact => { + let text = self.infer(label)?; + self.unify(string(), text.tipo(), text.location(), false)?; + Ok(TypedExpr::Trace { location, - tipo: string(), - value: self - .lines - .line_and_column_number(location.start) - .expect("Spans are within bounds.") - .to_string(), - }), - }), - TraceLevel::Verbose => Ok(TypedExpr::Trace { - location, - tipo, - then: Box::new(then), - text: Box::new(text), - }), + tipo, + then: Box::new(then), + text: Box::new(text), + }) + } + TraceLevel::Verbose => { + let label = self.infer_trace_arg(label)?; + + let text = if typed_arguments.is_empty() { + label + } else { + let delimiter = |ix| TypedExpr::String { + location: Span::empty(), + tipo: string(), + value: if ix == 0 { ": " } else { ", " }.to_string(), + }; + typed_arguments + .into_iter() + .enumerate() + .fold(label, |text, (ix, arg)| { + append_string_expr(append_string_expr(text, delimiter(ix)), arg) + }) + }; + + Ok(TypedExpr::Trace { + location, + tipo, + then: Box::new(then), + text: Box::new(text), + }) + } } } @@ -2784,3 +2799,115 @@ pub fn ensure_serialisable(is_top_level: bool, t: Rc, location: Span) -> R } } } + +fn diagnose_expr(expr: TypedExpr) -> TypedExpr { + // NOTE: The IdGenerator is unused. See similar note in 'append_string_expr' + let decode_utf8_constructor = + from_default_function(DefaultFunction::DecodeUtf8, &IdGenerator::new()); + + let decode_utf8 = TypedExpr::ModuleSelect { + location: expr.location(), + tipo: decode_utf8_constructor.tipo.clone(), + label: DefaultFunction::DecodeUtf8.aiken_name(), + module_name: BUILTIN.to_string(), + module_alias: BUILTIN.to_string(), + constructor: decode_utf8_constructor.variant.to_module_value_constructor( + decode_utf8_constructor.tipo, + BUILTIN, + &DefaultFunction::AppendString.aiken_name(), + ), + }; + + let diagnostic = TypedExpr::Var { + location: expr.location(), + name: "diagnostic".to_string(), + constructor: ValueConstructor { + public: true, + tipo: function(vec![data(), byte_array()], byte_array()), + variant: ValueConstructorVariant::ModuleFn { + name: "diagnostic".to_string(), + field_map: None, + module: "".to_string(), + arity: 2, + location: Span::empty(), + builtin: None, + }, + }, + }; + + let location = expr.location(); + + TypedExpr::Call { + tipo: string(), + fun: Box::new(decode_utf8.clone()), + args: vec![CallArg { + label: None, + location: expr.location(), + value: TypedExpr::Call { + tipo: byte_array(), + fun: Box::new(diagnostic.clone()), + args: vec![ + CallArg { + label: None, + value: expr, + location, + }, + CallArg { + label: None, + location, + value: TypedExpr::ByteArray { + tipo: byte_array(), + bytes: vec![], + location, + }, + }, + ], + location, + }, + }], + location, + } +} + +fn append_string_expr(left: TypedExpr, right: TypedExpr) -> TypedExpr { + // NOTE: The IdGenerator is unused here, as it's only necessary for generic builtin + // functions such as if_then_else or head_list. However, if such functions were needed, + // passing a brand new IdGenerator here would be WRONG and cause issues down the line. + // + // So this is merely a small work-around for convenience. The proper way here would be to + // pull the function definition for append_string from the pre-registered builtins + // functions somewhere in the environment. + let value_constructor = + from_default_function(DefaultFunction::AppendString, &IdGenerator::new()); + + let append_string = TypedExpr::ModuleSelect { + location: Span::empty(), + tipo: value_constructor.tipo.clone(), + label: DefaultFunction::AppendString.aiken_name(), + module_name: BUILTIN.to_string(), + module_alias: BUILTIN.to_string(), + constructor: value_constructor.variant.to_module_value_constructor( + value_constructor.tipo, + BUILTIN, + &DefaultFunction::AppendString.aiken_name(), + ), + }; + + TypedExpr::Call { + location: Span::empty(), + tipo: string(), + fun: Box::new(append_string.clone()), + args: vec![ + CallArg { + label: None, + location: left.location(), + value: left, + }, + CallArg { + label: None, + location: right.location(), + value: right, + }, + ], + } +} diff --git a/crates/aiken-lang/src/tipo/infer.rs b/crates/aiken-lang/src/tipo/infer.rs index 075aa774b..b9a35b0e4 100644 --- a/crates/aiken-lang/src/tipo/infer.rs +++ b/crates/aiken-lang/src/tipo/infer.rs @@ -13,7 +13,6 @@ use crate::{ }, builtins, builtins::{fuzzer, generic_var}, - line_numbers::LineNumbers, tipo::{expr::infer_function, Span, Type, TypeVar}, IdGenerator, }; @@ -86,14 +85,8 @@ impl UntypedModule { } for def in consts.into_iter().chain(not_consts) { - let definition = infer_definition( - def, - &module_name, - &mut hydrators, - &mut environment, - &self.lines, - tracing, - )?; + let definition = + infer_definition(def, &module_name, &mut hydrators, &mut environment, tracing)?; definitions.push(definition); } @@ -162,7 +155,6 @@ fn infer_definition( module_name: &String, hydrators: &mut HashMap, environment: &mut Environment<'_>, - lines: &LineNumbers, tracing: Tracing, ) -> Result { match def { @@ -171,7 +163,6 @@ fn infer_definition( module_name, hydrators, environment, - lines, tracing, )?)), @@ -228,7 +219,7 @@ fn infer_definition( } let mut typed_fun = - infer_function(&fun, module_name, hydrators, environment, lines, tracing)?; + infer_function(&fun, module_name, hydrators, environment, tracing)?; if !typed_fun.return_type.is_bool() { return Err(Error::ValidatorMustReturnBool { @@ -267,14 +258,8 @@ fn infer_definition( let params = params.into_iter().chain(other.arguments); other.arguments = params.collect(); - let mut other_typed_fun = infer_function( - &other, - module_name, - hydrators, - environment, - lines, - tracing, - )?; + let mut other_typed_fun = + infer_function(&other, module_name, hydrators, environment, tracing)?; if !other_typed_fun.return_type.is_bool() { return Err(Error::ValidatorMustReturnBool { @@ -338,8 +323,7 @@ fn infer_definition( }); } - let typed_via = - ExprTyper::new(environment, lines, tracing).infer(arg.via.clone())?; + let typed_via = ExprTyper::new(environment, tracing).infer(arg.via.clone())?; let hydrator: &mut Hydrator = hydrators.get_mut(&f.name).unwrap(); @@ -404,14 +388,7 @@ fn infer_definition( None => Ok((None, None)), }?; - let typed_f = infer_function( - &f.into(), - module_name, - hydrators, - environment, - lines, - tracing, - )?; + let typed_f = infer_function(&f.into(), module_name, hydrators, environment, tracing)?; environment.unify( typed_f.return_type.clone(), @@ -629,7 +606,7 @@ fn infer_definition( tipo: _, }) => { let typed_expr = - ExprTyper::new(environment, lines, tracing).infer_const(&annotation, *value)?; + ExprTyper::new(environment, tracing).infer_const(&annotation, *value)?; let tipo = typed_expr.tipo(); diff --git a/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__free_vars.snap b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__free_vars.snap index af354a210..8f64d017a 100644 --- a/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__free_vars.snap +++ b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__free_vars.snap @@ -9,7 +9,7 @@ Schema { Var { tipo: RefCell { value: Generic { - id: 35, + id: 64, }, }, alias: None, diff --git a/crates/aiken-project/src/error.rs b/crates/aiken-project/src/error.rs index b262ed11f..b53980189 100644 --- a/crates/aiken-project/src/error.rs +++ b/crates/aiken-project/src/error.rs @@ -324,7 +324,7 @@ impl Diagnostic for Error { "Try moving the shared code to a separate module that the others can depend on\n- {}", modules.join("\n- ") ))), - Error::Parse { error, .. } => error.kind.help(), + Error::Parse { error, .. } => error.help(), Error::Type { error, .. } => error.help(), Error::StandardIo(_) => None, Error::MissingManifest { .. } => Some(Box::new( diff --git a/crates/aiken-project/src/lib.rs b/crates/aiken-project/src/lib.rs index 93196f5f1..6efab6156 100644 --- a/crates/aiken-project/src/lib.rs +++ b/crates/aiken-project/src/lib.rs @@ -130,7 +130,7 @@ where module_types.insert("aiken".to_string(), builtins::prelude(&id_gen)); module_types.insert("aiken/builtin".to_string(), builtins::plutus(&id_gen)); - let functions = builtins::prelude_functions(&id_gen); + let functions = builtins::prelude_functions(&id_gen, &module_types); let data_types = builtins::prelude_data_types(&id_gen); diff --git a/crates/aiken-project/src/snapshots/aiken_project__export__tests__cannot_export_generics.snap b/crates/aiken-project/src/snapshots/aiken_project__export__tests__cannot_export_generics.snap index 69be98f0a..32ab88819 100644 --- a/crates/aiken-project/src/snapshots/aiken_project__export__tests__cannot_export_generics.snap +++ b/crates/aiken-project/src/snapshots/aiken_project__export__tests__cannot_export_generics.snap @@ -9,7 +9,7 @@ Schema { Var { tipo: RefCell { value: Generic { - id: 35, + id: 64, }, }, alias: None, diff --git a/crates/aiken-project/src/test_framework.rs b/crates/aiken-project/src/test_framework.rs index a9ccebb9b..15d1f57d2 100644 --- a/crates/aiken-project/src/test_framework.rs +++ b/crates/aiken-project/src/test_framework.rs @@ -1328,7 +1328,7 @@ mod test { .last() .expect("No test found in declared src?"); - let mut functions = builtins::prelude_functions(&id_gen); + let mut functions = builtins::prelude_functions(&id_gen, &module_types); let mut data_types = builtins::prelude_data_types(&id_gen); ast.register_definitions(&mut functions, &mut data_types); diff --git a/crates/aiken-project/src/tests/gen_uplc.rs b/crates/aiken-project/src/tests/gen_uplc.rs index 20c88855d..0019836e4 100644 --- a/crates/aiken-project/src/tests/gen_uplc.rs +++ b/crates/aiken-project/src/tests/gen_uplc.rs @@ -7003,3 +7003,25 @@ fn bls12_381_elements_from_data_conversion() { false, ) } + +#[test] +fn qualified_prelude_functions() { + let src = r#" + use aiken + + test foo() { + aiken.identity(True) && identity(True) + } + "#; + + let constant_true = Term::Constant(Constant::Bool(true).into()); + let constant_false = Term::Constant(Constant::Bool(false).into()); + + assert_uplc( + src, + constant_true + .clone() + .delayed_if_then_else(constant_true, constant_false), + false, + ) +} diff --git a/crates/aiken-project/src/tests/mod.rs b/crates/aiken-project/src/tests/mod.rs index 94ddf664c..d0967542f 100644 --- a/crates/aiken-project/src/tests/mod.rs +++ b/crates/aiken-project/src/tests/mod.rs @@ -46,7 +46,7 @@ impl TestProject { module_types.insert("aiken".to_string(), builtins::prelude(&id_gen)); module_types.insert("aiken/builtin".to_string(), builtins::plutus(&id_gen)); - let functions = builtins::prelude_functions(&id_gen); + let functions = builtins::prelude_functions(&id_gen, &module_types); let data_types = builtins::prelude_data_types(&id_gen); TestProject {