From 444bccf19c6307602ef82c97c416dc6cb09b1cea Mon Sep 17 00:00:00 2001 From: microproofs Date: Tue, 30 Jan 2024 23:53:33 -0500 Subject: [PATCH] fix: change list_access_to_uplc to properly handle list discards --- crates/aiken-lang/src/gen_uplc/builder.rs | 213 ++++++++++++--------- examples/acceptance_tests/070/aiken.lock | 16 ++ examples/acceptance_tests/070/aiken.toml | 8 + examples/acceptance_tests/070/lib/tests.ak | 47 +++++ 4 files changed, 190 insertions(+), 94 deletions(-) create mode 100644 examples/acceptance_tests/070/aiken.lock create mode 100644 examples/acceptance_tests/070/aiken.toml create mode 100644 examples/acceptance_tests/070/lib/tests.ak diff --git a/crates/aiken-lang/src/gen_uplc/builder.rs b/crates/aiken-lang/src/gen_uplc/builder.rs index 7bf38564..25538724 100644 --- a/crates/aiken-lang/src/gen_uplc/builder.rs +++ b/crates/aiken-lang/src/gen_uplc/builder.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, ops::Deref, rc::Rc}; use indexmap::{IndexMap, IndexSet}; -use itertools::Itertools; +use itertools::{Itertools, Position}; use uplc::{ ast::{Constant as UplcConstant, Name, Term, Type as UplcType}, builder::{CONSTR_FIELDS_EXPOSER, CONSTR_INDEX_EXPOSER}, @@ -1530,127 +1530,152 @@ pub fn list_access_to_uplc( error_term: Term, ) -> Term { let names_len = names_types_ids.len(); + // Should never be expect level none on a list + assert!(!(matches!(expect_level, ExpectLevel::None) && is_list_accessor)); let mut no_tailing_discards = names_types_ids .iter() .rev() - .skip_while(|(name, _, _)| name == "_") + .with_position() + .skip_while(|pos| match pos { + // Items are reversed order + Position::Last((name, _, _)) | Position::Middle((name, _, _)) => { + name == "_" && matches!(expect_level, ExpectLevel::None) + } + Position::First((name, _, _)) | Position::Only((name, _, _)) => { + name == "_" && (tail_present || matches!(expect_level, ExpectLevel::None)) + } + }) + .map(|position| match position { + Position::First(a) | Position::Middle(a) | Position::Last(a) | Position::Only(a) => a, + }) .collect_vec(); - // If the the is just discards and check_last_item then we check for empty list + // If is just discards and check_last_item then we check for empty list if no_tailing_discards.is_empty() { - if !tail_present && matches!(expect_level, ExpectLevel::Full | ExpectLevel::Items) { + if tail_present || matches!(expect_level, ExpectLevel::None) { + return term.lambda("_"); + } else { return Term::var("empty_list") .delayed_choose_list(term, error_term) .lambda("empty_list"); - } else { - return term.lambda("_"); } } // reverse back to original order no_tailing_discards.reverse(); - let no_tailing_len = no_tailing_discards.len(); - // If we cut off at least one element then that was tail and possibly some heads let tail_wasnt_cutoff = tail_present && no_tailing_discards.len() == names_len; - no_tailing_discards.into_iter().enumerate().rev().fold( - term, - |acc, (index, (name, tipo, id))| { - let tail_name = format!("tail_index_{}_{}", index, id); + let tail_name = |id| format!("tail_id_{}", id); - let head_list = - if matches!(tipo.get_uplc_type(), UplcType::Pair(_, _)) && is_list_accessor { - Term::head_list().apply(Term::var(tail_name.to_string())) - } else if matches!(expect_level, ExpectLevel::Full) && error_term != Term::Error { - convert_data_to_type_debug( - Term::head_list().apply(Term::var(tail_name.to_string())), - &tipo.to_owned(), - error_term.clone(), - ) - } else { - convert_data_to_type( - Term::head_list().apply(Term::var(tail_name.to_string())), - &tipo.to_owned(), - ) - }; + let head_item = |name, tipo: &Rc, tail_name: &str| { + if name == "_" { + Term::unit() + } else if matches!(tipo.get_uplc_type(), UplcType::Pair(_, _)) && is_list_accessor { + Term::head_list().apply(Term::var(tail_name.to_string())) + } else if matches!(expect_level, ExpectLevel::Full) && error_term != Term::Error { + convert_data_to_type_debug( + Term::head_list().apply(Term::var(tail_name.to_string())), + &tipo.to_owned(), + error_term.clone(), + ) + } else { + convert_data_to_type( + Term::head_list().apply(Term::var(tail_name.to_string())), + &tipo.to_owned(), + ) + } + }; - // handle tail case - // name is guaranteed to not be discard at this point - if index == no_tailing_len - 1 && tail_wasnt_cutoff { - // simply lambda for tail name - acc.lambda(name) - } else if index == no_tailing_len - 1 { - // case for no tail - // name is guaranteed to not be discard at this point + // Remember we reverse here so the First or Only is the last item + no_tailing_discards + .into_iter() + .rev() + .with_position() + .fold(term, |acc, position| { + match position { + Position::First((name, _, _)) | Position::Only((name, _, _)) + if tail_wasnt_cutoff => + { + // case for tail as the last item + acc.lambda(name) + } - match expect_level { - ExpectLevel::None => acc.lambda(name).apply(head_list).lambda(tail_name), - ExpectLevel::Full | ExpectLevel::Items => { - if error_term == Term::Error && tail_present { - acc.lambda(name).apply(head_list).lambda(tail_name) - } else if tail_present { - // Custom error instead of trying to do head_list on a possibly empty list. - Term::var(tail_name.to_string()) - .delayed_choose_list( - error_term.clone(), - acc.lambda(name).apply(head_list), - ) - .lambda(tail_name) - } else if error_term == Term::Error { - // Check head is last item in this list - Term::tail_list() - .apply(Term::var(tail_name.to_string())) - .delayed_choose_list(acc, error_term.clone()) - .lambda(name) - .apply(head_list) - .lambda(tail_name) - } else { - // Custom error if list is not empty after this head - Term::var(tail_name.to_string()) - .delayed_choose_list( - error_term.clone(), - Term::tail_list() - .apply(Term::var(tail_name.to_string())) - .delayed_choose_list(acc, error_term.clone()) - .lambda(name) - .apply(head_list), - ) - .lambda(tail_name) + Position::First((name, tipo, id)) | Position::Only((name, tipo, id)) => { + // case for no tail, but last item + let tail_name = tail_name(id); + + let head_item = head_item(name, tipo, &tail_name); + + match expect_level { + ExpectLevel::None => acc.lambda(name).apply(head_item).lambda(tail_name), + + ExpectLevel::Full | ExpectLevel::Items => { + if error_term == Term::Error && tail_present { + // No need to check last item if tail was present + acc.lambda(name).apply(head_item).lambda(tail_name) + } else if tail_present { + // Custom error instead of trying to do head_item on a possibly empty list. + Term::var(tail_name.to_string()) + .delayed_choose_list( + error_term.clone(), + acc.lambda(name).apply(head_item), + ) + .lambda(tail_name) + } else if error_term == Term::Error { + // Check head is last item in this list + Term::tail_list() + .apply(Term::var(tail_name.to_string())) + .delayed_choose_list(acc, error_term.clone()) + .lambda(name) + .apply(head_item) + .lambda(tail_name) + } else { + // Custom error if list is not empty after this head + Term::var(tail_name.to_string()) + .delayed_choose_list( + error_term.clone(), + Term::tail_list() + .apply(Term::var(tail_name.to_string())) + .delayed_choose_list(acc, error_term.clone()) + .lambda(name) + .apply(head_item), + ) + .lambda(tail_name) + } } } } - } else if name == "_" { - if matches!(expect_level, ExpectLevel::None) || error_term == Term::Error { - acc.apply(Term::tail_list().apply(Term::var(tail_name.to_string()))) - .lambda(tail_name) - } else { - Term::var(tail_name.to_string()) - .delayed_choose_list( - error_term.clone(), - acc.apply(Term::tail_list().apply(Term::var(tail_name.to_string()))), - ) - .lambda(tail_name) - } - } else if matches!(expect_level, ExpectLevel::None) || error_term == Term::Error { - acc.apply(Term::tail_list().apply(Term::var(tail_name.to_string()))) - .lambda(name) - .apply(head_list) - .lambda(tail_name) - } else { - Term::var(tail_name.to_string()) - .delayed_choose_list( - error_term.clone(), + + Position::Middle((name, tipo, id)) | Position::Last((name, tipo, id)) => { + // case for every item except the last item + let tail_name = tail_name(id); + + let head_item = head_item(name, tipo, &tail_name); + + if matches!(expect_level, ExpectLevel::None) || error_term == Term::Error { acc.apply(Term::tail_list().apply(Term::var(tail_name.to_string()))) .lambda(name) - .apply(head_list), - ) - .lambda(tail_name) + .apply(head_item) + .lambda(tail_name) + } else { + // case for a custom error if the list is empty at this point + Term::var(tail_name.to_string()) + .delayed_choose_list( + error_term.clone(), + acc.apply( + Term::tail_list().apply(Term::var(tail_name.to_string())), + ) + .lambda(name) + .apply(head_item), + ) + .lambda(tail_name) + } + } } - }, - ) + }) } pub fn apply_builtin_forces(mut term: Term, force_count: u32) -> Term { diff --git a/examples/acceptance_tests/070/aiken.lock b/examples/acceptance_tests/070/aiken.lock new file mode 100644 index 00000000..2625bc84 --- /dev/null +++ b/examples/acceptance_tests/070/aiken.lock @@ -0,0 +1,16 @@ +# This file was generated by Aiken +# You typically do not need to edit this file + +[[requirements]] +name = "aiken-lang/stdlib" +version = "main" +source = "github" + +[[packages]] +name = "aiken-lang/stdlib" +version = "main" +requirements = [] +source = "github" + +[etags] +"aiken-lang/stdlib@main" = [{ secs_since_epoch = 1706674613, nanos_since_epoch = 871553000 }, "cf946239d3dd481ed41f20e56bf24910b5229ea35aa171a708edc2a47fc20a7b"] diff --git a/examples/acceptance_tests/070/aiken.toml b/examples/acceptance_tests/070/aiken.toml new file mode 100644 index 00000000..d000db37 --- /dev/null +++ b/examples/acceptance_tests/070/aiken.toml @@ -0,0 +1,8 @@ +name = "aiken-lang/acceptance_test_070" +version = '0.0.0' +description = '' + +[[dependencies]] +name = 'aiken-lang/stdlib' +version = 'main' +source = 'github' diff --git a/examples/acceptance_tests/070/lib/tests.ak b/examples/acceptance_tests/070/lib/tests.ak new file mode 100644 index 00000000..d5ed1ef6 --- /dev/null +++ b/examples/acceptance_tests/070/lib/tests.ak @@ -0,0 +1,47 @@ +use aiken/list +use aiken/transaction.{InlineDatum, Input, OutputReference, TransactionId} + +type OtherInput { + output_reference: OutputReference, + other: Data, +} + +type MyDatum { + Constructor1(a) + Constructor2 +} + +test discard_partitions() { + let all_inputs = + [ + OtherInput(OutputReference(TransactionId(#"aabb"), 2), 3), + OtherInput(OutputReference(TransactionId(#"aabbcc"), 3), 3), + ] + + let own_out_ref = OutputReference(TransactionId(#"aabb"), 2) + + expect ([_], other_inputs) = + list.partition( + all_inputs, + fn(input) { input.output_reference == own_out_ref }, + ) + + let inputs: List = + [] + + list.all( + inputs, + fn(input) { + expect dat: MyDatum = + when input.output.datum is { + InlineDatum(d) -> d + _ -> fail @"Not an inline datum" + } + + when dat is { + Constructor1 { .. } -> True + _ -> False + } + }, + ) +}