use std::collections::HashSet;
use blake2::{
Blake2bVar,
digest::{Update, VariableOutput},
};
use crate::{
ir::{
MatchArm as AeriMatchArm, Pattern as AeriPattern, Program as AeriProgram, Term as AeriTerm,
},
types::{Type, is_transaction_builtin},
};
use serde::Serialize;
const MATCH_SUBJECT_BINDER: &str = "aeri_match_subject";
const SEQUENCE_DISCARD_BINDER: &str = "aeri_seq_discard";
const SUPPORTED_UPLC_VERSION: (u8, u8, u8) = (1, 0, 0);
pub const CARDANO_SCRIPT_VERSION: u8 = 2;
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct LedgerProgram {
pub text: String,
pub flat_hex: String,
pub cbor_hex: String,
pub script_hash: String,
pub script_json: PlutusScriptJson,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct PlutusScriptJson {
#[serde(rename = "type")]
pub script_type: String,
pub description: String,
pub cbor_hex: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LedgerEmitError {
pub blockers: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CustomTypeLayout {
pub name: String,
pub variants: Vec<CustomTypeVariantLayout>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CustomTypeVariantLayout {
pub tag: usize,
pub field_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Program {
pub version: (u8, u8, u8),
pub term: Term,
}
impl Program {
pub fn render(&self) -> String {
format!(
"(uplc-program {}.{}.{} {})",
self.version.0,
self.version.1,
self.version.2,
self.term.render()
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Term {
Var(String),
Lambda {
param: String,
body: Box<Term>,
},
Apply {
function: Box<Term>,
argument: Box<Term>,
},
Delay(Box<Term>),
Force(Box<Term>),
Constant(Constant),
Builtin(String),
Error,
}
impl Term {
fn render(&self) -> String {
match self {
Self::Var(name) => name.clone(),
Self::Lambda { param, body } => format!("(lam {param} {})", body.render()),
Self::Apply { function, argument } => {
format!("(apply {} {})", function.render(), argument.render())
}
Self::Delay(term) => format!("(delay {})", term.render()),
Self::Force(term) => format!("(force {})", term.render()),
Self::Constant(constant) => constant.render(),
Self::Builtin(name) => format!("(builtin {name})"),
Self::Error => "(error)".to_string(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Constant {
Bool(bool),
Int(i64),
String(String),
ByteArray(String),
Unit,
}
impl Constant {
fn render(&self) -> String {
match self {
Self::Bool(true) => "(con bool True)".to_string(),
Self::Bool(false) => "(con bool False)".to_string(),
Self::Int(value) => format!("(con integer {value})"),
Self::String(value) => format!("(con string \"{}\")", escape(value)),
Self::ByteArray(hex) => format!("(con bytes #{hex})"),
Self::Unit => "(con unit ())".to_string(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoweringReport {
pub program: Option<Program>,
pub blockers: Vec<String>,
}
impl LoweringReport {
pub fn lowered(&self) -> bool {
self.program.is_some() && self.blockers.is_empty()
}
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ValidationReport {
pub valid: bool,
pub errors: Vec<String>,
pub nodes: usize,
pub max_depth: usize,
pub builtin_applications: Vec<BuiltinApplication>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct BuiltinApplication {
pub name: String,
pub expected_arity: usize,
pub applied_args: usize,
}
#[derive(Debug, Default)]
struct ValidationState {
errors: Vec<String>,
nodes: usize,
max_depth: usize,
builtin_applications: Vec<BuiltinApplication>,
}
pub fn validate_program(program: &Program) -> ValidationReport {
let mut state = ValidationState::default();
let mut scope = Vec::new();
validate_version(program.version, &mut state);
validate_term(&program.term, &mut scope, 1, false, false, &mut state);
state.errors.sort();
state.errors.dedup();
ValidationReport {
valid: state.errors.is_empty(),
errors: state.errors,
nodes: state.nodes,
max_depth: state.max_depth,
builtin_applications: state.builtin_applications,
}
}
fn validate_version(version: (u8, u8, u8), state: &mut ValidationState) {
if version == SUPPORTED_UPLC_VERSION {
return;
}
state.errors.push(format!(
"unsupported UPLC version {}.{}.{}, expected {}.{}.{}",
version.0,
version.1,
version.2,
SUPPORTED_UPLC_VERSION.0,
SUPPORTED_UPLC_VERSION.1,
SUPPORTED_UPLC_VERSION.2
));
}
pub fn lower_program(program: &AeriProgram) -> LoweringReport {
let mut blockers = Vec::new();
let term = lower_term(&program.term, &mut blockers);
blockers.sort();
blockers.dedup();
LoweringReport {
program: term.filter(|_| blockers.is_empty()).map(|term| Program {
version: program.version,
term,
}),
blockers,
}
}
pub fn emit_ledger_program(
program: &AeriProgram,
param_types: &[(String, Type)],
) -> Result<LedgerProgram, LedgerEmitError> {
emit_ledger_program_with_layouts(program, param_types, &[])
}
pub fn emit_ledger_program_with_layouts(
program: &AeriProgram,
param_types: &[(String, Type)],
custom_types: &[CustomTypeLayout],
) -> Result<LedgerProgram, LedgerEmitError> {
let report = lower_program(program);
let mut blockers = report.blockers;
let Some(mut program) = report.program else {
blockers.sort();
blockers.dedup();
return Err(LedgerEmitError { blockers });
};
program = wrap_validator_bool_result(program);
program = apply_validator_data_decoders(program, param_types, custom_types, &mut blockers);
let validation = validate_program(&program);
if !validation.valid {
blockers.extend(
validation
.errors
.into_iter()
.map(|error| format!("UPLC validation failed: {error}")),
);
}
if !blockers.is_empty() {
blockers.sort();
blockers.dedup();
return Err(LedgerEmitError { blockers });
}
let flat_bytes = flat_encode_program(&program).map_err(|error| LedgerEmitError {
blockers: vec![format!("UPLC flat encoding failed: {error}")],
})?;
let cbor_bytes = cbor_bytestring(&flat_bytes);
let cbor_hex = hex_encode(&cbor_bytes);
let script_hash = script_hash(CARDANO_SCRIPT_VERSION, &flat_bytes);
Ok(LedgerProgram {
text: program.render(),
flat_hex: hex_encode(&flat_bytes),
cbor_hex: cbor_hex.clone(),
script_hash,
script_json: PlutusScriptJson {
script_type: format!("PlutusScriptV{CARDANO_SCRIPT_VERSION}"),
description: "Aeri generated UPLC script".to_string(),
cbor_hex,
},
})
}
fn apply_validator_data_decoders(
program: Program,
param_types: &[(String, Type)],
custom_types: &[CustomTypeLayout],
blockers: &mut Vec<String>,
) -> Program {
Program {
version: program.version,
term: apply_data_decoders_to_lambdas(program.term, param_types, custom_types, blockers),
}
}
fn apply_data_decoders_to_lambdas(
term: Term,
param_types: &[(String, Type)],
custom_types: &[CustomTypeLayout],
blockers: &mut Vec<String>,
) -> Term {
if param_types.is_empty() {
return term;
}
let Term::Lambda { param, body } = term else {
blockers.push(
"validator UPLC program must expose one lambda per validator parameter".to_string(),
);
return Term::Error;
};
let (expected_name, ty) = ¶m_types[0];
if ¶m != expected_name {
blockers.push(format!(
"validator parameter order mismatch while preparing ledger script: expected '{expected_name}', found '{param}'"
));
}
let body = apply_data_decoders_to_lambdas(*body, ¶m_types[1..], custom_types, blockers);
if !has_data_decoder_for_type(ty, custom_types) {
if supports_raw_ledger_type(ty) {
return Term::Lambda {
param,
body: Box::new(body),
};
}
blockers.push(format!(
"validator parameter '{param}: {ty}' needs Plutus Data decoder lowering"
));
return Term::Lambda {
param,
body: Box::new(body),
};
}
let mut names = HashSet::new();
collect_names(&body, &mut names);
let raw_param = fresh_name(&format!("{param}_data"), &names);
names.insert(raw_param.clone());
let decoded =
match decoder_term_for_type(ty, Term::Var(raw_param.clone()), &mut names, custom_types) {
Ok(decoded) => decoded.expect("decoder support was checked before term construction"),
Err(blocker) => {
blockers.push(blocker);
return Term::Lambda {
param,
body: Box::new(body),
};
}
};
Term::Lambda {
param: raw_param,
body: Box::new(apply(
Term::Lambda {
param,
body: Box::new(body),
},
decoded,
)),
}
}
fn has_data_decoder_for_type(ty: &Type, custom_types: &[CustomTypeLayout]) -> bool {
matches!(
ty,
Type::Bool | Type::Int | Type::ByteArray | Type::String | Type::Unit
) || matches!(ty, Type::Custom(name) if custom_type_layout(name, custom_types).is_some())
|| matches!(ty, Type::List(item) if has_list_data_decoder_for_item(item, custom_types))
}
fn has_list_data_decoder_for_item(item: &Type, custom_types: &[CustomTypeLayout]) -> bool {
matches!(
item,
Type::Bool | Type::Int | Type::ByteArray | Type::Data | Type::String | Type::Unit
) || matches!(
item,
Type::Custom(name) if custom_type_layout(name, custom_types)
.is_some_and(custom_type_layout_is_nullary)
)
}
fn decoder_term_for_type(
ty: &Type,
raw: Term,
names: &mut HashSet<String>,
custom_types: &[CustomTypeLayout],
) -> Result<Option<Term>, String> {
Ok(match ty {
Type::Int => Some(apply_builtin("unIData", vec![raw])),
Type::ByteArray => Some(apply_builtin("unBData", vec![raw])),
Type::String => Some(apply_builtin(
"decodeUtf8",
vec![apply_builtin("unBData", vec![raw])],
)),
Type::Unit => Some(unit_data_decoder(raw, names)),
Type::Bool => Some(bool_data_decoder(raw, names)),
Type::List(item) if has_list_data_decoder_for_item(item, custom_types) => {
Some(list_data_decoder(raw, item, names, custom_types)?)
}
Type::Custom(name) => custom_type_layout(name, custom_types)
.map(|layout| custom_data_decoder(raw, layout, names))
.transpose()?,
_ => None,
})
}
fn list_data_decoder(
raw: Term,
item: &Type,
names: &mut HashSet<String>,
custom_types: &[CustomTypeLayout],
) -> Result<Term, String> {
let list = fresh_name("aeri_list_data", names);
names.insert(list.clone());
let checked = fresh_name("aeri_list_checked", names);
names.insert(checked.clone());
let raw_list = apply_builtin("unListData", vec![raw]);
let validation = validate_data_list(Term::Var(list.clone()), item, names, custom_types)?;
Ok(bind_terms_in_order(
vec![(list.clone(), raw_list), (checked, validation)],
Term::Var(list),
))
}
fn validate_data_list(
list: Term,
item: &Type,
names: &mut HashSet<String>,
custom_types: &[CustomTypeLayout],
) -> Result<Term, String> {
collect_names(&list, names);
let self_name = fresh_name("aeri_list_validate_loop", names);
names.insert(self_name.clone());
let list_name = fresh_name("aeri_list_validate_items", names);
names.insert(list_name.clone());
let head_name = fresh_name("aeri_list_validate_head", names);
names.insert(head_name.clone());
let checked_name = fresh_name("aeri_list_validate_item", names);
names.insert(checked_name.clone());
let head = apply_forced_builtin_n("headList", 1, vec![Term::Var(list_name.clone())]);
let item_validation =
validate_data_list_item(item, Term::Var(head_name.clone()), names, custom_types)?;
let recurse = apply(
apply(Term::Var(self_name.clone()), Term::Var(self_name.clone())),
apply_forced_builtin_n("tailList", 1, vec![Term::Var(list_name.clone())]),
);
let non_empty = bind_terms_in_order(
vec![(head_name, head), (checked_name, item_validation)],
recurse,
);
Ok(apply(
apply(
Term::Lambda {
param: self_name.clone(),
body: Box::new(apply(
Term::Var(self_name.clone()),
Term::Var(self_name.clone()),
)),
},
Term::Lambda {
param: self_name.clone(),
body: Box::new(Term::Lambda {
param: list_name.clone(),
body: Box::new(lower_if(
apply_forced_builtin_n("nullList", 1, vec![Term::Var(list_name)]),
Term::Constant(Constant::Unit),
non_empty,
)),
}),
},
),
list,
))
}
fn validate_data_list_item(
item: &Type,
raw: Term,
names: &mut HashSet<String>,
custom_types: &[CustomTypeLayout],
) -> Result<Term, String> {
match item {
Type::Data => Ok(Term::Constant(Constant::Unit)),
Type::Bool | Type::Int | Type::ByteArray | Type::String | Type::Unit => {
decoder_term_for_type(item, raw, names, custom_types).map(|term| {
term.expect("primitive list item decoder is available after support check")
})
}
Type::Custom(name) => custom_type_layout(name, custom_types)
.filter(|layout| custom_type_layout_is_nullary(layout))
.map(|layout| custom_data_decoder(raw, layout, names))
.transpose()?
.ok_or_else(|| {
format!("list parameter item type '{item}' needs Plutus Data decoder lowering")
}),
_ => Err(format!(
"list parameter item type '{item}' needs Plutus Data decoder lowering"
)),
}
}
fn custom_type_layout<'a>(
name: &str,
custom_types: &'a [CustomTypeLayout],
) -> Option<&'a CustomTypeLayout> {
custom_types.iter().find(|layout| layout.name == name)
}
fn custom_type_layout_is_nullary(layout: &CustomTypeLayout) -> bool {
layout
.variants
.iter()
.all(|variant| variant.field_count == 0)
}
fn custom_tag_data_decoder(raw: Term, names: &mut HashSet<String>) -> Term {
let constr = fresh_name("aeri_custom_tag_constr", names);
names.insert(constr.clone());
let tag = fresh_name("aeri_custom_tag", names);
names.insert(tag.clone());
let fields = fresh_name("aeri_custom_tag_fields", names);
names.insert(fields.clone());
let body = lower_if(
apply_forced_builtin_n("nullList", 1, vec![Term::Var(fields.clone())]),
Term::Var(tag.clone()),
Term::Error,
);
bind_terms_in_order(
vec![
(constr.clone(), apply_builtin("unConstrData", vec![raw])),
(
tag,
apply_forced_builtin_n("fstPair", 2, vec![Term::Var(constr.clone())]),
),
(
fields,
apply_forced_builtin_n("sndPair", 2, vec![Term::Var(constr)]),
),
],
body,
)
}
fn unit_data_decoder(raw: Term, names: &mut HashSet<String>) -> Term {
let constr = fresh_name("aeri_unit_constr", names);
names.insert(constr.clone());
let tag = fresh_name("aeri_unit_tag", names);
names.insert(tag.clone());
let fields = fresh_name("aeri_unit_fields", names);
names.insert(fields.clone());
let tag_is_unit = apply_builtin(
"equalsInteger",
vec![Term::Var(tag.clone()), Term::Constant(Constant::Int(0))],
);
let decoded_tag = lower_if(tag_is_unit, Term::Constant(Constant::Unit), Term::Error);
let body = lower_if(
apply_forced_builtin_n("nullList", 1, vec![Term::Var(fields.clone())]),
decoded_tag,
Term::Error,
);
bind_terms_in_order(
vec![
(constr.clone(), apply_builtin("unConstrData", vec![raw])),
(
tag,
apply_forced_builtin_n("fstPair", 2, vec![Term::Var(constr.clone())]),
),
(
fields,
apply_forced_builtin_n("sndPair", 2, vec![Term::Var(constr)]),
),
],
body,
)
}
fn bool_data_decoder(raw: Term, names: &mut HashSet<String>) -> Term {
let constr = fresh_name("aeri_bool_constr", names);
names.insert(constr.clone());
let tag = fresh_name("aeri_bool_tag", names);
names.insert(tag.clone());
let fields = fresh_name("aeri_bool_fields", names);
names.insert(fields.clone());
let tag_is_false = apply_builtin(
"equalsInteger",
vec![Term::Var(tag.clone()), Term::Constant(Constant::Int(0))],
);
let tag_is_true = apply_builtin(
"equalsInteger",
vec![Term::Var(tag.clone()), Term::Constant(Constant::Int(1))],
);
let decoded_tag = lower_if(
tag_is_false,
Term::Constant(Constant::Bool(false)),
lower_if(
tag_is_true,
Term::Constant(Constant::Bool(true)),
Term::Error,
),
);
let body = lower_if(
apply_forced_builtin_n("nullList", 1, vec![Term::Var(fields.clone())]),
decoded_tag,
Term::Error,
);
bind_terms_in_order(
vec![
(constr.clone(), apply_builtin("unConstrData", vec![raw])),
(
tag,
apply_forced_builtin_n("fstPair", 2, vec![Term::Var(constr.clone())]),
),
(
fields,
apply_forced_builtin_n("sndPair", 2, vec![Term::Var(constr)]),
),
],
body,
)
}
fn custom_data_decoder(
raw: Term,
layout: &CustomTypeLayout,
names: &mut HashSet<String>,
) -> Result<Term, String> {
let constr = fresh_name("aeri_custom_constr", names);
names.insert(constr.clone());
let tag = fresh_name("aeri_custom_tag", names);
names.insert(tag.clone());
let fields = fresh_name("aeri_custom_fields", names);
names.insert(fields.clone());
let mut body = Term::Error;
for variant in layout.variants.iter().rev() {
let tag_value = i64::try_from(variant.tag).map_err(|_| {
format!(
"custom type '{}' constructor tag {} cannot be represented as a Plutus integer",
layout.name, variant.tag
)
})?;
let tag_matches = apply_builtin(
"equalsInteger",
vec![
Term::Var(tag.clone()),
Term::Constant(Constant::Int(tag_value)),
],
);
let field_count_matches =
list_has_exact_length(Term::Var(fields.clone()), variant.field_count);
body = lower_if(
tag_matches,
lower_if(
field_count_matches,
Term::Constant(Constant::Int(tag_value)),
Term::Error,
),
body,
);
}
Ok(bind_terms_in_order(
vec![
(constr.clone(), apply_builtin("unConstrData", vec![raw])),
(
tag,
apply_forced_builtin_n("fstPair", 2, vec![Term::Var(constr.clone())]),
),
(
fields,
apply_forced_builtin_n("sndPair", 2, vec![Term::Var(constr)]),
),
],
body,
))
}
fn list_has_exact_length(list: Term, len: usize) -> Term {
if len == 0 {
return apply_forced_builtin_n("nullList", 1, vec![list]);
}
lower_if(
apply_forced_builtin_n("nullList", 1, vec![list.clone()]),
Term::Constant(Constant::Bool(false)),
list_has_exact_length(apply_forced_builtin_n("tailList", 1, vec![list]), len - 1),
)
}
fn supports_raw_ledger_type(ty: &Type) -> bool {
matches!(ty, Type::Data | Type::Tx)
}
fn wrap_validator_bool_result(program: Program) -> Program {
Program {
version: program.version,
term: wrap_bool_result_term(program.term),
}
}
fn wrap_bool_result_term(term: Term) -> Term {
match term {
Term::Lambda { param, body } => Term::Lambda {
param,
body: Box::new(wrap_bool_result_term(*body)),
},
term => lower_if(term, Term::Constant(Constant::Unit), Term::Error),
}
}
fn flat_encode_program(program: &Program) -> Result<Vec<u8>, String> {
let mut encoder = FlatEncoder::new();
encoder.word(program.version.0 as u64);
encoder.word(program.version.1 as u64);
encoder.word(program.version.2 as u64);
let mut scope = Vec::new();
flat_encode_term(&program.term, &mut scope, &mut encoder)?;
encoder.filler();
Ok(encoder.finish())
}
fn flat_encode_term(
term: &Term,
scope: &mut Vec<String>,
encoder: &mut FlatEncoder,
) -> Result<(), String> {
match term {
Term::Var(name) => {
encoder.bits(4, 0)?;
let index = scope
.iter()
.rev()
.position(|bound| bound == name)
.map(|index| index + 1)
.ok_or_else(|| format!("unbound variable '{name}' during flat encoding"))?;
encoder.word(index as u64);
}
Term::Delay(term) => {
encoder.bits(4, 1)?;
flat_encode_term(term, scope, encoder)?;
}
Term::Lambda { param, body } => {
encoder.bits(4, 2)?;
scope.push(param.clone());
flat_encode_term(body, scope, encoder)?;
scope.pop();
}
Term::Apply { function, argument } => {
encoder.bits(4, 3)?;
flat_encode_term(function, scope, encoder)?;
flat_encode_term(argument, scope, encoder)?;
}
Term::Constant(constant) => {
encoder.bits(4, 4)?;
flat_encode_constant(constant, encoder)?;
}
Term::Force(term) => {
encoder.bits(4, 5)?;
flat_encode_term(term, scope, encoder)?;
}
Term::Error => {
encoder.bits(4, 6)?;
}
Term::Builtin(name) => {
encoder.bits(4, 7)?;
let tag = builtin_flat_tag(name)
.ok_or_else(|| format!("unknown UPLC builtin '{name}' during flat encoding"))?;
encoder.bits(7, tag)?;
}
}
Ok(())
}
fn flat_encode_constant(constant: &Constant, encoder: &mut FlatEncoder) -> Result<(), String> {
match constant {
Constant::Int(value) => {
flat_encode_constant_type(&[0], encoder)?;
encoder.signed_integer(*value);
}
Constant::ByteArray(hex) => {
flat_encode_constant_type(&[1], encoder)?;
encoder.bytes(&hex_decode(hex)?);
}
Constant::String(value) => {
flat_encode_constant_type(&[2], encoder)?;
encoder.bytes(value.as_bytes());
}
Constant::Unit => {
flat_encode_constant_type(&[3], encoder)?;
}
Constant::Bool(value) => {
flat_encode_constant_type(&[4], encoder)?;
encoder.bool(*value);
}
}
Ok(())
}
fn flat_encode_constant_type(tags: &[u8], encoder: &mut FlatEncoder) -> Result<(), String> {
for tag in tags {
encoder.bool(true);
encoder.bits(4, *tag)?;
}
encoder.bool(false);
Ok(())
}
fn cbor_bytestring(bytes: &[u8]) -> Vec<u8> {
let mut encoded = Vec::new();
let len = bytes.len();
match len {
0..=23 => encoded.push(0x40 | len as u8),
24..=0xff => {
encoded.push(0x58);
encoded.push(len as u8);
}
0x100..=0xffff => {
encoded.push(0x59);
encoded.extend_from_slice(&(len as u16).to_be_bytes());
}
0x1_0000..=0xffff_ffff => {
encoded.push(0x5a);
encoded.extend_from_slice(&(len as u32).to_be_bytes());
}
_ => {
encoded.push(0x5b);
encoded.extend_from_slice(&(len as u64).to_be_bytes());
}
}
encoded.extend_from_slice(bytes);
encoded
}
fn script_hash(version: u8, flat_bytes: &[u8]) -> String {
let mut output = [0u8; 28];
let mut hasher = Blake2bVar::new(output.len()).expect("valid Blake2b output size");
hasher.update(&[version]);
hasher.update(flat_bytes);
hasher
.finalize_variable(&mut output)
.expect("Blake2b output size matches buffer");
hex_encode(&output)
}
fn builtin_flat_tag(name: &str) -> Option<u8> {
Some(match name {
"addInteger" => 0,
"subtractInteger" => 1,
"multiplyInteger" => 2,
"quotientInteger" => 4,
"remainderInteger" => 5,
"equalsInteger" => 7,
"lessThanInteger" => 8,
"lessThanEqualsInteger" => 9,
"appendByteString" => 10,
"equalsByteString" => 15,
"sha2_256" => 18,
"blake2b_256" => 20,
"equalsString" => 23,
"decodeUtf8" => 25,
"ifThenElse" => 26,
"trace" => 28,
"fstPair" => 29,
"sndPair" => 30,
"headList" => 33,
"tailList" => 34,
"nullList" => 35,
"unConstrData" => 42,
"unMapData" => 43,
"unListData" => 44,
"unIData" => 45,
"unBData" => 46,
"equalsData" => 47,
_ => return None,
})
}
fn hex_decode(hex: &str) -> Result<Vec<u8>, String> {
if !hex.len().is_multiple_of(2) {
return Err(format!(
"hex string '#{hex}' must have an even number of digits"
));
}
(0..hex.len())
.step_by(2)
.map(|index| {
u8::from_str_radix(&hex[index..index + 2], 16)
.map_err(|_| format!("hex string '#{hex}' contains invalid digits"))
})
.collect()
}
fn hex_encode(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut encoded = String::with_capacity(bytes.len() * 2);
for byte in bytes {
encoded.push(HEX[(byte >> 4) as usize] as char);
encoded.push(HEX[(byte & 0x0f) as usize] as char);
}
encoded
}
struct FlatEncoder {
buffer: Vec<u8>,
used_bits: u8,
current_byte: u8,
}
impl FlatEncoder {
fn new() -> Self {
Self {
buffer: Vec::new(),
used_bits: 0,
current_byte: 0,
}
}
fn finish(self) -> Vec<u8> {
self.buffer
}
fn bool(&mut self, value: bool) {
if value {
self.one();
} else {
self.zero();
}
}
fn signed_integer(&mut self, value: i64) {
self.word(zigzag_i64(value));
}
fn word(&mut self, mut value: u64) {
loop {
let mut byte = (value & 0x7f) as u8;
value >>= 7;
if value != 0 {
byte |= 0x80;
}
self.bits(8, byte).expect("8-bit word always fits");
if value == 0 {
break;
}
}
}
fn bytes(&mut self, bytes: &[u8]) {
self.filler();
for chunk in bytes.chunks(255) {
self.buffer.push(chunk.len() as u8);
self.buffer.extend_from_slice(chunk);
}
self.buffer.push(0);
}
fn bits(&mut self, num_bits: u8, value: u8) -> Result<(), String> {
if num_bits == 0 || num_bits > 8 {
return Err(format!("invalid flat bit width {num_bits}"));
}
if num_bits < 8 && value >= (1u8 << num_bits) {
return Err(format!("value {value} does not fit in {num_bits} bits"));
}
for shift in (0..num_bits).rev() {
self.bool(((value >> shift) & 1) == 1);
}
Ok(())
}
fn filler(&mut self) {
self.current_byte |= 1;
self.next_word();
}
fn zero(&mut self) {
if self.used_bits == 7 {
self.next_word();
} else {
self.used_bits += 1;
}
}
fn one(&mut self) {
if self.used_bits == 7 {
self.current_byte |= 1;
self.next_word();
} else {
self.current_byte |= 128 >> self.used_bits;
self.used_bits += 1;
}
}
fn next_word(&mut self) {
self.buffer.push(self.current_byte);
self.current_byte = 0;
self.used_bits = 0;
}
}
fn zigzag_i64(value: i64) -> u64 {
((value << 1) ^ (value >> 63)) as u64
}
fn lower_term(term: &AeriTerm, blockers: &mut Vec<String>) -> Option<Term> {
match term {
AeriTerm::Var(name) => Some(Term::Var(name.clone())),
AeriTerm::Bool(value) => Some(Term::Constant(Constant::Bool(*value))),
AeriTerm::Int(value) => Some(Term::Constant(Constant::Int(*value))),
AeriTerm::String(value) => Some(Term::Constant(Constant::String(value.clone()))),
AeriTerm::ByteArray(hex) => Some(Term::Constant(Constant::ByteArray(hex.clone()))),
AeriTerm::Unit => Some(Term::Constant(Constant::Unit)),
AeriTerm::List(items) => {
for item in items {
let _ = lower_term(item, blockers);
}
blockers.push(
"list literals need Plutus list encoding and element type lowering".to_string(),
);
None
}
AeriTerm::Lambda { param, body } => lower_term(body, blockers).map(|body| Term::Lambda {
param: param.clone(),
body: Box::new(body),
}),
AeriTerm::Let { name, value, body } => {
let value = lower_term(value, blockers);
let body = lower_term(body, blockers);
match (value, body) {
(Some(value), Some(body)) => Some(apply(
Term::Lambda {
param: name.clone(),
body: Box::new(body),
},
value,
)),
_ => None,
}
}
AeriTerm::If {
condition,
then_term,
else_term,
} => {
let condition = lower_term(condition, blockers);
let then_term = lower_term(then_term, blockers);
let else_term = lower_term(else_term, blockers);
match (condition, then_term, else_term) {
(Some(condition), Some(then_term), Some(else_term)) => {
Some(lower_if(condition, then_term, else_term))
}
_ => None,
}
}
AeriTerm::BuiltinCall { name, args } => lower_builtin_call(name, args, blockers),
AeriTerm::Constr { tag, fields, .. } if fields.is_empty() => {
match constructor_tag_value(*tag) {
Some(tag) => Some(Term::Constant(Constant::Int(tag))),
None => {
blockers.push(format!(
"constructor tag {tag} cannot be represented as a Plutus integer"
));
None
}
}
}
AeriTerm::Constr { fields, .. } => {
for field in fields {
let _ = lower_term(field, blockers);
}
blockers.push(
"custom constructors with fields need Plutus Data encoding lowering".to_string(),
);
None
}
AeriTerm::Match { subject, arms } => lower_match(subject, arms, blockers),
AeriTerm::Trace { message, then_term } => {
let message = lower_term(message, blockers);
let then_term = lower_term(then_term, blockers);
match (message, then_term) {
(Some(message), Some(then_term)) => {
Some(apply_forced_builtin("trace", vec![message, then_term]))
}
_ => None,
}
}
AeriTerm::Error(_) => Some(Term::Error),
AeriTerm::Sequence { first, then_term } => {
let first = lower_term(first, blockers);
let then_term = lower_term(then_term, blockers);
match (first, then_term) {
(Some(first), Some(then_term)) => {
let discard = fresh_sequence_discard_binder(&then_term);
Some(apply(
Term::Lambda {
param: discard,
body: Box::new(then_term),
},
first,
))
}
_ => None,
}
}
}
}
fn fresh_sequence_discard_binder(term: &Term) -> String {
let mut names = HashSet::new();
collect_names(term, &mut names);
fresh_name(SEQUENCE_DISCARD_BINDER, &names)
}
fn fresh_name(base: &str, names: &HashSet<String>) -> String {
if !names.contains(base) {
return base.to_string();
}
(1..)
.map(|index| format!("{base}_{index}"))
.find(|candidate| !names.contains(candidate))
.expect("fresh binder search is unbounded")
}
fn collect_names(term: &Term, names: &mut HashSet<String>) {
match term {
Term::Var(name) | Term::Builtin(name) => {
names.insert(name.clone());
}
Term::Lambda { param, body } => {
names.insert(param.clone());
collect_names(body, names);
}
Term::Apply { function, argument } => {
collect_names(function, names);
collect_names(argument, names);
}
Term::Delay(term) | Term::Force(term) => collect_names(term, names),
Term::Constant(_) | Term::Error => (),
}
}
#[derive(Debug, Clone)]
struct LoweredMatchArm {
pattern: AeriPattern,
body: Term,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PrimitivePatternKind {
Bool,
Unit,
Int,
ByteArray,
String,
Constructor,
}
fn lower_match(
subject: &AeriTerm,
arms: &[AeriMatchArm],
blockers: &mut Vec<String>,
) -> Option<Term> {
if needs_direct_constructor_match_lowering(subject, arms) {
if let AeriTerm::Constr { name, tag, fields } = subject {
return lower_direct_constructor_match(name, *tag, fields, arms, blockers);
}
}
let subject = lower_term(subject, blockers);
let mut lowered_arms = Vec::new();
let mut literal_kind = None;
let mut has_catch_all = false;
let mut has_bool_true = false;
let mut has_bool_false = false;
let mut unsupported_pattern = false;
let mut all_bodies_lowered = true;
let mut bool_patterns = HashSet::new();
let mut int_patterns = HashSet::new();
let mut byte_patterns = HashSet::new();
let mut string_patterns = HashSet::new();
let mut constructor_patterns = HashSet::new();
let mut has_unit_pattern = false;
let last_index = arms.len().saturating_sub(1);
for (index, arm) in arms.iter().enumerate() {
match &arm.pattern {
AeriPattern::Constructor {
name,
tag,
bindings,
} if bindings.is_empty() => {
if constructor_tag_value(*tag).is_none() {
unsupported_pattern = true;
blockers.push(format!(
"constructor match pattern '{name}#{tag}' tag cannot be represented as a Plutus integer"
));
}
unsupported_pattern |= !note_primitive_pattern_kind(
PrimitivePatternKind::Constructor,
&mut literal_kind,
blockers,
);
unsupported_pattern |= !note_unique_primitive_match_pattern(
format!("constructor '{name}#{tag}'"),
constructor_patterns.insert(*tag),
blockers,
);
}
AeriPattern::Constructor { name, .. } => {
unsupported_pattern = true;
blockers.push(format!(
"constructor match pattern '{name}' with fields needs Plutus Data/case lowering"
));
}
AeriPattern::Wildcard | AeriPattern::Binding(_) => {
has_catch_all = true;
if index != last_index {
unsupported_pattern = true;
blockers.push(
"catch-all match patterns must be last for UPLC-core lowering".to_string(),
);
}
}
AeriPattern::Bool(value) => {
unsupported_pattern |= !note_primitive_pattern_kind(
PrimitivePatternKind::Bool,
&mut literal_kind,
blockers,
);
has_bool_true |= *value;
has_bool_false |= !*value;
unsupported_pattern |= !note_unique_primitive_match_pattern(
format!("'{value}'"),
bool_patterns.insert(*value),
blockers,
);
}
AeriPattern::Unit => {
unsupported_pattern |= !note_primitive_pattern_kind(
PrimitivePatternKind::Unit,
&mut literal_kind,
blockers,
);
unsupported_pattern |= !note_unique_primitive_match_pattern(
"'()'".to_string(),
!has_unit_pattern,
blockers,
);
has_unit_pattern = true;
}
AeriPattern::Int(_) => {
unsupported_pattern |= !note_primitive_pattern_kind(
PrimitivePatternKind::Int,
&mut literal_kind,
blockers,
);
if let AeriPattern::Int(value) = &arm.pattern {
unsupported_pattern |= !note_unique_primitive_match_pattern(
format!("'{value}'"),
int_patterns.insert(*value),
blockers,
);
}
}
AeriPattern::ByteArray(_) => {
unsupported_pattern |= !note_primitive_pattern_kind(
PrimitivePatternKind::ByteArray,
&mut literal_kind,
blockers,
);
if let AeriPattern::ByteArray(hex) = &arm.pattern {
unsupported_pattern |= !note_unique_primitive_match_pattern(
format!("'#{hex}'"),
byte_patterns.insert(hex.clone()),
blockers,
);
}
}
AeriPattern::String(_) => {
unsupported_pattern |= !note_primitive_pattern_kind(
PrimitivePatternKind::String,
&mut literal_kind,
blockers,
);
if let AeriPattern::String(value) = &arm.pattern {
unsupported_pattern |= !note_unique_primitive_match_pattern(
format!("string '{value}'"),
string_patterns.insert(value.clone()),
blockers,
);
}
}
}
match lower_term(&arm.body, blockers) {
Some(body) => lowered_arms.push(LoweredMatchArm {
pattern: arm.pattern.clone(),
body,
}),
None => all_bodies_lowered = false,
}
}
let exhaustive_without_catch_all = matches!(
literal_kind,
Some(PrimitivePatternKind::Bool) if has_bool_true && has_bool_false
) || matches!(literal_kind, Some(PrimitivePatternKind::Unit) if has_unit_pattern);
if !has_catch_all && !exhaustive_without_catch_all {
blockers.push(
"literal/constructor match lowering needs a catch-all arm unless Bool patterns cover true and false"
.to_string(),
);
unsupported_pattern = true;
}
if unsupported_pattern || !all_bodies_lowered {
return None;
}
let subject = subject?;
let binder = fresh_match_subject_binder(&subject, &lowered_arms);
let body = lower_match_arms(Term::Var(binder.clone()), &lowered_arms);
Some(apply(
Term::Lambda {
param: binder,
body: Box::new(body),
},
subject,
))
}
fn needs_direct_constructor_match_lowering(subject: &AeriTerm, arms: &[AeriMatchArm]) -> bool {
let AeriTerm::Constr { fields, .. } = subject else {
return false;
};
!fields.is_empty()
|| arms.iter().any(|arm| {
matches!(
&arm.pattern,
AeriPattern::Constructor { bindings, .. } if !bindings.is_empty()
)
})
}
fn lower_direct_constructor_match(
subject_name: &str,
subject_tag: usize,
fields: &[AeriTerm],
arms: &[AeriMatchArm],
blockers: &mut Vec<String>,
) -> Option<Term> {
let mut selected_arm = None;
let mut has_catch_all = false;
let mut unsupported_pattern = false;
let mut constructor_patterns = HashSet::new();
if constructor_tag_value(subject_tag).is_none() {
unsupported_pattern = true;
blockers.push(format!(
"constructor match subject '{subject_name}#{subject_tag}' tag cannot be represented as a Plutus integer"
));
}
let last_index = arms.len().saturating_sub(1);
for (index, arm) in arms.iter().enumerate() {
match &arm.pattern {
AeriPattern::Constructor { name, tag, .. } => {
if constructor_tag_value(*tag).is_none() {
unsupported_pattern = true;
blockers.push(format!(
"constructor match pattern '{name}#{tag}' tag cannot be represented as a Plutus integer"
));
}
unsupported_pattern |= !note_unique_primitive_match_pattern(
format!("constructor '{name}#{tag}'"),
constructor_patterns.insert(*tag),
blockers,
);
if *tag == subject_tag && selected_arm.is_none() {
selected_arm = Some(arm);
}
}
AeriPattern::Wildcard | AeriPattern::Binding(_) => {
has_catch_all = true;
if index != last_index {
unsupported_pattern = true;
blockers.push(
"catch-all match patterns must be last for UPLC-core lowering".to_string(),
);
}
if selected_arm.is_none() {
selected_arm = Some(arm);
}
}
AeriPattern::Bool(_)
| AeriPattern::Unit
| AeriPattern::Int(_)
| AeriPattern::ByteArray(_)
| AeriPattern::String(_) => {
unsupported_pattern = true;
blockers.push(
"direct constructor match lowering only supports constructor or catch-all patterns"
.to_string(),
);
}
}
}
if !has_catch_all {
blockers.push(
"literal/constructor match lowering needs a catch-all arm unless Bool patterns cover true and false"
.to_string(),
);
unsupported_pattern = true;
}
let Some(selected_arm) = selected_arm else {
blockers.push(format!(
"direct constructor match for '{subject_name}#{subject_tag}' has no matching arm"
));
return None;
};
if unsupported_pattern {
return None;
}
match &selected_arm.pattern {
AeriPattern::Constructor { name, bindings, .. } => {
if bindings.len() != fields.len() {
blockers.push(format!(
"constructor match pattern '{name}' binds {} field(s), but direct subject has {}",
bindings.len(),
fields.len()
));
return None;
}
lower_direct_constructor_arm(fields, bindings, &selected_arm.body, blockers)
}
AeriPattern::Wildcard => {
let bindings = vec![None; fields.len()];
lower_direct_constructor_arm(fields, &bindings, &selected_arm.body, blockers)
}
AeriPattern::Binding(name) => {
blockers.push(format!(
"binding catch-all pattern '{name}' for constructor value needs Plutus Data encoding lowering"
));
None
}
AeriPattern::Bool(_)
| AeriPattern::Unit
| AeriPattern::Int(_)
| AeriPattern::ByteArray(_)
| AeriPattern::String(_) => {
unreachable!("direct constructor match selection excludes primitive patterns")
}
}
}
fn lower_direct_constructor_arm(
fields: &[AeriTerm],
bindings: &[Option<String>],
body: &AeriTerm,
blockers: &mut Vec<String>,
) -> Option<Term> {
let mut lowered_fields = Vec::with_capacity(fields.len());
let mut all_fields_lowered = true;
for field in fields {
match lower_term(field, blockers) {
Some(field) => lowered_fields.push(field),
None => all_fields_lowered = false,
}
}
let body = lower_term(body, blockers);
if !all_fields_lowered {
return None;
}
let body = body?;
let mut names = HashSet::new();
collect_names(&body, &mut names);
for field in &lowered_fields {
collect_names(field, &mut names);
}
let mut field_bindings = Vec::with_capacity(lowered_fields.len());
for (field, binding) in lowered_fields.into_iter().zip(bindings) {
let name = binding.clone().unwrap_or_else(|| {
let name = fresh_name("aeri_constructor_field", &names);
names.insert(name.clone());
name
});
names.insert(name.clone());
field_bindings.push((name, field));
}
Some(bind_terms_in_order(field_bindings, body))
}
fn note_primitive_pattern_kind(
kind: PrimitivePatternKind,
literal_kind: &mut Option<PrimitivePatternKind>,
blockers: &mut Vec<String>,
) -> bool {
match literal_kind {
Some(previous) if *previous != kind => {
blockers.push("primitive match patterns must use one literal type".to_string());
false
}
Some(_) => true,
None => {
*literal_kind = Some(kind);
true
}
}
}
fn note_unique_primitive_match_pattern(
label: String,
inserted: bool,
blockers: &mut Vec<String>,
) -> bool {
if inserted {
return true;
}
blockers.push(format!(
"duplicate primitive match pattern {label} cannot be lowered to UPLC-core"
));
false
}
fn fresh_match_subject_binder(subject: &Term, arms: &[LoweredMatchArm]) -> String {
let mut names = HashSet::new();
collect_names(subject, &mut names);
for arm in arms {
collect_pattern_names(&arm.pattern, &mut names);
collect_names(&arm.body, &mut names);
}
fresh_name(MATCH_SUBJECT_BINDER, &names)
}
fn collect_pattern_names(pattern: &AeriPattern, names: &mut HashSet<String>) {
match pattern {
AeriPattern::Binding(name) => {
names.insert(name.clone());
}
AeriPattern::Constructor { name, bindings, .. } => {
names.insert(name.clone());
for binding in bindings.iter().flatten() {
names.insert(binding.clone());
}
}
AeriPattern::Wildcard
| AeriPattern::Bool(_)
| AeriPattern::Unit
| AeriPattern::Int(_)
| AeriPattern::ByteArray(_)
| AeriPattern::String(_) => (),
}
}
fn lower_match_arms(subject: Term, arms: &[LoweredMatchArm]) -> Term {
let mut fallback = Term::Error;
for arm in arms.iter().rev() {
match &arm.pattern {
AeriPattern::Wildcard => fallback = arm.body.clone(),
AeriPattern::Binding(name) => {
fallback = apply(
Term::Lambda {
param: name.clone(),
body: Box::new(arm.body.clone()),
},
subject.clone(),
);
}
AeriPattern::Constructor { bindings, .. } if bindings.is_empty() => {
fallback = lower_if(
match_condition(subject.clone(), &arm.pattern),
arm.body.clone(),
fallback,
);
}
AeriPattern::Bool(_)
| AeriPattern::Unit
| AeriPattern::Int(_)
| AeriPattern::ByteArray(_)
| AeriPattern::String(_) => {
fallback = lower_if(
match_condition(subject.clone(), &arm.pattern),
arm.body.clone(),
fallback,
);
}
AeriPattern::Constructor { .. } => {
unreachable!("constructor patterns with fields are blocked")
}
}
}
fallback
}
fn match_condition(subject: Term, pattern: &AeriPattern) -> Term {
match pattern {
AeriPattern::Bool(true) => subject,
AeriPattern::Bool(false) => lower_not(subject),
AeriPattern::Unit => Term::Constant(Constant::Bool(true)),
AeriPattern::Int(value) => apply_builtin(
"equalsInteger",
vec![subject, Term::Constant(Constant::Int(*value))],
),
AeriPattern::ByteArray(hex) => apply_builtin(
"equalsByteString",
vec![subject, Term::Constant(Constant::ByteArray(hex.clone()))],
),
AeriPattern::String(value) => apply_builtin(
"equalsString",
vec![subject, Term::Constant(Constant::String(value.clone()))],
),
AeriPattern::Constructor { tag, bindings, .. } if bindings.is_empty() => apply_builtin(
"equalsInteger",
vec![
subject,
Term::Constant(Constant::Int(
constructor_tag_value(*tag)
.expect("constructor tags are validated before match lowering"),
)),
],
),
AeriPattern::Wildcard | AeriPattern::Binding(_) | AeriPattern::Constructor { .. } => {
unreachable!(
"catch-all and constructor patterns with fields do not have match conditions"
)
}
}
}
fn constructor_tag_value(tag: usize) -> Option<i64> {
i64::try_from(tag).ok()
}
fn lower_builtin_call(name: &str, args: &[AeriTerm], blockers: &mut Vec<String>) -> Option<Term> {
if is_list_helper_builtin(name) {
return lower_list_helper_call(name, args, blockers);
}
if let Some((kind, equal)) = list_equality_kind(name) {
return lower_list_equality_call(name, args, kind, equal, blockers);
}
let args = args
.iter()
.map(|arg| lower_term(arg, blockers))
.collect::<Vec<_>>();
let args = args.into_iter().collect::<Option<Vec<_>>>()?;
if is_transaction_builtin(name) {
return match name {
"tx_signed_by" if args.len() == 2 => {
Some(lower_tx_signed_by(args[0].clone(), args[1].clone()))
}
"tx_has_datum" if args.len() == 2 => {
Some(lower_tx_has_datum(args[0].clone(), args[1].clone()))
}
_ => {
blockers.push(transaction_builtin_blocker(name));
None
}
};
}
match name {
"not" if args.len() == 1 => Some(lower_not(args[0].clone())),
"negateInteger" if args.len() == 1 => Some(apply_builtin(
"subtractInteger",
vec![Term::Constant(Constant::Int(0)), args[0].clone()],
)),
"equalsBool" if args.len() == 2 => Some(lower_bool_equality(args)),
"notEqualsBool" if args.len() == 2 => Some(lower_bool_inequality(args)),
"notEqualsData" if args.len() == 2 => Some(lower_not_equals("equalsData", args)),
"notEqualsInteger" if args.len() == 2 => Some(lower_not_equals("equalsInteger", args)),
"notEqualsByteString" if args.len() == 2 => {
Some(lower_not_equals("equalsByteString", args))
}
"notEqualsString" if args.len() == 2 => Some(lower_not_equals("equalsString", args)),
"equalsCustomTag" if args.len() == 2 => Some(apply_builtin("equalsInteger", args)),
"notEqualsCustomTag" if args.len() == 2 => Some(lower_not_equals("equalsInteger", args)),
"datum_equals" => Some(apply_builtin("equalsData", args)),
"append_bytes" => Some(apply_builtin("appendByteString", args)),
"greaterThanInteger" if args.len() == 2 => Some(apply_builtin(
"lessThanInteger",
vec![args[1].clone(), args[0].clone()],
)),
"greaterThanEqualsInteger" if args.len() == 2 => Some(apply_builtin(
"lessThanEqualsInteger",
vec![args[1].clone(), args[0].clone()],
)),
name if plutus_builtin_name(name).is_some() => Some(apply_builtin(
plutus_builtin_name(name).expect("builtin is mapped"),
args,
)),
"equalsAeriValue" | "notEqualsAeriValue" => {
blockers.push(
"structural equality for lists/custom types needs Plutus Data encoding lowering"
.to_string(),
);
None
}
_ => {
blockers.push(format!(
"Aeri builtin '{name}' needs an explicit Plutus builtin or helper lowering"
));
None
}
}
}
fn transaction_builtin_blocker(name: &str) -> String {
match name {
"tx_spends" => format!(
"transaction builtin '{name}' needs ScriptContext decoding/lowering plus a full TxOutRef ABI; ByteArray-only transaction-id matching would ignore the output index"
),
"tx_paid_to" => format!(
"transaction builtin '{name}' needs ScriptContext decoding/lowering for Plutus V2 outputs, addresses, and values"
),
"tx_mints" => format!(
"transaction builtin '{name}' needs ScriptContext decoding/lowering for Plutus V2 mint values, policy ids, and token names"
),
"tx_after" | "tx_before" => format!(
"transaction builtin '{name}' needs ScriptContext decoding/lowering plus a reviewed slot-to-POSIX-time policy for Plutus intervals"
),
_ => format!("transaction builtin '{name}' needs ScriptContext decoding/lowering"),
}
}
fn lower_tx_signed_by(ctx: Term, signer: Term) -> Term {
let signatories = unlist_data(nth_constr_field(nth_constr_field(ctx, 2, 0), 12, 8));
byte_data_list_has(signatories, signer)
}
fn lower_tx_has_datum(ctx: Term, datum: Term) -> Term {
let datums = unmap_data(nth_constr_field(nth_constr_field(ctx, 2, 0), 12, 10));
data_map_has_value(datums, datum)
}
fn nth_constr_field(data: Term, expected_len: usize, index: usize) -> Term {
let mut names = HashSet::new();
collect_names(&data, &mut names);
let constr = fresh_name("aeri_constr", &names);
names.insert(constr.clone());
let tag = fresh_name("aeri_constr_tag", &names);
names.insert(tag.clone());
let fields = fresh_name("aeri_constr_fields", &names);
let body = lower_if(
apply_builtin(
"equalsInteger",
vec![Term::Var(tag.clone()), Term::Constant(Constant::Int(0))],
),
lower_if(
list_has_exact_length(Term::Var(fields.clone()), expected_len),
list_nth(Term::Var(fields.clone()), index),
Term::Error,
),
Term::Error,
);
bind_terms_in_order(
vec![
(constr.clone(), apply_builtin("unConstrData", vec![data])),
(
tag,
apply_forced_builtin_n("fstPair", 2, vec![Term::Var(constr.clone())]),
),
(
fields,
apply_forced_builtin_n("sndPair", 2, vec![Term::Var(constr)]),
),
],
body,
)
}
fn list_nth(list: Term, index: usize) -> Term {
let mut term = list;
for _ in 0..index {
term = apply_forced_builtin_n("tailList", 1, vec![term]);
}
apply_forced_builtin_n("headList", 1, vec![term])
}
fn unlist_data(data: Term) -> Term {
apply_builtin("unListData", vec![data])
}
fn unmap_data(data: Term) -> Term {
apply_builtin("unMapData", vec![data])
}
fn byte_data_list_has(list: Term, needle: Term) -> Term {
let mut names = HashSet::new();
collect_names(&list, &mut names);
collect_names(&needle, &mut names);
let self_name = fresh_name("aeri_tx_signed_by_loop", &names);
names.insert(self_name.clone());
let list_name = fresh_name("aeri_tx_signatories", &names);
names.insert(list_name.clone());
let needle_name = fresh_name("aeri_tx_signer", &names);
apply(
Term::Lambda {
param: needle_name.clone(),
body: Box::new(apply(
apply(
Term::Lambda {
param: self_name.clone(),
body: Box::new(apply(
Term::Var(self_name.clone()),
Term::Var(self_name.clone()),
)),
},
Term::Lambda {
param: self_name.clone(),
body: Box::new(Term::Lambda {
param: list_name.clone(),
body: Box::new(lower_if(
apply_forced_builtin_n(
"nullList",
1,
vec![Term::Var(list_name.clone())],
),
Term::Constant(Constant::Bool(false)),
lower_if(
apply_builtin(
"equalsByteString",
vec![
apply_builtin(
"unBData",
vec![apply_forced_builtin_n(
"headList",
1,
vec![Term::Var(list_name.clone())],
)],
),
Term::Var(needle_name.clone()),
],
),
Term::Constant(Constant::Bool(true)),
apply(
apply(Term::Var(self_name.clone()), Term::Var(self_name)),
apply_forced_builtin_n(
"tailList",
1,
vec![Term::Var(list_name)],
),
),
),
)),
}),
},
),
list,
)),
},
needle,
)
}
fn data_map_has_value(map: Term, needle: Term) -> Term {
let mut names = HashSet::new();
collect_names(&map, &mut names);
collect_names(&needle, &mut names);
let self_name = fresh_name("aeri_tx_datum_loop", &names);
names.insert(self_name.clone());
let map_name = fresh_name("aeri_tx_datums", &names);
names.insert(map_name.clone());
let needle_name = fresh_name("aeri_tx_datum", &names);
apply(
Term::Lambda {
param: needle_name.clone(),
body: Box::new(apply(
apply(
Term::Lambda {
param: self_name.clone(),
body: Box::new(apply(
Term::Var(self_name.clone()),
Term::Var(self_name.clone()),
)),
},
Term::Lambda {
param: self_name.clone(),
body: Box::new(Term::Lambda {
param: map_name.clone(),
body: Box::new(lower_if(
apply_forced_builtin_n(
"nullList",
1,
vec![Term::Var(map_name.clone())],
),
Term::Constant(Constant::Bool(false)),
lower_if(
apply_builtin(
"equalsData",
vec![
apply_forced_builtin_n(
"sndPair",
2,
vec![apply_forced_builtin_n(
"headList",
1,
vec![Term::Var(map_name.clone())],
)],
),
Term::Var(needle_name.clone()),
],
),
Term::Constant(Constant::Bool(true)),
apply(
apply(Term::Var(self_name.clone()), Term::Var(self_name)),
apply_forced_builtin_n(
"tailList",
1,
vec![Term::Var(map_name)],
),
),
),
)),
}),
},
),
map,
)),
},
needle,
)
}
fn is_list_helper_builtin(name: &str) -> bool {
matches!(
name,
"list_has_bytes"
| "list_has_int"
| "list_has_bool"
| "list_has_string"
| "list_has_data"
| "list_has_unit"
| "list_has_custom_tag"
| "list_len_bytes"
| "list_len_int"
| "list_len_bool"
| "list_len_string"
| "list_len_data"
| "list_len_unit"
| "list_len"
)
}
#[derive(Debug, Clone, Copy)]
enum ListElementKind {
Bool,
ByteArray,
Data,
Int,
String,
Unit,
CustomTag,
}
fn lower_list_helper_call(
name: &str,
args: &[AeriTerm],
blockers: &mut Vec<String>,
) -> Option<Term> {
match name {
"list_has_bytes" if args.len() == 2 => lower_list_contains(
name,
&args[0],
&args[1],
ListElementKind::ByteArray,
blockers,
),
"list_has_int" if args.len() == 2 => {
lower_list_contains(name, &args[0], &args[1], ListElementKind::Int, blockers)
}
"list_has_bool" if args.len() == 2 => {
lower_list_contains(name, &args[0], &args[1], ListElementKind::Bool, blockers)
}
"list_has_string" if args.len() == 2 => {
lower_list_contains(name, &args[0], &args[1], ListElementKind::String, blockers)
}
"list_has_data" if args.len() == 2 => {
lower_list_contains(name, &args[0], &args[1], ListElementKind::Data, blockers)
}
"list_has_unit" if args.len() == 2 => {
lower_list_contains(name, &args[0], &args[1], ListElementKind::Unit, blockers)
}
"list_has_custom_tag" if args.len() == 2 => lower_list_contains(
name,
&args[0],
&args[1],
ListElementKind::CustomTag,
blockers,
),
"list_len_bytes" if args.len() == 1 => {
lower_list_len(name, &args[0], ListElementKind::ByteArray, blockers)
}
"list_len_int" if args.len() == 1 => {
lower_list_len(name, &args[0], ListElementKind::Int, blockers)
}
"list_len_bool" if args.len() == 1 => {
lower_list_len(name, &args[0], ListElementKind::Bool, blockers)
}
"list_len_string" if args.len() == 1 => {
lower_list_len(name, &args[0], ListElementKind::String, blockers)
}
"list_len_data" if args.len() == 1 => {
lower_list_len(name, &args[0], ListElementKind::Data, blockers)
}
"list_len_unit" if args.len() == 1 => {
lower_list_len(name, &args[0], ListElementKind::Unit, blockers)
}
"list_len" if args.len() == 1 => lower_any_list_len(name, &args[0], blockers),
_ => {
blockers.push(format!(
"Aeri builtin '{name}' needs an explicit Plutus list helper lowering"
));
None
}
}
}
fn lower_list_contains(
name: &str,
list: &AeriTerm,
needle: &AeriTerm,
kind: ListElementKind,
blockers: &mut Vec<String>,
) -> Option<Term> {
if matches!(list, AeriTerm::List(_)) {
lower_literal_list_contains(name, list, needle, kind, blockers)
} else {
lower_runtime_list_contains(name, list, needle, kind, blockers)
}
}
fn lower_list_len(
name: &str,
list: &AeriTerm,
kind: ListElementKind,
blockers: &mut Vec<String>,
) -> Option<Term> {
if matches!(list, AeriTerm::List(_)) {
lower_literal_list_len(name, list, kind, blockers)
} else {
lower_runtime_list_len(name, list, blockers)
}
}
fn lower_any_list_len(name: &str, list: &AeriTerm, blockers: &mut Vec<String>) -> Option<Term> {
if matches!(list, AeriTerm::List(_)) {
lower_any_literal_list_len(name, list, blockers)
} else {
lower_runtime_list_len(name, list, blockers)
}
}
fn lower_literal_list_contains(
name: &str,
list: &AeriTerm,
needle: &AeriTerm,
kind: ListElementKind,
blockers: &mut Vec<String>,
) -> Option<Term> {
let items = lower_list_literal_items(name, list, kind, blockers)?;
let needle = lower_term(needle, blockers)?;
let mut names = HashSet::new();
collect_names(&needle, &mut names);
for item in &items {
collect_names(item, &mut names);
}
let (mut bindings, item_vars) = bind_list_items("aeri_list_item", items, &mut names);
let needle_name = fresh_name("aeri_list_needle", &names);
names.insert(needle_name.clone());
let mut body = Term::Constant(Constant::Bool(false));
for item in item_vars.into_iter().rev() {
body = lower_if(
lower_list_element_equality(kind, Term::Var(needle_name.clone()), item),
Term::Constant(Constant::Bool(true)),
body,
);
}
bindings.push((needle_name, needle));
Some(bind_terms_in_order(bindings, body))
}
fn lower_runtime_list_contains(
_name: &str,
list: &AeriTerm,
needle: &AeriTerm,
kind: ListElementKind,
blockers: &mut Vec<String>,
) -> Option<Term> {
let list = lower_term(list, blockers)?;
let needle = lower_term(needle, blockers)?;
let mut names = HashSet::new();
collect_names(&list, &mut names);
collect_names(&needle, &mut names);
let self_name = fresh_name("aeri_list_has_loop", &names);
names.insert(self_name.clone());
let list_name = fresh_name("aeri_list_has_items", &names);
names.insert(list_name.clone());
let needle_name = fresh_name("aeri_list_has_needle", &names);
names.insert(needle_name.clone());
let head_name = fresh_name("aeri_list_has_head", &names);
names.insert(head_name.clone());
let item_name = fresh_name("aeri_list_has_item", &names);
names.insert(item_name.clone());
let decoded_item =
decode_data_list_item_for_kind(kind, Term::Var(head_name.clone()), &mut names);
let recurse = apply(
apply(Term::Var(self_name.clone()), Term::Var(self_name.clone())),
apply_forced_builtin_n("tailList", 1, vec![Term::Var(list_name.clone())]),
);
let non_empty = bind_terms_in_order(
vec![
(
head_name,
apply_forced_builtin_n("headList", 1, vec![Term::Var(list_name.clone())]),
),
(item_name.clone(), decoded_item),
],
lower_if(
lower_list_element_equality(kind, Term::Var(needle_name.clone()), Term::Var(item_name)),
Term::Constant(Constant::Bool(true)),
recurse,
),
);
Some(apply(
Term::Lambda {
param: needle_name.clone(),
body: Box::new(apply(
apply(
Term::Lambda {
param: self_name.clone(),
body: Box::new(apply(
Term::Var(self_name.clone()),
Term::Var(self_name.clone()),
)),
},
Term::Lambda {
param: self_name.clone(),
body: Box::new(Term::Lambda {
param: list_name.clone(),
body: Box::new(lower_if(
apply_forced_builtin_n("nullList", 1, vec![Term::Var(list_name)]),
Term::Constant(Constant::Bool(false)),
non_empty,
)),
}),
},
),
list,
)),
},
needle,
))
}
fn decode_data_list_item_for_kind(
kind: ListElementKind,
raw: Term,
names: &mut HashSet<String>,
) -> Term {
match kind {
ListElementKind::Bool => bool_data_decoder(raw, names),
ListElementKind::ByteArray => apply_builtin("unBData", vec![raw]),
ListElementKind::Data => raw,
ListElementKind::Int => apply_builtin("unIData", vec![raw]),
ListElementKind::String => {
apply_builtin("decodeUtf8", vec![apply_builtin("unBData", vec![raw])])
}
ListElementKind::Unit => unit_data_decoder(raw, names),
ListElementKind::CustomTag => custom_tag_data_decoder(raw, names),
}
}
fn list_equality_kind(name: &str) -> Option<(ListElementKind, bool)> {
Some(match name {
"equalsListBool" => (ListElementKind::Bool, true),
"notEqualsListBool" => (ListElementKind::Bool, false),
"equalsListInteger" => (ListElementKind::Int, true),
"notEqualsListInteger" => (ListElementKind::Int, false),
"equalsListByteString" => (ListElementKind::ByteArray, true),
"notEqualsListByteString" => (ListElementKind::ByteArray, false),
"equalsListData" => (ListElementKind::Data, true),
"notEqualsListData" => (ListElementKind::Data, false),
"equalsListString" => (ListElementKind::String, true),
"notEqualsListString" => (ListElementKind::String, false),
"equalsListUnit" => (ListElementKind::Unit, true),
"notEqualsListUnit" => (ListElementKind::Unit, false),
"equalsListCustomTag" => (ListElementKind::CustomTag, true),
"notEqualsListCustomTag" => (ListElementKind::CustomTag, false),
_ => return None,
})
}
fn lower_list_equality_call(
name: &str,
args: &[AeriTerm],
kind: ListElementKind,
equal: bool,
blockers: &mut Vec<String>,
) -> Option<Term> {
if args.len() != 2 {
blockers.push(format!(
"Aeri builtin '{name}' needs an explicit Plutus list equality lowering"
));
return None;
}
match (&args[0], &args[1]) {
(AeriTerm::List(_), AeriTerm::List(_)) => (),
(AeriTerm::List(_), _) => {
return lower_mixed_list_equality(name, &args[1], &args[0], kind, equal, blockers);
}
(_, AeriTerm::List(_)) => {
return lower_mixed_list_equality(name, &args[0], &args[1], kind, equal, blockers);
}
_ => return lower_runtime_list_equality(name, &args[0], &args[1], kind, equal, blockers),
}
let left_items = lower_list_literal_items(name, &args[0], kind, blockers)?;
let right_items = lower_list_literal_items(name, &args[1], kind, blockers)?;
let mut names = HashSet::new();
for item in left_items.iter().chain(right_items.iter()) {
collect_names(item, &mut names);
}
let (mut bindings, left_vars) = bind_list_items("aeri_list_left", left_items, &mut names);
let (right_bindings, right_vars) = bind_list_items("aeri_list_right", right_items, &mut names);
bindings.extend(right_bindings);
let mut body = if left_vars.len() == right_vars.len() {
lower_list_element_equalities(kind, left_vars, right_vars)
} else {
Term::Constant(Constant::Bool(false))
};
if !equal {
body = lower_not(body);
}
Some(bind_terms_in_order(bindings, body))
}
fn lower_mixed_list_equality(
name: &str,
runtime: &AeriTerm,
literal: &AeriTerm,
kind: ListElementKind,
equal: bool,
blockers: &mut Vec<String>,
) -> Option<Term> {
let literal_items = lower_list_literal_items(name, literal, kind, blockers)?;
let runtime = lower_term(runtime, blockers)?;
let mut names = HashSet::new();
collect_names(&runtime, &mut names);
for item in &literal_items {
collect_names(item, &mut names);
}
let runtime_name = fresh_name("aeri_list_eq_runtime", &names);
names.insert(runtime_name.clone());
let (mut bindings, literal_vars) =
bind_list_items("aeri_list_eq_literal", literal_items, &mut names);
bindings.push((runtime_name.clone(), runtime));
let mut body =
data_list_matches_literal(Term::Var(runtime_name), literal_vars, kind, &mut names);
if !equal {
body = lower_not(body);
}
Some(bind_terms_in_order(bindings, body))
}
fn data_list_matches_literal(
list: Term,
literal_items: Vec<Term>,
kind: ListElementKind,
names: &mut HashSet<String>,
) -> Term {
data_list_matches_literal_items(list, &literal_items, kind, names)
}
fn data_list_matches_literal_items(
list: Term,
literal_items: &[Term],
kind: ListElementKind,
names: &mut HashSet<String>,
) -> Term {
let Some((literal, rest)) = literal_items.split_first() else {
return apply_forced_builtin_n("nullList", 1, vec![list]);
};
let list_name = fresh_name("aeri_list_eq_segment", names);
names.insert(list_name.clone());
let head_name = fresh_name("aeri_list_eq_head", names);
names.insert(head_name.clone());
let item_name = fresh_name("aeri_list_eq_item", names);
names.insert(item_name.clone());
let tail_name = fresh_name("aeri_list_eq_tail", names);
names.insert(tail_name.clone());
let decoded_item = decode_data_list_item_for_kind(kind, Term::Var(head_name.clone()), names);
let rest_matches =
data_list_matches_literal_items(Term::Var(tail_name.clone()), rest, kind, names);
bind_terms_in_order(
vec![(list_name.clone(), list)],
lower_if(
apply_forced_builtin_n("nullList", 1, vec![Term::Var(list_name.clone())]),
Term::Constant(Constant::Bool(false)),
bind_terms_in_order(
vec![
(
head_name,
apply_forced_builtin_n("headList", 1, vec![Term::Var(list_name.clone())]),
),
(item_name.clone(), decoded_item),
(
tail_name,
apply_forced_builtin_n("tailList", 1, vec![Term::Var(list_name)]),
),
],
lower_if(
lower_list_element_equality(kind, Term::Var(item_name), literal.clone()),
rest_matches,
Term::Constant(Constant::Bool(false)),
),
),
),
)
}
fn lower_runtime_list_equality(
_name: &str,
left: &AeriTerm,
right: &AeriTerm,
kind: ListElementKind,
equal: bool,
blockers: &mut Vec<String>,
) -> Option<Term> {
let left = lower_term(left, blockers)?;
let right = lower_term(right, blockers)?;
let mut body = data_list_equality(left, right, kind);
if !equal {
body = lower_not(body);
}
Some(body)
}
fn data_list_equality(left: Term, right: Term, kind: ListElementKind) -> Term {
let mut names = HashSet::new();
collect_names(&left, &mut names);
collect_names(&right, &mut names);
let self_name = fresh_name("aeri_list_eq_loop", &names);
names.insert(self_name.clone());
let left_name = fresh_name("aeri_list_eq_left", &names);
names.insert(left_name.clone());
let right_name = fresh_name("aeri_list_eq_right", &names);
names.insert(right_name.clone());
let left_head_name = fresh_name("aeri_list_eq_left_head", &names);
names.insert(left_head_name.clone());
let right_head_name = fresh_name("aeri_list_eq_right_head", &names);
names.insert(right_head_name.clone());
let left_item_name = fresh_name("aeri_list_eq_left_item", &names);
names.insert(left_item_name.clone());
let right_item_name = fresh_name("aeri_list_eq_right_item", &names);
names.insert(right_item_name.clone());
let recurse = apply(
apply(
apply(Term::Var(self_name.clone()), Term::Var(self_name.clone())),
apply_forced_builtin_n("tailList", 1, vec![Term::Var(left_name.clone())]),
),
apply_forced_builtin_n("tailList", 1, vec![Term::Var(right_name.clone())]),
);
let left_item =
decode_data_list_item_for_kind(kind, Term::Var(left_head_name.clone()), &mut names);
let right_item =
decode_data_list_item_for_kind(kind, Term::Var(right_head_name.clone()), &mut names);
let non_empty = bind_terms_in_order(
vec![
(
left_head_name,
apply_forced_builtin_n("headList", 1, vec![Term::Var(left_name.clone())]),
),
(
right_head_name,
apply_forced_builtin_n("headList", 1, vec![Term::Var(right_name.clone())]),
),
(left_item_name.clone(), left_item),
(right_item_name.clone(), right_item),
],
lower_if(
lower_list_element_equality(
kind,
Term::Var(left_item_name),
Term::Var(right_item_name),
),
recurse,
Term::Constant(Constant::Bool(false)),
),
);
let body = lower_if(
apply_forced_builtin_n("nullList", 1, vec![Term::Var(left_name.clone())]),
apply_forced_builtin_n("nullList", 1, vec![Term::Var(right_name.clone())]),
lower_if(
apply_forced_builtin_n("nullList", 1, vec![Term::Var(right_name.clone())]),
Term::Constant(Constant::Bool(false)),
non_empty,
),
);
apply(
apply(
apply(
Term::Lambda {
param: self_name.clone(),
body: Box::new(apply(
Term::Var(self_name.clone()),
Term::Var(self_name.clone()),
)),
},
Term::Lambda {
param: self_name.clone(),
body: Box::new(Term::Lambda {
param: left_name,
body: Box::new(Term::Lambda {
param: right_name,
body: Box::new(body),
}),
}),
},
),
left,
),
right,
)
}
fn lower_list_element_equalities(
kind: ListElementKind,
left_vars: Vec<Term>,
right_vars: Vec<Term>,
) -> Term {
left_vars.into_iter().zip(right_vars).rev().fold(
Term::Constant(Constant::Bool(true)),
|body, (left, right)| {
lower_if(
lower_list_element_equality(kind, left, right),
body,
Term::Constant(Constant::Bool(false)),
)
},
)
}
fn lower_list_element_equality(kind: ListElementKind, left: Term, right: Term) -> Term {
match kind {
ListElementKind::Bool => lower_bool_equality(vec![left, right]),
ListElementKind::ByteArray => apply_builtin("equalsByteString", vec![left, right]),
ListElementKind::Data => apply_builtin("equalsData", vec![left, right]),
ListElementKind::Int => apply_builtin("equalsInteger", vec![left, right]),
ListElementKind::String => apply_builtin("equalsString", vec![left, right]),
ListElementKind::Unit => Term::Constant(Constant::Bool(true)),
ListElementKind::CustomTag => apply_builtin("equalsInteger", vec![left, right]),
}
}
fn lower_literal_list_len(
name: &str,
list: &AeriTerm,
kind: ListElementKind,
blockers: &mut Vec<String>,
) -> Option<Term> {
let items = lower_list_literal_items(name, list, kind, blockers)?;
let len = items.len() as i64;
let mut names = HashSet::new();
for item in &items {
collect_names(item, &mut names);
}
let (bindings, _) = bind_list_items("aeri_list_item", items, &mut names);
Some(bind_terms_in_order(
bindings,
Term::Constant(Constant::Int(len)),
))
}
fn lower_any_literal_list_len(
name: &str,
list: &AeriTerm,
blockers: &mut Vec<String>,
) -> Option<Term> {
let AeriTerm::List(items) = list else {
blockers.push(format!(
"Aeri builtin '{name}' can only lower list literal inputs to UPLC today"
));
return None;
};
let mut lowered = Vec::with_capacity(items.len());
let mut supported = true;
for item in items {
if !lower_list_len_item(item, blockers, &mut lowered) {
supported = false;
}
}
if !supported {
blockers.push(format!(
"Aeri builtin '{name}' can only lower list literal inputs with supported elements to UPLC today"
));
return None;
}
let len = items.len() as i64;
let mut names = HashSet::new();
for item in &lowered {
collect_names(item, &mut names);
}
let (bindings, _) = bind_list_items("aeri_list_item", lowered, &mut names);
Some(bind_terms_in_order(
bindings,
Term::Constant(Constant::Int(len)),
))
}
fn lower_runtime_list_len(
_name: &str,
list: &AeriTerm,
blockers: &mut Vec<String>,
) -> Option<Term> {
let list = lower_term(list, blockers)?;
Some(data_list_len(list))
}
fn data_list_len(list: Term) -> Term {
let mut names = HashSet::new();
collect_names(&list, &mut names);
let self_name = fresh_name("aeri_list_len_loop", &names);
names.insert(self_name.clone());
let list_name = fresh_name("aeri_list_len_items", &names);
apply(
apply(
Term::Lambda {
param: self_name.clone(),
body: Box::new(apply(
Term::Var(self_name.clone()),
Term::Var(self_name.clone()),
)),
},
Term::Lambda {
param: self_name.clone(),
body: Box::new(Term::Lambda {
param: list_name.clone(),
body: Box::new(lower_if(
apply_forced_builtin_n("nullList", 1, vec![Term::Var(list_name.clone())]),
Term::Constant(Constant::Int(0)),
apply_builtin(
"addInteger",
vec![
Term::Constant(Constant::Int(1)),
apply(
apply(Term::Var(self_name.clone()), Term::Var(self_name)),
apply_forced_builtin_n(
"tailList",
1,
vec![Term::Var(list_name)],
),
),
],
),
)),
}),
},
),
list,
)
}
fn lower_list_len_item(
item: &AeriTerm,
blockers: &mut Vec<String>,
lowered: &mut Vec<Term>,
) -> bool {
if let AeriTerm::Constr { fields, .. } = item {
if !fields.is_empty() {
let mut supported = true;
for field in fields {
if !lower_list_len_direct_constructor_field(field, blockers, lowered) {
supported = false;
}
}
return supported;
}
}
lower_list_len_direct_constructor_field(item, blockers, lowered)
}
fn lower_list_len_direct_constructor_field(
field: &AeriTerm,
blockers: &mut Vec<String>,
lowered: &mut Vec<Term>,
) -> bool {
if let AeriTerm::Constr { fields, .. } = field {
if !fields.is_empty() {
let mut supported = true;
for field in fields {
if !lower_list_len_direct_constructor_field(field, blockers, lowered) {
supported = false;
}
}
return supported;
}
}
match lower_term(field, blockers) {
Some(field) => {
lowered.push(field);
true
}
None => false,
}
}
fn lower_list_literal_items(
name: &str,
list: &AeriTerm,
kind: ListElementKind,
blockers: &mut Vec<String>,
) -> Option<Vec<Term>> {
let AeriTerm::List(items) = list else {
blockers.push(format!(
"Aeri builtin '{name}' can only lower list literal inputs to UPLC today"
));
return None;
};
let mut lowered = Vec::with_capacity(items.len());
let mut supported = true;
for item in items {
if !list_literal_element_can_have_kind(item, kind) {
blockers.push(format!(
"Aeri builtin '{name}' found a list literal element that is not compatible with {kind}"
));
supported = false;
continue;
}
if let Some(item) = lower_term(item, blockers) {
lowered.push(item);
} else {
supported = false;
}
}
if !supported {
blockers.push(format!(
"Aeri builtin '{name}' can only lower list literal inputs with supported {kind} elements to UPLC today"
));
return None;
}
Some(lowered)
}
fn list_literal_element_can_have_kind(item: &AeriTerm, kind: ListElementKind) -> bool {
match item {
AeriTerm::Bool(_) => matches!(kind, ListElementKind::Bool),
AeriTerm::ByteArray(_) => matches!(kind, ListElementKind::ByteArray),
AeriTerm::Int(_) => matches!(kind, ListElementKind::Int),
AeriTerm::String(_) => matches!(kind, ListElementKind::String),
AeriTerm::Unit => matches!(kind, ListElementKind::Unit),
AeriTerm::List(_) => false,
AeriTerm::Constr { fields, .. } => {
fields.is_empty() && matches!(kind, ListElementKind::CustomTag)
}
AeriTerm::Error(_) => true,
AeriTerm::Var(_)
| AeriTerm::Lambda { .. }
| AeriTerm::Let { .. }
| AeriTerm::If { .. }
| AeriTerm::BuiltinCall { .. }
| AeriTerm::Match { .. }
| AeriTerm::Trace { .. }
| AeriTerm::Sequence { .. } => true,
}
}
fn bind_list_items(
base: &str,
items: Vec<Term>,
names: &mut HashSet<String>,
) -> (Vec<(String, Term)>, Vec<Term>) {
let mut bindings = Vec::with_capacity(items.len());
let mut vars = Vec::with_capacity(items.len());
for item in items {
let name = fresh_name(base, names);
names.insert(name.clone());
bindings.push((name.clone(), item));
vars.push(Term::Var(name));
}
(bindings, vars)
}
fn bind_terms_in_order(bindings: Vec<(String, Term)>, body: Term) -> Term {
bindings
.into_iter()
.rev()
.fold(body, |body, (name, value)| {
apply(
Term::Lambda {
param: name,
body: Box::new(body),
},
value,
)
})
}
impl std::fmt::Display for ListElementKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Bool => write!(f, "Bool"),
Self::ByteArray => write!(f, "ByteArray"),
Self::Data => write!(f, "Data"),
Self::Int => write!(f, "Int"),
Self::String => write!(f, "String"),
Self::Unit => write!(f, "Unit"),
Self::CustomTag => write!(f, "nullary custom constructor"),
}
}
}
fn plutus_builtin_name(name: &str) -> Option<&'static str> {
Some(match name {
"equalsData" => "equalsData",
"equalsInteger" => "equalsInteger",
"equalsByteString" => "equalsByteString",
"equalsString" => "equalsString",
"lessThanInteger" => "lessThanInteger",
"lessThanEqualsInteger" => "lessThanEqualsInteger",
"addInteger" => "addInteger",
"subtractInteger" => "subtractInteger",
"multiplyInteger" => "multiplyInteger",
"quotientInteger" => "quotientInteger",
"remainderInteger" => "remainderInteger",
"sha2_256" => "sha2_256",
"blake2b_256" => "blake2b_256",
_ => return None,
})
}
pub fn supports_aeri_builtin(name: &str) -> bool {
matches!(
name,
"append_bytes"
| "datum_equals"
| "equalsBool"
| "equalsByteString"
| "equalsData"
| "equalsInteger"
| "equalsString"
| "greaterThanInteger"
| "greaterThanEqualsInteger"
| "equalsListBool"
| "equalsListByteString"
| "equalsListData"
| "equalsListInteger"
| "equalsListString"
| "equalsListUnit"
| "equalsListCustomTag"
| "equalsCustomTag"
| "list_has_bytes"
| "list_has_int"
| "list_has_bool"
| "list_has_string"
| "list_has_data"
| "list_has_unit"
| "list_has_custom_tag"
| "list_len_bytes"
| "list_len_int"
| "list_len_bool"
| "list_len_string"
| "list_len_data"
| "list_len_unit"
| "list_len"
| "negateInteger"
| "not"
| "notEqualsBool"
| "notEqualsByteString"
| "notEqualsData"
| "notEqualsInteger"
| "notEqualsListBool"
| "notEqualsListByteString"
| "notEqualsListData"
| "notEqualsListInteger"
| "notEqualsListString"
| "notEqualsListUnit"
| "notEqualsListCustomTag"
| "notEqualsCustomTag"
| "notEqualsString"
) || plutus_builtin_name(name).is_some()
}
fn lower_not_equals(equals_builtin: &str, args: Vec<Term>) -> Term {
lower_if(
apply_builtin(equals_builtin, args),
Term::Constant(Constant::Bool(false)),
Term::Constant(Constant::Bool(true)),
)
}
fn lower_bool_equality(args: Vec<Term>) -> Term {
lower_if(args[0].clone(), args[1].clone(), lower_not(args[1].clone()))
}
fn lower_bool_inequality(args: Vec<Term>) -> Term {
lower_if(args[0].clone(), lower_not(args[1].clone()), args[1].clone())
}
fn lower_not(term: Term) -> Term {
lower_if(
term,
Term::Constant(Constant::Bool(false)),
Term::Constant(Constant::Bool(true)),
)
}
fn lower_if(condition: Term, then_term: Term, else_term: Term) -> Term {
Term::Force(Box::new(apply_forced_builtin(
"ifThenElse",
vec![
condition,
Term::Delay(Box::new(then_term)),
Term::Delay(Box::new(else_term)),
],
)))
}
fn apply_builtin(name: &str, args: Vec<Term>) -> Term {
args.into_iter()
.fold(Term::Builtin(name.to_string()), apply)
}
fn apply_forced_builtin(name: &str, args: Vec<Term>) -> Term {
apply_forced_builtin_n(name, 1, args)
}
fn apply_forced_builtin_n(name: &str, force_count: usize, args: Vec<Term>) -> Term {
let builtin = (0..force_count).fold(Term::Builtin(name.to_string()), |term, _| {
Term::Force(Box::new(term))
});
args.into_iter().fold(builtin, apply)
}
fn apply(function: Term, argument: Term) -> Term {
Term::Apply {
function: Box::new(function),
argument: Box::new(argument),
}
}
fn validate_term(
term: &Term,
scope: &mut Vec<String>,
depth: usize,
function_child: bool,
force_child: bool,
state: &mut ValidationState,
) {
state.nodes += 1;
state.max_depth = state.max_depth.max(depth);
match term {
Term::Var(name) => {
validate_identifier("variable", name, state);
if !scope.iter().rev().any(|bound| bound == name) {
state.errors.push(format!("unbound variable '{name}'"));
}
}
Term::Lambda { param, body } => {
validate_identifier("lambda parameter", param, state);
scope.push(param.clone());
validate_term(body, scope, depth + 1, false, false, state);
scope.pop();
}
Term::Apply { function, argument } => {
if !function_child {
validate_application_spine(term, force_child, state);
}
validate_term(function, scope, depth + 1, true, false, state);
validate_term(argument, scope, depth + 1, false, false, state);
}
Term::Delay(term) => validate_term(term, scope, depth + 1, false, false, state),
Term::Force(term) => validate_term(term, scope, depth + 1, function_child, true, state),
Term::Constant(constant) => validate_constant(constant, state),
Term::Error => (),
Term::Builtin(name) => {
if !function_child {
validate_builtin_application(name, 0, state);
}
}
}
}
fn validate_identifier(kind: &str, name: &str, state: &mut ValidationState) {
let mut chars = name.chars();
let Some(first) = chars.next() else {
state.errors.push(format!("{kind} name cannot be empty"));
return;
};
if !(first.is_ascii_alphabetic() || first == '_') {
state
.errors
.push(format!("{kind} '{name}' is not a valid UPLC identifier"));
return;
}
if !chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_') {
state
.errors
.push(format!("{kind} '{name}' is not a valid UPLC identifier"));
}
}
fn validate_constant(constant: &Constant, state: &mut ValidationState) {
if let Constant::ByteArray(hex) = constant {
if !hex.len().is_multiple_of(2) || !hex.chars().all(|ch| ch.is_ascii_hexdigit()) {
state
.errors
.push(format!("byte string constant '#{hex}' is not valid hex"));
}
}
}
fn validate_application_spine(term: &Term, force_child: bool, state: &mut ValidationState) {
let mut args = Vec::new();
let head = collect_application_spine(term, &mut args);
if let Some((name, force_count)) = collect_forced_builtin_head(head) {
validate_builtin_application(name, args.len(), state);
validate_builtin_forces(name, force_count, state);
validate_builtin_shape(name, &args, force_child, state);
}
}
fn collect_application_spine<'a>(term: &'a Term, args: &mut Vec<&'a Term>) -> &'a Term {
match term {
Term::Apply { function, argument } => {
let head = collect_application_spine(function, args);
args.push(argument);
head
}
_ => term,
}
}
fn collect_forced_builtin_head(term: &Term) -> Option<(&str, usize)> {
let mut force_count = 0;
let mut current = term;
while let Term::Force(inner) = current {
force_count += 1;
current = inner;
}
match current {
Term::Builtin(name) => Some((name, force_count)),
_ => None,
}
}
fn validate_builtin_application(name: &str, applied_args: usize, state: &mut ValidationState) {
let Some(expected_arity) = builtin_arity(name) else {
state.errors.push(format!("unknown UPLC builtin '{name}'"));
return;
};
state.builtin_applications.push(BuiltinApplication {
name: name.to_string(),
expected_arity,
applied_args,
});
if applied_args != expected_arity {
state.errors.push(format!(
"builtin '{name}' expects {expected_arity} argument(s), got {applied_args}"
));
}
}
fn validate_builtin_forces(name: &str, force_count: usize, state: &mut ValidationState) {
let Some(expected_force_count) = builtin_force_count(name) else {
return;
};
if force_count != expected_force_count {
state.errors.push(format!(
"builtin '{name}' requires {expected_force_count} force(s) before arguments, got {force_count}"
));
}
}
fn validate_builtin_shape(
name: &str,
args: &[&Term],
force_child: bool,
state: &mut ValidationState,
) {
if name != "ifThenElse" || args.len() != 3 {
return;
}
if !force_child {
state.errors.push(
"fully applied ifThenElse result must be forced to select a delayed branch".to_string(),
);
}
if !matches!(args[1], Term::Delay(_)) {
state
.errors
.push("ifThenElse then branch must be delayed".to_string());
}
if !matches!(args[2], Term::Delay(_)) {
state
.errors
.push("ifThenElse else branch must be delayed".to_string());
}
}
fn builtin_arity(name: &str) -> Option<usize> {
Some(match name {
"ifThenElse" => 3,
"equalsData" => 2,
"equalsInteger" => 2,
"equalsByteString" => 2,
"equalsString" => 2,
"appendByteString" => 2,
"lessThanInteger" => 2,
"lessThanEqualsInteger" => 2,
"addInteger" => 2,
"subtractInteger" => 2,
"multiplyInteger" => 2,
"quotientInteger" => 2,
"remainderInteger" => 2,
"sha2_256" => 1,
"blake2b_256" => 1,
"decodeUtf8" => 1,
"fstPair" => 1,
"sndPair" => 1,
"headList" => 1,
"tailList" => 1,
"nullList" => 1,
"unConstrData" => 1,
"unMapData" => 1,
"unListData" => 1,
"unIData" => 1,
"unBData" => 1,
"trace" => 2,
_ => return None,
})
}
fn builtin_force_count(name: &str) -> Option<usize> {
Some(match name {
"ifThenElse" | "trace" => 1,
"equalsData"
| "equalsInteger"
| "equalsByteString"
| "equalsString"
| "appendByteString"
| "lessThanInteger"
| "lessThanEqualsInteger"
| "addInteger"
| "subtractInteger"
| "multiplyInteger"
| "quotientInteger"
| "remainderInteger"
| "sha2_256"
| "blake2b_256"
| "decodeUtf8"
| "unConstrData"
| "unMapData"
| "unListData"
| "unIData"
| "unBData" => 0,
"headList" | "nullList" | "tailList" => 1,
"fstPair" | "sndPair" => 2,
_ => return None,
})
}
fn escape(value: &str) -> String {
value
.chars()
.flat_map(|ch| match ch {
'\\' => "\\\\".chars().collect::<Vec<_>>(),
'"' => "\\\"".chars().collect(),
'\n' => "\\n".chars().collect(),
'\r' => "\\r".chars().collect(),
'\t' => "\\t".chars().collect(),
_ => vec![ch],
})
.collect()
}
#[cfg(test)]
mod tests {
use crate::{compile_source, ir::Term as AeriTerm};
use super::*;
fn decode_cbor_bytestring(encoded: &[u8]) -> Vec<u8> {
let Some((&head, rest)) = encoded.split_first() else {
panic!("CBOR bytestring is empty");
};
let (offset, len) = match head {
0x40..=0x57 => (1, usize::from(head & 0x1f)),
0x58 => (2, usize::from(rest[0])),
0x59 => {
let len = u16::from_be_bytes([rest[0], rest[1]]) as usize;
(3, len)
}
0x5a => {
let len = u32::from_be_bytes([rest[0], rest[1], rest[2], rest[3]]) as usize;
(5, len)
}
0x5b => {
let len = u64::from_be_bytes([
rest[0], rest[1], rest[2], rest[3], rest[4], rest[5], rest[6], rest[7],
]) as usize;
(9, len)
}
_ => panic!("CBOR value is not a definite bytestring: 0x{head:02x}"),
};
assert_eq!(encoded.len(), offset + len);
encoded[offset..].to_vec()
}
#[test]
fn lowers_pure_validator_subset_to_uplc_core() {
let source = "module demo;\n\nvalidator gate(redeemer: ByteArray, ctx: Tx) { require redeemer == #01; true }\n";
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
assert!(
report.lowered(),
"unexpected blockers: {:?}",
report.blockers
);
let rendered = report.program.expect("uplc core program").render();
assert!(rendered.contains("uplc-program 1.0.0"));
assert!(rendered.contains("builtin equalsByteString"));
assert!(rendered.contains("builtin ifThenElse"));
}
#[test]
fn validates_lowered_pure_validator_subset() {
let source = "module demo;\n\nvalidator gate(datum: Data, redeemer: ByteArray, ctx: Tx) { require redeemer == #01; true }\n";
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(validation.nodes > 0);
assert!(validation.max_depth > 0);
assert!(
validation
.builtin_applications
.iter()
.any(|application| application.name == "equalsByteString"
&& application.applied_args == 2)
);
}
#[test]
fn flat_encoder_matches_known_identity_lambda_fixture() {
let program = Program {
version: (1, 0, 0),
term: Term::Lambda {
param: "x".to_string(),
body: Box::new(Term::Var("x".to_string())),
},
};
let flat = flat_encode_program(&program).expect("program flat-encodes");
assert_eq!(hex_encode(&flat), "010000200101");
assert_eq!(hex_encode(&cbor_bytestring(&flat)), "46010000200101");
}
#[test]
fn cbor_bytestring_wraps_flat_bytes_at_length_boundaries() {
let cases = [
(0usize, vec![0x40]),
(23, vec![0x57]),
(24, vec![0x58, 24]),
(255, vec![0x58, 255]),
(256, vec![0x59, 1, 0]),
(65_536, vec![0x5a, 0, 1, 0, 0]),
];
for (len, prefix) in cases {
let flat = vec![0xab; len];
let cbor = cbor_bytestring(&flat);
assert!(cbor.starts_with(&prefix), "bad prefix for length {len}");
assert_eq!(decode_cbor_bytestring(&cbor), flat);
}
}
#[test]
fn emits_cbor_script_for_supported_ledger_subset() {
let source = "module demo;\n\nvalidator gate(datum: Data, redeemer: ByteArray, ctx: Tx) { redeemer == #01 }\n";
let output = compile_source("demo.aeri", source).expect("source compiles");
let validator = &output.lowered_validators[0];
let ledger = emit_ledger_program(&validator.program, &validator.param_types)
.expect("supported subset emits ledger program");
assert_eq!(ledger.script_json.script_type, "PlutusScriptV2");
assert_eq!(ledger.script_json.cbor_hex, ledger.cbor_hex);
assert!(ledger.cbor_hex.starts_with('4') || ledger.cbor_hex.starts_with('5'));
assert_eq!(ledger.script_hash.len(), 56);
assert!(ledger.text.contains("builtin unBData"));
assert!(
ledger
.text
.contains("(lam ctx (force (apply (apply (apply (force (builtin ifThenElse))")
);
assert!(ledger.text.contains("(delay (con unit ()))"));
assert!(ledger.text.contains("(delay (error))"));
}
#[test]
fn emits_bool_plutus_data_decoder_for_ledger_param() {
let source = "module demo;\n\nvalidator gate(flag: Bool, ctx: Tx) { flag }\n";
let output = compile_source("demo.aeri", source).expect("source compiles");
let validator = &output.lowered_validators[0];
let ledger = emit_ledger_program(&validator.program, &validator.param_types)
.expect("Bool ledger parameter emits");
assert!(ledger.text.contains("builtin unConstrData"));
assert!(ledger.text.contains("builtin fstPair"));
assert!(ledger.text.contains("builtin sndPair"));
assert!(ledger.text.contains("builtin nullList"));
assert!(ledger.text.contains("builtin equalsInteger"));
assert!(ledger.text.contains("(con integer 0)"));
assert!(ledger.text.contains("(con integer 1)"));
assert!(ledger.text.contains("(con bool False)"));
assert!(ledger.text.contains("(con bool True)"));
assert_eq!(ledger.script_json.script_type, "PlutusScriptV2");
assert_eq!(ledger.script_hash.len(), 56);
}
#[test]
fn emits_string_plutus_data_decoder_for_ledger_param() {
let source = "module demo;\n\nvalidator gate(label: String, ctx: Tx) { label == \"go\" }\n";
let output = compile_source("demo.aeri", source).expect("source compiles");
let validator = &output.lowered_validators[0];
let ledger = emit_ledger_program(&validator.program, &validator.param_types)
.expect("String ledger parameter emits");
assert!(ledger.text.contains("builtin unBData"));
assert!(ledger.text.contains("builtin decodeUtf8"));
assert!(ledger.text.contains("builtin equalsString"));
assert!(ledger.text.contains("(con string \"go\")"));
assert_eq!(ledger.script_json.script_type, "PlutusScriptV2");
assert_eq!(ledger.script_hash.len(), 56);
}
#[test]
fn emits_unit_plutus_data_decoder_for_ledger_param() {
let source = "module demo;\n\nvalidator gate(marker: Unit, redeemer: ByteArray, ctx: Tx) { redeemer == #01 }\n";
let output = compile_source("demo.aeri", source).expect("source compiles");
let validator = &output.lowered_validators[0];
let ledger = emit_ledger_program(&validator.program, &validator.param_types)
.expect("Unit ledger parameter emits");
assert!(ledger.text.contains("builtin unConstrData"));
assert!(ledger.text.contains("builtin fstPair"));
assert!(ledger.text.contains("builtin sndPair"));
assert!(ledger.text.contains("builtin nullList"));
assert!(ledger.text.contains("builtin equalsInteger"));
assert!(ledger.text.contains("(con integer 0)"));
assert!(ledger.text.contains("(con unit ())"));
assert!(ledger.text.contains("builtin unBData"));
assert_eq!(ledger.script_json.script_type, "PlutusScriptV2");
assert_eq!(ledger.script_hash.len(), 56);
}
#[test]
fn ledger_cbor_round_trips_flat_bytes_for_constructor_deconstruction_subset() {
let source = r#"module demo;
type Action {
Spend(owner: ByteArray),
Close
}
validator gate(redeemer: ByteArray, ctx: Tx) {
let matched = match Spend(redeemer) {
Spend(owner) => owner == redeemer,
Close => false
};
matched &&
Spend(redeemer) == Spend(redeemer) &&
[Spend(redeemer), Close] == [Spend(redeemer), Close] &&
list_len([Spend(redeemer), Close]) == 2
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let validator = &output.lowered_validators[0];
let ledger = emit_ledger_program(&validator.program, &validator.param_types)
.expect("constructor subset emits ledger program");
let flat = hex_decode(&ledger.flat_hex).expect("flat hex decodes");
let cbor = hex_decode(&ledger.cbor_hex).expect("cbor hex decodes");
assert_eq!(ledger.script_json.script_type, "PlutusScriptV2");
assert_eq!(ledger.script_json.cbor_hex, ledger.cbor_hex);
assert_eq!(decode_cbor_bytestring(&cbor), flat);
assert_eq!(
ledger.script_hash,
script_hash(CARDANO_SCRIPT_VERSION, &flat)
);
assert_eq!(ledger.script_hash.len(), 56);
assert!(ledger.text.contains("builtin unBData"));
assert!(ledger.text.contains("builtin equalsByteString"));
assert!(ledger.text.contains("(apply (lam owner"));
assert!(ledger.text.contains("(delay (con unit ()))"));
}
#[test]
fn lowers_primitive_equality_to_typed_uplc_core() {
let source = "module demo;\n\nvalidator gate(redeemer: ByteArray, ctx: Tx) { let count = 1; let label = \"go\"; redeemer == #01 && count != 2 && label == \"go\" && true != false }\n";
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin equalsByteString"));
assert!(rendered.contains("builtin equalsInteger"));
assert!(rendered.contains("builtin equalsString"));
assert!(rendered.contains("builtin ifThenElse"));
assert!(!rendered.contains("builtin equalsData"));
}
#[test]
fn lowers_division_and_remainder_to_truncating_uplc_builtins() {
let source = "module demo;\n\nvalidator gate(redeemer: ByteArray, ctx: Tx) { let n = -3; n / 2 == -1 && n % 2 == -1 }\n";
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin quotientInteger"));
assert!(rendered.contains("builtin remainderInteger"));
assert!(!rendered.contains("builtin divideInteger"));
assert!(!rendered.contains("builtin modInteger"));
}
#[test]
fn lowers_hash_and_append_bytes_to_uplc_core() {
let source = "module demo;\n\nvalidator gate(redeemer: ByteArray, ctx: Tx) { sha2_256(append_bytes(redeemer, #02)) == sha2_256(#0102) && blake2b_256(redeemer) != #00 }\n";
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin appendByteString"));
assert!(rendered.contains("builtin sha2_256"));
assert!(rendered.contains("builtin blake2b_256"));
}
#[test]
fn lowers_list_literal_helpers_to_uplc_core() {
let source = r#"module demo;
const ADMINS: List<ByteArray> = [#01, #02];
const THRESHOLDS: List<Int> = [3, 5, 8];
validator gate(redeemer: ByteArray, ctx: Tx) {
list_has_bytes(ADMINS, redeemer) &&
list_has_int(THRESHOLDS, 5) &&
list_len_bytes(ADMINS) == 2 &&
list_len_int(THRESHOLDS) == 3
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin equalsByteString"));
assert!(rendered.contains("builtin equalsInteger"));
assert!(rendered.contains("(con bytes #01)"));
assert!(rendered.contains("(con integer 8)"));
assert!(!rendered.contains("list_has_bytes"));
assert!(!rendered.contains("list_len_int"));
}
#[test]
fn lowers_bool_string_and_data_list_literal_helpers_to_uplc_core() {
let source = r#"module demo;
validator gate(datum: Data, redeemer: ByteArray, ctx: Tx) {
list_has_bool([true, false], true) &&
list_has_string(["go", "stop"], "go") &&
list_has_data([datum], datum) &&
list_len_bool([true]) == 1 &&
list_len_string(["a", "b"]) == 2 &&
list_len_data([datum, datum]) == 2
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin equalsString"));
assert!(rendered.contains("builtin equalsData"));
assert!(rendered.contains("(con string \"go\")"));
assert!(rendered.contains("(con bool True)"));
assert!(!rendered.contains("list_has_bool"));
assert!(!rendered.contains("list_len_data"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin equalsData"));
assert!(artifact.text.contains("builtin equalsString"));
}
#[test]
fn lowers_generic_list_len_for_supported_literals_to_uplc_core() {
let source = r#"module demo;
type Action {
Open,
Close
}
validator gate(datum: Data, redeemer: ByteArray, ctx: Tx) {
list_len([redeemer, #01]) == 2 &&
list_len([true, false]) == 2 &&
list_len(["a", "b"]) == 2 &&
list_len([datum]) == 1 &&
list_len([(), ()]) == 2 &&
list_len([Open, Close]) == 2
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("(con integer 2)"));
assert!(rendered.contains("(con bytes #01)"));
assert!(rendered.contains("(con string \"a\")"));
assert!(!rendered.contains("list_len"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin unBData"));
assert!(artifact.text.contains("(delay (con unit ()))"));
}
#[test]
fn lowers_empty_list_len_and_bare_empty_equality_to_uplc_constants() {
let source = r#"module demo;
validator gate(redeemer: ByteArray, ctx: Tx) {
list_len([]) == 0 && [] == [] && !([] != []) && redeemer == #01
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
assert!(output.script.contains("list_len"));
assert!(output.script.contains("(con bool True)"));
assert!(output.script.contains("(con bool False)"));
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("empty list constants lower");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("(con integer 0)"));
assert!(rendered.contains("(con bool True)"));
assert!(rendered.contains("(con bool False)"));
assert!(!rendered.contains("list_len"));
assert!(!rendered.contains("equalsList"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("(con integer 0)"));
assert!(!artifact.text.contains("equalsList"));
assert_eq!(artifact.script_json.script_type, "PlutusScriptV2");
}
#[test]
fn lowers_typed_empty_list_equality_to_uplc_core() {
let source = r#"module demo;
validator gate(items: List<Int>, ctx: Tx) {
[] == items && items == [] && [] != [1] && [1] != []
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
assert!(output.script.contains("equalsListInteger"));
assert!(output.script.contains("notEqualsListInteger"));
let report = lower_program(&output.lowered_validators[0].program);
assert!(
report.blockers.is_empty(),
"unexpected blockers: {:?}",
report.blockers
);
let program = report.program.expect("typed empty list equality lowers");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin nullList"));
assert!(rendered.contains("(con integer 1)"));
assert!(rendered.contains("(con bool True)"));
assert!(!rendered.contains("equalsListInteger"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin unListData"));
assert!(artifact.text.contains("builtin unIData"));
assert!(!artifact.text.contains("equalsListInteger"));
}
#[test]
fn lowers_non_primitive_typed_empty_list_equality_to_uplc_core() {
let source = r#"module demo;
type Action {
Open,
Close
}
validator unit_gate(items: List<Unit>, ctx: Tx) {
[] == items && items == [] && [] != [()] && [()] != []
}
validator custom_gate(actions: List<Action>, ctx: Tx) {
[] == actions && actions == [] && [] != [Open] && [Open] != []
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
assert!(output.script.contains("equalsListUnit"));
assert!(output.script.contains("notEqualsListUnit"));
assert!(output.script.contains("equalsListCustomTag"));
assert!(output.script.contains("notEqualsListCustomTag"));
let unit_validator = output
.lowered_validators
.iter()
.find(|validator| validator.title == "demo.unit_gate")
.expect("unit validator exists");
let unit_report = lower_program(&unit_validator.program);
assert!(
unit_report.blockers.is_empty(),
"unexpected unit blockers: {:?}",
unit_report.blockers
);
let unit_program = unit_report.program.expect("unit equality lowers");
let unit_validation = validate_program(&unit_program);
let unit_text = unit_program.render();
assert!(
unit_validation.valid,
"unexpected unit errors: {:?}",
unit_validation.errors
);
assert!(unit_text.contains("builtin nullList"));
assert!(unit_text.contains("(con unit ())"));
assert!(!unit_text.contains("equalsListUnit"));
let custom_validator = output
.lowered_validators
.iter()
.find(|validator| validator.title == "demo.custom_gate")
.expect("custom validator exists");
let custom_report = lower_program(&custom_validator.program);
assert!(
custom_report.blockers.is_empty(),
"unexpected custom blockers: {:?}",
custom_report.blockers
);
let custom_program = custom_report.program.expect("custom equality lowers");
let custom_validation = validate_program(&custom_program);
let custom_text = custom_program.render();
assert!(
custom_validation.valid,
"unexpected custom errors: {:?}",
custom_validation.errors
);
assert!(custom_text.contains("builtin nullList"));
assert!(custom_text.contains("(con integer 0)"));
assert!(!custom_text.contains("equalsListCustomTag"));
let unit_artifact =
emit_ledger_program(&unit_validator.program, &unit_validator.param_types)
.expect("unit ledger program emits");
assert!(unit_artifact.text.contains("builtin unListData"));
assert!(unit_artifact.text.contains("builtin nullList"));
let custom_artifact = emit_ledger_program_with_layouts(
&custom_validator.program,
&custom_validator.param_types,
&custom_validator.custom_types,
)
.expect("custom ledger program emits");
assert!(custom_artifact.text.contains("builtin unListData"));
assert!(custom_artifact.text.contains("builtin unConstrData"));
assert!(custom_artifact.text.contains("builtin equalsInteger"));
}
#[test]
fn lowers_generic_list_len_for_direct_constructor_literals_to_uplc_core() {
let source = r#"module demo;
type Action {
Spend(owner: ByteArray),
Close
}
validator gate(redeemer: ByteArray, ctx: Tx) {
list_len([Spend(redeemer), Close]) == 2
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("(apply (lam aeri_list_item"));
assert!(rendered.contains("redeemer"));
assert!(rendered.contains("(con integer 2)"));
assert!(!rendered.contains("custom constructors with fields"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin unBData"));
assert!(artifact.text.contains("(delay (con unit ()))"));
}
#[test]
fn lowers_generic_list_len_for_direct_constructor_literals_with_supported_fields() {
let source = r#"module demo;
type Mode {
Hot,
Cold
}
type Envelope {
Wrap(ok: Bool, amount: Int, owner: ByteArray, label: String, payload: Data, marker: Unit, mode: Mode),
Other
}
validator gate(payload: Data, redeemer: ByteArray, ctx: Tx) {
list_len([Wrap(true, 7, redeemer, "ok", payload, (), Hot), Other]) == 2
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
assert!(
report.blockers.is_empty(),
"unexpected blockers: {:?}",
report.blockers
);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("(apply (lam aeri_list_item"));
assert!(rendered.contains("redeemer"));
assert!(rendered.contains("payload"));
assert!(rendered.contains("(con integer 7)"));
assert!(rendered.contains("(con string \"ok\")"));
assert!(rendered.contains("(con unit ())"));
assert!(rendered.contains("(con integer 2)"));
assert!(!rendered.contains("custom constructors with fields"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin unBData"));
assert!(artifact.text.contains("(delay (con unit ()))"));
}
#[test]
fn lowers_generic_list_len_for_direct_constructor_literals_with_nested_direct_fields() {
let source = r#"module demo;
type Token {
Native(policy: ByteArray),
Other
}
type Action {
Spend(token: Token),
Close
}
validator gate(redeemer: ByteArray, ctx: Tx) {
list_len([Spend(Native(redeemer)), Spend(Other), Close]) == 3
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
assert!(
report.blockers.is_empty(),
"unexpected blockers: {:?}",
report.blockers
);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("(apply (lam aeri_list_item"));
assert!(rendered.contains("redeemer"));
assert!(rendered.contains("(con integer 3)"));
assert!(!rendered.contains("custom constructors with fields"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin unBData"));
assert!(artifact.text.contains("(delay (con unit ()))"));
}
#[test]
fn generic_list_len_binds_constructor_fields_before_length_result() {
let source = r#"module demo;
type Action {
Spend(owner: ByteArray, token: ByteArray),
Close
}
validator gate(redeemer: ByteArray, ctx: Tx) {
list_len([Spend(redeemer, fail)]) == 1
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("(apply (lam aeri_list_item"));
assert!(rendered.contains("(error)"));
assert!(rendered.contains("(con integer 1)"));
assert!(!rendered.contains("(con integer 2)"));
}
#[test]
fn generic_list_len_binds_nested_constructor_fields_before_length_result() {
let source = r#"module demo;
type Token {
Native(policy: ByteArray),
Other
}
type Action {
Spend(token: Token),
Close
}
validator gate(redeemer: ByteArray, ctx: Tx) {
list_len([Spend(Native(fail))]) == 1
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("(apply (lam aeri_list_item"));
assert!(rendered.contains("(error)"));
assert!(rendered.contains("(con integer 1)"));
assert!(!rendered.contains("custom constructors with fields"));
}
#[test]
fn reports_nested_list_field_in_direct_constructor_list_len_as_core_blocker() {
let source = r#"module demo;
type Action {
Spend(ids: List<ByteArray>),
Close
}
validator gate(redeemer: ByteArray, ctx: Tx) {
list_len([Spend([redeemer])]) == 1
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
assert!(!report.lowered());
assert!(report.program.is_none());
assert!(report.blockers.iter().any(|blocker| {
blocker.contains("list literals need Plutus list encoding and element type lowering")
}));
}
#[test]
fn lowers_generic_list_has_for_primitive_literals_to_uplc_core() {
let source = r#"module demo;
validator gate(datum: Data, redeemer: ByteArray, ctx: Tx) {
list_has([redeemer, #01], redeemer) &&
!list_has([], redeemer) &&
list_has([true, false], true) &&
!list_has([], false) &&
list_has(["go", "stop"], "go") &&
!list_has([], "go") &&
list_has([datum], datum)
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(output.script.contains("list_has_bytes"));
assert!(output.script.contains("list_has_bool"));
assert!(output.script.contains("list_has_string"));
assert!(output.script.contains("list_has_data"));
assert!(rendered.contains("builtin equalsByteString"));
assert!(rendered.contains("builtin equalsString"));
assert!(rendered.contains("builtin equalsData"));
assert!(!rendered.contains("list_has"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin equalsData"));
assert!(artifact.text.contains("builtin unBData"));
}
#[test]
fn lowers_generic_list_has_for_unit_literals_to_uplc_core() {
let source = r#"module demo;
validator gate(marker: Unit, redeemer: ByteArray, ctx: Tx) {
list_has([()], marker) && list_has([(), ()], ()) && !list_has([], marker) && redeemer == #01
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
assert!(output.script.contains("list_has_unit"));
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("(con unit ())"));
assert!(rendered.contains("(con bool True)"));
assert!(rendered.contains("(con bool False)"));
assert!(!rendered.contains("list_has_unit"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin unConstrData"));
assert!(artifact.text.contains("(con unit ())"));
}
#[test]
fn lowers_generic_list_has_for_nullary_custom_literals_to_uplc_core() {
let source = r#"module demo;
type Action {
Open,
Close
}
validator gate(redeemer: ByteArray, ctx: Tx) {
list_has([Open, Close], Open) && !list_has([Open], Close) && !list_has([], Close)
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(output.script.contains("list_has_custom_tag"));
assert!(rendered.contains("builtin equalsInteger"));
assert!(rendered.contains("(con integer 0)"));
assert!(rendered.contains("(con integer 1)"));
assert!(!rendered.contains("list_has_custom_tag"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin equalsInteger"));
assert!(artifact.text.contains("(delay (con unit ()))"));
}
#[test]
fn lowers_generic_list_has_for_direct_constructor_literals_with_supported_fields() {
let source = r#"module demo;
type Mode {
Hot,
Cold
}
type Envelope {
Wrap(ok: Bool, amount: Int, owner: ByteArray, label: String, payload: Data, marker: Unit, mode: Mode),
Other
}
validator gate(payload: Data, redeemer: ByteArray, ctx: Tx) {
list_has([Wrap(true, 7, redeemer, "ok", payload, (), Hot), Other], Wrap(true, 7, #01, "ok", payload, (), Hot)) &&
!list_has([Wrap(true, 7, #02, "ok", payload, (), Hot)], Wrap(true, 7, #02, "ok", payload, (), Cold))
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
assert!(
report.blockers.is_empty(),
"unexpected blockers: {:?}",
report.blockers
);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin ifThenElse"));
assert!(rendered.contains("builtin equalsInteger"));
assert!(rendered.contains("builtin equalsByteString"));
assert!(rendered.contains("builtin equalsString"));
assert!(rendered.contains("builtin equalsData"));
assert!(rendered.contains("aeri_list_has_constructor_field"));
assert!(rendered.contains("aeri_list_has_needle_field"));
assert!(rendered.contains("(con unit ())"));
assert!(!rendered.contains("equalsAeriValue"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin equalsInteger"));
assert!(artifact.text.contains("builtin equalsByteString"));
assert!(artifact.text.contains("builtin equalsString"));
assert!(artifact.text.contains("builtin equalsData"));
assert!(artifact.text.contains("builtin unBData"));
}
#[test]
fn lowers_generic_list_has_for_empty_direct_constructor_literals_with_supported_fields() {
let source = r#"module demo;
type Mode {
Hot,
Cold
}
type Envelope {
Wrap(ok: Bool, amount: Int, owner: ByteArray, label: String, payload: Data, marker: Unit, mode: Mode),
Other
}
validator gate(payload: Data, redeemer: ByteArray, ctx: Tx) {
!list_has([], Wrap(true, 7, redeemer, "ok", payload, (), Hot)) && !list_has([], Other)
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
assert!(
report.blockers.is_empty(),
"unexpected blockers: {:?}",
report.blockers
);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("aeri_empty_list_has_needle_field"));
assert!(rendered.contains("redeemer"));
assert!(rendered.contains("payload"));
assert!(rendered.contains("(con bool False)"));
assert!(rendered.contains("(con unit ())"));
assert!(!rendered.contains("equalsAeriValue"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin unBData"));
assert!(artifact.text.contains("(delay (con unit ()))"));
}
#[test]
fn direct_constructor_list_has_binds_item_fields_before_membership_logic() {
let source = r#"module demo;
type Action {
Spend(owner: ByteArray),
Close
}
validator gate(redeemer: ByteArray, ctx: Tx) {
list_has([Spend(fail), Close], Spend(redeemer))
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("aeri_list_has_constructor_field"));
assert!(rendered.contains("aeri_list_has_needle_field"));
assert!(rendered.contains("(error)"));
assert!(rendered.contains("builtin equalsByteString"));
assert!(!rendered.contains("equalsAeriValue"));
}
#[test]
fn direct_constructor_list_has_binds_needle_fields_before_tag_mismatch() {
let source = r#"module demo;
type Action {
Spend(owner: ByteArray),
Close
}
validator gate(redeemer: ByteArray, ctx: Tx) {
!list_has([Close], Spend(fail)) && redeemer == #01
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("aeri_list_has_needle_field"));
assert!(rendered.contains("(error)"));
assert!(rendered.contains("(con bool False)"));
assert!(!rendered.contains("equalsAeriValue"));
}
#[test]
fn empty_direct_constructor_list_has_binds_needle_fields_before_false() {
let source = r#"module demo;
type Action {
Spend(owner: ByteArray),
Close
}
validator gate(redeemer: ByteArray, ctx: Tx) {
!list_has([], Spend(fail)) && redeemer == #01
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("aeri_empty_list_has_needle_field"));
assert!(rendered.contains("(error)"));
assert!(rendered.contains("(con bool False)"));
assert!(!rendered.contains("equalsAeriValue"));
}
#[test]
fn lowers_list_literal_helpers_with_variable_elements_to_uplc_core() {
let source = r#"module demo;
validator gate(owner: ByteArray, redeemer: ByteArray, ctx: Tx) {
list_has_bytes([owner, #02], redeemer)
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("(apply (lam aeri_list_item"));
assert!(rendered.contains("(apply (lam aeri_list_needle"));
assert!(rendered.contains("owner"));
assert!(rendered.contains("builtin equalsByteString"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin unBData"));
assert!(artifact.text.contains("(delay (con unit ()))"));
}
#[test]
fn list_helper_lowering_binds_items_before_helper_logic() {
let program = AeriProgram::new(AeriTerm::BuiltinCall {
name: "list_has_int".to_string(),
args: vec![
AeriTerm::List(vec![AeriTerm::Int(1), AeriTerm::Error("boom".to_string())]),
AeriTerm::Int(1),
],
});
let report = lower_program(&program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("(apply (lam aeri_list_item"));
assert!(rendered.contains("(apply (lam aeri_list_item_1"));
assert!(rendered.contains("(apply (lam aeri_list_needle"));
assert!(rendered.contains("(error)"));
}
#[test]
fn lowers_list_literal_equality_to_uplc_core() {
let source = r#"module demo;
const WANT: List<Int> = [1, 2];
validator gate(value: Int, ctx: Tx) {
[1, value] == WANT && [value] != [3]
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin equalsInteger"));
assert!(rendered.contains("(apply (lam aeri_list_left"));
assert!(rendered.contains("(apply (lam aeri_list_right"));
assert!(!rendered.contains("equalsAeriValue"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin unIData"));
assert!(artifact.text.contains("(delay (con unit ()))"));
}
#[test]
fn lowers_remaining_primitive_list_literal_equality_to_uplc_core() {
let source = r#"module demo;
validator gate(datum: Data, redeemer: ByteArray, ctx: Tx) {
[true, false] == [true, false] &&
[redeemer, #01] != [#02, redeemer] &&
["a", "b"] == ["a", "b"] &&
[datum] == [datum]
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
assert!(output.script.contains("equalsListBool"));
assert!(output.script.contains("notEqualsListByteString"));
assert!(output.script.contains("equalsListString"));
assert!(output.script.contains("equalsListData"));
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("list literal equality lowers");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin equalsByteString"));
assert!(rendered.contains("builtin equalsString"));
assert!(rendered.contains("builtin equalsData"));
assert!(rendered.contains("(apply (lam aeri_list_left"));
assert!(rendered.contains("(apply (lam aeri_list_right"));
assert!(!rendered.contains("equalsAeriValue"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin equalsByteString"));
assert!(artifact.text.contains("builtin equalsString"));
assert!(artifact.text.contains("builtin equalsData"));
assert_eq!(artifact.script_json.script_type, "PlutusScriptV2");
}
#[test]
fn list_literal_equality_binds_items_before_length_result() {
let program = AeriProgram::new(AeriTerm::BuiltinCall {
name: "equalsListInteger".to_string(),
args: vec![
AeriTerm::List(vec![AeriTerm::Error("boom".to_string())]),
AeriTerm::List(vec![]),
],
});
let report = lower_program(&program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("(apply (lam aeri_list_left"));
assert!(rendered.contains("(error)"));
assert!(rendered.contains("(con bool False)"));
}
#[test]
fn lowers_mixed_runtime_and_literal_primitive_list_equality() {
let program = AeriProgram::new(AeriTerm::Lambda {
param: "ids".to_string(),
body: Box::new(AeriTerm::BuiltinCall {
name: "equalsListInteger".to_string(),
args: vec![
AeriTerm::Var("ids".to_string()),
AeriTerm::List(vec![AeriTerm::Int(1), AeriTerm::Int(2)]),
],
}),
});
let report = lower_program(&program);
let program = report.program.expect("mixed list equality lowers");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin nullList"));
assert!(rendered.contains("builtin headList"));
assert!(rendered.contains("builtin tailList"));
assert!(rendered.contains("builtin unIData"));
assert!(rendered.contains("builtin equalsInteger"));
}
#[test]
fn lowers_mixed_runtime_and_literal_remaining_primitive_list_equality_from_source() {
let source = r#"module demo;
validator bool_gate(items: List<Bool>, ctx: Tx) {
items == [true, false] && [true, false] == items && items != [false] && [false] != items
}
validator bytes_gate(items: List<ByteArray>, wanted: ByteArray, ctx: Tx) {
items == [wanted, #01] && [wanted, #01] == items && items != [] && [#ff] != items
}
validator string_gate(items: List<String>, ctx: Tx) {
items == ["a", "b"] && ["a", "b"] == items && items != [] && ["z"] != items
}
validator data_gate(items: List<Data>, wanted: Data, ctx: Tx) {
items == [wanted] && [wanted] == items && items != [] && [wanted, wanted] != items
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
assert_eq!(output.validator_count, 4);
assert!(output.script.contains("equalsListBool"));
assert!(output.script.contains("notEqualsListBool"));
assert!(output.script.contains("equalsListByteString"));
assert!(output.script.contains("notEqualsListByteString"));
assert!(output.script.contains("equalsListString"));
assert!(output.script.contains("notEqualsListString"));
assert!(output.script.contains("equalsListData"));
assert!(output.script.contains("notEqualsListData"));
let emit_text = |title: &str| {
let validator = output
.lowered_validators
.iter()
.find(|validator| validator.title == title)
.expect("validator exists");
let report = lower_program(&validator.program);
let program = report.program.expect("mixed list equality lowers");
let validation = validate_program(&program);
assert!(
validation.valid,
"unexpected errors for {title}: {:?}",
validation.errors
);
let artifact = emit_ledger_program(&validator.program, &validator.param_types)
.expect("ledger program emits");
assert_eq!(artifact.script_json.script_type, "PlutusScriptV2");
artifact.text
};
let bool_text = emit_text("demo.bool_gate");
assert!(bool_text.contains("builtin unListData"));
assert!(bool_text.contains("builtin unConstrData"));
assert!(bool_text.contains("builtin nullList"));
let bytes_text = emit_text("demo.bytes_gate");
assert!(bytes_text.contains("builtin unListData"));
assert!(bytes_text.contains("builtin unBData"));
assert!(bytes_text.contains("builtin equalsByteString"));
let string_text = emit_text("demo.string_gate");
assert!(string_text.contains("builtin unListData"));
assert!(string_text.contains("builtin unBData"));
assert!(string_text.contains("builtin decodeUtf8"));
assert!(string_text.contains("builtin equalsString"));
let data_text = emit_text("demo.data_gate");
assert!(data_text.contains("builtin unListData"));
assert!(data_text.contains("builtin equalsData"));
}
#[test]
fn lowers_runtime_nullary_custom_list_params_to_uplc_core() {
let source = r#"module demo;
type Action {
Open,
Close
}
validator gate(actions: List<Action>, expected: List<Action>, ctx: Tx) {
require list_has(actions, Open);
actions == expected &&
list_len(actions) == list_len(expected) &&
actions == [Open, Close] &&
[Open, Close] == actions &&
actions != [Close] &&
[Close] != actions
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
assert!(output.script.contains("list_has_custom_tag"));
assert!(output.script.contains("equalsListCustomTag"));
assert!(output.script.contains("notEqualsListCustomTag"));
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("nullary custom list params lower");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin unConstrData"));
assert!(rendered.contains("builtin fstPair"));
assert!(rendered.contains("builtin sndPair"));
assert!(rendered.contains("builtin nullList"));
assert!(rendered.contains("builtin addInteger"));
assert!(rendered.contains("builtin equalsInteger"));
assert!(!rendered.contains("list_has_custom_tag"));
assert!(!rendered.contains("equalsListCustomTag"));
let artifact = emit_ledger_program_with_layouts(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
&output.lowered_validators[0].custom_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin unListData"));
assert!(artifact.text.contains("builtin unConstrData"));
assert_eq!(artifact.script_json.script_type, "PlutusScriptV2");
}
#[test]
fn reports_fieldful_custom_list_parameter_as_emit_blocker() {
let source = r#"module demo;
type Action {
Spend(owner: ByteArray),
Close
}
validator gate(actions: List<Action>, ctx: Tx) {
list_len(actions) >= 0
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
assert!(
report.lowered(),
"unexpected blockers: {:?}",
report.blockers
);
let error = emit_ledger_program_with_layouts(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
&output.lowered_validators[0].custom_types,
)
.expect_err("fieldful custom list parameter blocks emission");
assert!(error.blockers.iter().any(|blocker| {
blocker.contains(
"validator parameter 'actions: List<Action>' needs Plutus Data decoder lowering",
)
}));
}
#[test]
fn lowers_runtime_primitive_list_equality_for_ledger_list_params() {
let source = r#"module demo;
validator gate(left: List<Int>, right: List<Int>, ctx: Tx) {
left == right && left != right
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("runtime list equality lowers");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin nullList"));
assert!(rendered.contains("builtin headList"));
assert!(rendered.contains("builtin tailList"));
assert!(rendered.contains("builtin unIData"));
assert!(rendered.contains("builtin equalsInteger"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin unListData"));
assert!(artifact.text.contains("builtin equalsInteger"));
assert_eq!(artifact.script_json.script_type, "PlutusScriptV2");
}
#[test]
fn lowers_remaining_runtime_primitive_list_equality_for_ledger_list_params() {
let source = r#"module demo;
validator bool_gate(left: List<Bool>, right: List<Bool>, ctx: Tx) {
left == right || left != right
}
validator bytes_gate(left: List<ByteArray>, right: List<ByteArray>, ctx: Tx) {
left == right || left != right
}
validator string_gate(left: List<String>, right: List<String>, ctx: Tx) {
left == right || left != right
}
validator data_gate(left: List<Data>, right: List<Data>, ctx: Tx) {
left == right || left != right
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
assert_eq!(output.validator_count, 4);
assert!(output.script.contains("equalsListBool"));
assert!(output.script.contains("notEqualsListBool"));
assert!(output.script.contains("equalsListByteString"));
assert!(output.script.contains("notEqualsListByteString"));
assert!(output.script.contains("equalsListString"));
assert!(output.script.contains("notEqualsListString"));
assert!(output.script.contains("equalsListData"));
assert!(output.script.contains("notEqualsListData"));
let emit_text = |title: &str| {
let validator = output
.lowered_validators
.iter()
.find(|validator| validator.title == title)
.expect("validator exists");
let report = lower_program(&validator.program);
let program = report.program.expect("runtime list equality lowers");
let validation = validate_program(&program);
assert!(
validation.valid,
"unexpected errors for {title}: {:?}",
validation.errors
);
let artifact = emit_ledger_program(&validator.program, &validator.param_types)
.expect("ledger program emits");
assert_eq!(artifact.script_json.script_type, "PlutusScriptV2");
artifact.text
};
let bool_text = emit_text("demo.bool_gate");
assert!(bool_text.contains("builtin unListData"));
assert!(bool_text.contains("builtin unConstrData"));
assert!(bool_text.contains("builtin nullList"));
let bytes_text = emit_text("demo.bytes_gate");
assert!(bytes_text.contains("builtin unListData"));
assert!(bytes_text.contains("builtin unBData"));
assert!(bytes_text.contains("builtin equalsByteString"));
let string_text = emit_text("demo.string_gate");
assert!(string_text.contains("builtin unListData"));
assert!(string_text.contains("builtin unBData"));
assert!(string_text.contains("builtin decodeUtf8"));
assert!(string_text.contains("builtin equalsString"));
let data_text = emit_text("demo.data_gate");
assert!(data_text.contains("builtin unListData"));
assert!(data_text.contains("builtin equalsData"));
}
#[test]
fn lowers_runtime_unit_list_equality_for_ledger_list_params() {
let source = r#"module demo;
validator gate(items: List<Unit>, expected: List<Unit>, ctx: Tx) {
require items == expected;
items == [(), ()] && [(), ()] == items && items != [()] && [()] != items && items != [] && list_len(items) == list_len(expected)
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
assert!(output.script.contains("equalsListUnit"));
assert!(output.script.contains("notEqualsListUnit"));
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("runtime unit list equality lowers");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin nullList"));
assert!(rendered.contains("builtin headList"));
assert!(rendered.contains("builtin tailList"));
assert!(rendered.contains("builtin unConstrData"));
assert!(!rendered.contains("equalsAeriValue"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin unListData"));
assert!(artifact.text.contains("builtin unConstrData"));
assert!(artifact.text.contains("builtin nullList"));
assert_eq!(artifact.script_json.script_type, "PlutusScriptV2");
}
#[test]
fn lowers_generic_unit_list_has_for_ledger_list_params() {
let source = r#"module demo;
validator gate(items: List<Unit>, marker: Unit, ctx: Tx) {
list_has(items, marker) && list_len_unit(items) >= 1
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
assert!(output.script.contains("list_has_unit"));
assert!(output.script.contains("list_len_unit"));
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("runtime unit list_has lowers");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin nullList"));
assert!(rendered.contains("builtin headList"));
assert!(rendered.contains("builtin tailList"));
assert!(rendered.contains("builtin unConstrData"));
assert!(rendered.contains("builtin addInteger"));
assert!(!rendered.contains("list_has_unit"));
assert!(!rendered.contains("list_len_unit"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin unListData"));
assert!(artifact.text.contains("builtin unConstrData"));
assert_eq!(artifact.script_json.script_type, "PlutusScriptV2");
}
#[test]
fn lowers_nullary_constructor_match_to_uplc_core() {
let source = r#"module demo;
type Action {
Open,
Close
}
validator gate(redeemer: ByteArray, ctx: Tx) {
match Open {
Open => redeemer == #01,
Close => false
}
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin equalsInteger"));
assert!(rendered.contains("(con integer 0)"));
assert!(rendered.contains("(con integer 1)"));
assert!(rendered.contains("builtin equalsByteString"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("(delay (con unit ()))"));
}
#[test]
fn lowers_nullary_constructor_equality_to_uplc_core() {
let source = r#"module demo;
type Action {
Open,
Close
}
validator gate(redeemer: ByteArray, ctx: Tx) {
Open != Close && Open == Open
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin equalsInteger"));
assert!(rendered.contains("(con integer 0)"));
assert!(rendered.contains("(con integer 1)"));
assert!(!rendered.contains("equalsCustomTag"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("(delay (con unit ()))"));
}
#[test]
fn lowers_nullary_constructor_list_equality_to_uplc_core() {
let source = r#"module demo;
type Action {
Open,
Close
}
validator gate(redeemer: ByteArray, ctx: Tx) {
[Open, Close] == [Open, Close] && [Open] != [Close]
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin equalsInteger"));
assert!(rendered.contains("(apply (lam aeri_list_left"));
assert!(rendered.contains("(apply (lam aeri_list_right"));
assert!(rendered.contains("(con integer 0)"));
assert!(rendered.contains("(con integer 1)"));
assert!(!rendered.contains("equalsListCustomTag"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("(delay (con unit ()))"));
}
#[test]
fn lowers_direct_constructor_field_list_equality_to_uplc_core() {
let source = r#"module demo;
type Action {
Spend(owner: ByteArray),
Close
}
validator gate(redeemer: ByteArray, ctx: Tx) {
[Spend(redeemer), Close] == [Spend(#01), Close] && [Spend(#01)] != [Close]
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
assert!(
report.blockers.is_empty(),
"unexpected blockers: {:?}",
report.blockers
);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin equalsByteString"));
assert!(rendered.contains("aeri_list_constructor_field"));
assert!(!rendered.contains("equalsAeriValue"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin equalsByteString"));
assert!(artifact.text.contains("builtin unBData"));
}
#[test]
fn lowers_direct_constructor_field_list_equality_for_supported_field_kinds() {
let source = r#"module demo;
type Mode {
Hot,
Cold
}
type Envelope {
Wrap(ok: Bool, amount: Int, owner: ByteArray, label: String, payload: Data, marker: Unit, mode: Mode),
Other
}
validator gate(payload: Data, redeemer: ByteArray, ctx: Tx) {
[Wrap(true, 7, redeemer, "ok", payload, (), Hot), Other] == [Wrap(true, 7, #01, "ok", payload, (), Hot), Other]
&& [Wrap(true, 7, #02, "ok", payload, (), Hot)] != [Wrap(true, 7, #02, "ok", payload, (), Cold)]
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
assert!(
report.blockers.is_empty(),
"unexpected blockers: {:?}",
report.blockers
);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin ifThenElse"));
assert!(rendered.contains("builtin equalsInteger"));
assert!(rendered.contains("builtin equalsByteString"));
assert!(rendered.contains("builtin equalsString"));
assert!(rendered.contains("builtin equalsData"));
assert!(rendered.contains("aeri_list_constructor_field"));
assert!(rendered.contains("(con unit ())"));
assert!(!rendered.contains("equalsAeriValue"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin equalsInteger"));
assert!(artifact.text.contains("builtin equalsByteString"));
assert!(artifact.text.contains("builtin equalsString"));
assert!(artifact.text.contains("builtin equalsData"));
assert!(artifact.text.contains("builtin unBData"));
}
#[test]
fn lowers_direct_constructor_field_list_equality_against_empty_lists() {
let source = r#"module demo;
type Action {
Spend(owner: ByteArray),
Close
}
validator gate(redeemer: ByteArray, ctx: Tx) {
[Spend(redeemer)] != [] && [] != [Spend(#01)] && [] == []
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
assert!(
report.blockers.is_empty(),
"unexpected blockers: {:?}",
report.blockers
);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("aeri_list_constructor_field"));
assert!(rendered.contains("redeemer"));
assert!(rendered.contains("(con bool True)"));
assert!(rendered.contains("(con bool False)"));
assert!(!rendered.contains("equalsAeriValue"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin unBData"));
assert!(artifact.text.contains("(delay (con unit ()))"));
}
#[test]
fn direct_constructor_list_equality_binds_fields_before_length_mismatch() {
let source = r#"module demo;
type Action {
Spend(owner: ByteArray),
Close
}
validator gate(redeemer: ByteArray, ctx: Tx) {
[Spend(fail)] != []
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("aeri_list_constructor_field"));
assert!(rendered.contains("(error)"));
assert!(rendered.contains("(con bool True)"));
assert!(!rendered.contains("equalsAeriValue"));
}
#[test]
fn direct_constructor_list_equality_binds_right_fields_before_length_mismatch() {
let source = r#"module demo;
type Action {
Spend(owner: ByteArray),
Close
}
validator gate(redeemer: ByteArray, ctx: Tx) {
[] != [Spend(fail)]
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("aeri_list_constructor_field"));
assert!(rendered.contains("(error)"));
assert!(rendered.contains("(con bool True)"));
assert!(!rendered.contains("equalsAeriValue"));
}
#[test]
fn reports_runtime_constructor_field_list_equality_as_core_blocker() {
let source = r#"module demo;
type Action {
Spend(owner: ByteArray),
Close
}
validator gate(redeemer: ByteArray, ctx: Tx) {
let actions = [Spend(#01)];
actions == actions
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
assert!(!report.lowered());
assert!(report.program.is_none());
assert!(report.blockers.iter().any(|blocker| {
blocker.contains("list literals need Plutus list encoding")
|| blocker.contains("structural equality")
}));
}
#[test]
fn reports_shadowed_constructor_equality_as_core_blocker() {
let source = r#"module demo;
type Action {
Spend(owner: ByteArray),
Close
}
validator direct_gate(Close: Action, ctx: Tx) {
Close == Spend(#01)
}
validator list_gate(Close: Action, ctx: Tx) {
[Close] == [Spend(#01)]
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
assert!(output.script.contains("equalsAeriValue"));
assert!(!output.script.contains("aeri_eq_left"));
assert!(!output.script.contains("aeri_list_constructor_field"));
for validator in &output.lowered_validators {
let report = lower_program(&validator.program);
assert!(
!report.lowered(),
"{} unexpectedly lowered",
validator.title
);
assert!(report.program.is_none());
assert!(report.blockers.iter().any(|blocker| {
blocker.contains("list literals need Plutus list encoding")
|| blocker.contains("custom constructors with fields")
|| blocker.contains("structural equality")
}));
}
}
#[test]
fn lowers_direct_constructor_field_equality_to_uplc_core() {
let source = r#"module demo;
type Action {
Spend(owner: ByteArray),
Close
}
validator gate(redeemer: ByteArray, ctx: Tx) {
Spend(redeemer) == Spend(#01) && Spend(#01) != Close
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
assert!(
report.blockers.is_empty(),
"unexpected blockers: {:?}",
report.blockers
);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin equalsByteString"));
assert!(rendered.contains("aeri_eq_left"));
assert!(rendered.contains("aeri_eq_right"));
assert!(!rendered.contains("equalsAeriValue"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin equalsByteString"));
assert!(artifact.text.contains("builtin unBData"));
}
#[test]
fn lowers_nested_direct_constructor_comparisons_to_uplc_core() {
let source = r#"module demo;
type Token {
Native(policy: ByteArray),
Other
}
type Action {
Spend(token: Token),
Close
}
validator gate(redeemer: ByteArray, ctx: Tx) {
Spend(Native(redeemer)) == Spend(Native(#01)) &&
[Spend(Native(redeemer)), Spend(Other), Close] != [Spend(Native(#ff)), Spend(Other), Close] &&
list_has([Spend(Native(redeemer)), Spend(Other), Close], Spend(Native(redeemer))) &&
!list_has([], Spend(Native(redeemer)))
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
assert!(
report.blockers.is_empty(),
"unexpected blockers: {:?}",
report.blockers
);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin equalsByteString"));
assert!(rendered.contains("aeri_eq_left"));
assert!(rendered.contains("aeri_eq_right"));
assert!(rendered.contains("aeri_list_constructor_field"));
assert!(rendered.contains("aeri_list_has_constructor_field"));
assert!(rendered.contains("aeri_list_has_needle_field"));
assert!(rendered.contains("aeri_empty_list_has_needle_field"));
assert!(!rendered.contains("equalsAeriValue"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin equalsByteString"));
assert!(artifact.text.contains("builtin unBData"));
}
#[test]
fn nested_direct_constructor_comparisons_bind_leaf_fields_before_results() {
let source = r#"module demo;
type Token {
Native(policy: ByteArray),
Other
}
type Action {
Spend(token: Token),
Close
}
validator equality_gate(redeemer: ByteArray, ctx: Tx) {
Spend(Native(fail)) != Close
}
validator list_equality_gate(redeemer: ByteArray, ctx: Tx) {
[Spend(Native(fail))] != []
}
validator membership_gate(redeemer: ByteArray, ctx: Tx) {
!list_has([Close], Spend(Native(fail)))
}
validator empty_membership_gate(redeemer: ByteArray, ctx: Tx) {
!list_has([], Spend(Native(fail)))
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let expected_binders = [
("demo.equality_gate", "aeri_eq_left"),
("demo.list_equality_gate", "aeri_list_constructor_field"),
("demo.membership_gate", "aeri_list_has_needle_field"),
(
"demo.empty_membership_gate",
"aeri_empty_list_has_needle_field",
),
];
for (title, binder) in expected_binders {
let validator = output
.lowered_validators
.iter()
.find(|validator| validator.title == title)
.expect("validator exists");
let report = lower_program(&validator.program);
let program = report.program.expect("nested comparison lowers");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors for {title}: {:?}",
validation.errors
);
assert!(rendered.contains(binder));
assert!(rendered.contains("(error)"));
assert!(rendered.contains("(con bool True)"));
assert!(!rendered.contains("equalsAeriValue"));
}
}
#[test]
fn direct_constructor_equality_binds_fields_before_tag_mismatch() {
let source = r#"module demo;
type Action {
Spend(owner: ByteArray),
Close
}
validator gate(redeemer: ByteArray, ctx: Tx) {
Spend(fail) != Close
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("aeri_eq_left"));
assert!(rendered.contains("(error)"));
assert!(rendered.contains("(con bool True)"));
assert!(!rendered.contains("equalsAeriValue"));
}
#[test]
fn direct_constructor_equality_binds_right_fields_before_tag_mismatch() {
let source = r#"module demo;
type Action {
Spend(owner: ByteArray),
Close
}
validator gate(redeemer: ByteArray, ctx: Tx) {
Close != Spend(fail)
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("aeri_eq_right"));
assert!(rendered.contains("(error)"));
assert!(rendered.contains("(con bool True)"));
assert!(!rendered.contains("equalsAeriValue"));
}
#[test]
fn lowers_direct_constructor_field_equality_for_supported_field_kinds() {
let source = r#"module demo;
type Mode {
Hot,
Cold
}
type Envelope {
Wrap(ok: Bool, amount: Int, owner: ByteArray, label: String, payload: Data, marker: Unit, mode: Mode),
Other
}
validator gate(payload: Data, redeemer: ByteArray, ctx: Tx) {
Wrap(true, 7, redeemer, "ok", payload, (), Hot) == Wrap(true, 7, #01, "ok", payload, (), Hot)
&& Wrap(true, 7, #02, "ok", payload, (), Hot) != Wrap(true, 7, #02, "ok", payload, (), Cold)
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
assert!(
report.blockers.is_empty(),
"unexpected blockers: {:?}",
report.blockers
);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin ifThenElse"));
assert!(rendered.contains("builtin equalsInteger"));
assert!(rendered.contains("builtin equalsByteString"));
assert!(rendered.contains("builtin equalsString"));
assert!(rendered.contains("builtin equalsData"));
assert!(rendered.contains("(con unit ())"));
assert!(!rendered.contains("equalsAeriValue"));
assert!(!rendered.contains("equalsCustomTag"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin equalsInteger"));
assert!(artifact.text.contains("builtin equalsByteString"));
assert!(artifact.text.contains("builtin equalsString"));
assert!(artifact.text.contains("builtin equalsData"));
assert!(artifact.text.contains("builtin unBData"));
}
#[test]
fn reports_runtime_constructor_field_equality_as_core_blocker() {
let source = r#"module demo;
type Action {
Spend(owner: ByteArray),
Close
}
validator gate(redeemer: ByteArray, ctx: Tx) {
let action = Spend(#01);
action == action
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
assert!(!report.lowered());
assert!(report.program.is_none());
assert!(report.blockers.iter().any(|blocker| {
blocker.contains("custom constructors with fields need Plutus Data encoding lowering")
|| blocker.contains("structural equality")
}));
}
#[test]
fn reports_unsupported_nested_constructor_comparisons_as_core_blockers() {
let source = r#"module demo;
type Token {
Native(policy: ByteArray),
Other
}
type Action {
Spend(token: Token),
Close
}
type Batch {
Bundle(ids: List<ByteArray>),
Empty
}
validator stored_gate(token: Token, ctx: Tx) {
Spend(token) == Spend(Native(#01))
}
validator stored_list_gate(token: Token, ctx: Tx) {
[Spend(token)] == [Spend(Native(#01))]
}
validator list_field_gate(redeemer: ByteArray, ctx: Tx) {
Bundle([redeemer]) == Bundle([#01])
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
assert!(output.script.contains("equalsAeriValue"));
for validator in &output.lowered_validators {
let report = lower_program(&validator.program);
assert!(
!report.lowered(),
"{} unexpectedly lowered",
validator.title
);
assert!(report.program.is_none());
assert!(report.blockers.iter().any(|blocker| {
blocker.contains("list literals need Plutus list encoding")
|| blocker.contains("custom constructors with fields")
|| blocker.contains("structural equality")
}));
}
}
#[test]
fn lowers_direct_constructor_fields_to_uplc_core() {
let source = r#"module demo;
type Action {
Spend(owner: ByteArray),
Close
}
validator gate(redeemer: ByteArray, ctx: Tx) {
match Spend(#01) {
Spend(owner) => owner == redeemer,
Close => false
}
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
assert!(
report.blockers.is_empty(),
"unexpected blockers: {:?}",
report.blockers
);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin equalsByteString"));
assert!(rendered.contains("(apply (lam owner"));
assert!(rendered.contains("(con bytes #01)"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin equalsByteString"));
}
#[test]
fn lowers_direct_nullary_constructor_with_fieldful_alternative_to_uplc_core() {
let source = r#"module demo;
type Action {
Spend(owner: ByteArray),
Close
}
validator gate(redeemer: ByteArray, ctx: Tx) {
match Close {
Spend(owner) => owner == redeemer,
Close => true
}
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
assert!(
report.blockers.is_empty(),
"unexpected blockers: {:?}",
report.blockers
);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(!rendered.contains("owner"));
assert!(!rendered.contains("equalsByteString"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("(delay (con unit ()))"));
}
#[test]
fn reports_variable_subject_constructor_fields_as_core_blocker() {
let source = r#"module demo;
type Action {
Spend(owner: ByteArray),
Close
}
validator gate(action: Action, redeemer: ByteArray, ctx: Tx) {
match action {
Spend(owner) => owner == redeemer,
Close => false
}
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
assert!(!report.lowered());
assert!(report.program.is_none());
assert!(
report.blockers.iter().any(|blocker| {
blocker.contains("constructor match pattern 'Spend' with fields")
})
);
}
#[test]
fn reports_public_constructor_match_without_catch_all_as_core_blocker() {
let program = AeriProgram::new(AeriTerm::Match {
subject: Box::new(AeriTerm::Constr {
name: "Open".to_string(),
tag: 0,
fields: Vec::new(),
}),
arms: vec![AeriMatchArm {
pattern: AeriPattern::Constructor {
name: "Open".to_string(),
tag: 0,
bindings: Vec::new(),
},
body: AeriTerm::Bool(true),
}],
});
let report = lower_program(&program);
assert!(!report.lowered());
assert!(report.program.is_none());
assert!(
report
.blockers
.iter()
.any(|blocker| blocker.contains("catch-all arm"))
);
}
#[test]
fn reports_unrepresentable_constructor_tags_as_core_blocker() {
let program = AeriProgram::new(AeriTerm::Match {
subject: Box::new(AeriTerm::Constr {
name: "Huge".to_string(),
tag: usize::MAX,
fields: Vec::new(),
}),
arms: vec![
AeriMatchArm {
pattern: AeriPattern::Constructor {
name: "Huge".to_string(),
tag: usize::MAX,
bindings: Vec::new(),
},
body: AeriTerm::Bool(true),
},
AeriMatchArm {
pattern: AeriPattern::Wildcard,
body: AeriTerm::Bool(false),
},
],
});
let report = lower_program(&program);
assert!(!report.lowered());
assert!(report.program.is_none());
assert!(
report
.blockers
.iter()
.any(|blocker| { blocker.contains("cannot be represented as a Plutus integer") })
);
}
#[test]
fn emits_cbor_script_for_list_literal_helper_subset() {
let source = r#"module demo;
const ADMINS: List<ByteArray> = [#01, #02];
validator gate(redeemer: ByteArray, ctx: Tx) {
list_has_bytes(ADMINS, redeemer)
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin equalsByteString"));
assert_eq!(artifact.script_json.script_type, "PlutusScriptV2");
assert!(!artifact.flat_hex.is_empty());
assert!(!artifact.cbor_hex.is_empty());
}
#[test]
fn lowers_runtime_list_helper_for_ledger_list_params() {
let program = AeriProgram::new(AeriTerm::Lambda {
param: "items".to_string(),
body: Box::new(AeriTerm::BuiltinCall {
name: "list_has_bytes".to_string(),
args: vec![
AeriTerm::Var("items".to_string()),
AeriTerm::ByteArray("01".to_string()),
],
}),
});
let report = lower_program(&program);
let program = report.program.expect("runtime list helper lowers");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin nullList"));
assert!(rendered.contains("builtin headList"));
assert!(rendered.contains("builtin tailList"));
assert!(rendered.contains("builtin unBData"));
assert!(rendered.contains("builtin equalsByteString"));
}
#[test]
fn emits_primitive_list_plutus_data_decoder_for_ledger_param() {
let source = r#"module demo;
validator gate(items: List<Int>, wanted: Int, ctx: Tx) {
require list_has(items, wanted);
list_len(items) >= 1
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
assert!(output.script.contains("list_has_int"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("ledger program emits");
assert!(artifact.text.contains("builtin unListData"));
assert!(artifact.text.contains("builtin unIData"));
assert!(artifact.text.contains("builtin nullList"));
assert!(artifact.text.contains("builtin headList"));
assert!(artifact.text.contains("builtin tailList"));
assert!(artifact.text.contains("builtin addInteger"));
assert!(artifact.text.contains("builtin equalsInteger"));
assert_eq!(artifact.script_json.script_type, "PlutusScriptV2");
}
#[test]
fn emits_remaining_primitive_list_plutus_data_decoders_for_ledger_params() {
let source = r#"module demo;
validator bool_gate(items: List<Bool>, wanted: Bool, ctx: Tx) {
list_has(items, wanted) && list_len(items) >= 0
}
validator bytes_gate(items: List<ByteArray>, wanted: ByteArray, ctx: Tx) {
list_has(items, wanted) && list_len(items) >= 0
}
validator string_gate(items: List<String>, wanted: String, ctx: Tx) {
list_has(items, wanted) && list_len(items) >= 0
}
validator data_gate(items: List<Data>, wanted: Data, ctx: Tx) {
list_has(items, wanted) && list_len(items) >= 0
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
assert_eq!(output.validator_count, 4);
assert!(output.script.contains("list_has_bool"));
assert!(output.script.contains("list_has_bytes"));
assert!(output.script.contains("list_has_string"));
assert!(output.script.contains("list_has_data"));
let emit_text = |title: &str| {
let validator = output
.lowered_validators
.iter()
.find(|validator| validator.title == title)
.expect("validator exists");
let artifact = emit_ledger_program(&validator.program, &validator.param_types)
.expect("ledger program emits");
assert_eq!(artifact.script_json.script_type, "PlutusScriptV2");
artifact.text
};
let bool_text = emit_text("demo.bool_gate");
assert!(bool_text.contains("builtin unListData"));
assert!(bool_text.contains("builtin unConstrData"));
assert!(bool_text.contains("builtin nullList"));
assert!(bool_text.contains("builtin addInteger"));
let bytes_text = emit_text("demo.bytes_gate");
assert!(bytes_text.contains("builtin unListData"));
assert!(bytes_text.contains("builtin unBData"));
assert!(bytes_text.contains("builtin equalsByteString"));
assert!(bytes_text.contains("builtin addInteger"));
let string_text = emit_text("demo.string_gate");
assert!(string_text.contains("builtin unListData"));
assert!(string_text.contains("builtin unBData"));
assert!(string_text.contains("builtin decodeUtf8"));
assert!(string_text.contains("builtin equalsString"));
assert!(string_text.contains("builtin addInteger"));
let data_text = emit_text("demo.data_gate");
assert!(data_text.contains("builtin unListData"));
assert!(data_text.contains("builtin equalsData"));
assert!(data_text.contains("builtin addInteger"));
}
#[test]
fn lowers_data_equality_to_uplc_core() {
let source = "module demo;\n\nvalidator gate(datum: Data, redeemer: Data, ctx: Tx) { datum == redeemer && datum != redeemer }\n";
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin equalsData"));
assert!(rendered.contains("builtin ifThenElse"));
}
#[test]
fn conditional_lowering_delays_branches_before_force() {
let source = "module demo;\n\nvalidator gate(redeemer: ByteArray, ctx: Tx) { require redeemer == #01; true }\n";
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("(force (apply (apply (apply (force (builtin ifThenElse))"));
assert!(rendered.contains("(delay (con bool True))"));
assert!(rendered.contains("(delay (error))"));
}
#[test]
fn short_circuit_lowering_delays_unselected_error_branch() {
let source =
"module demo;\n\nvalidator gate(redeemer: ByteArray, ctx: Tx) { false && fail }\n";
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("(delay (error))"));
assert!(rendered.contains("(delay (con bool False))"));
}
#[test]
fn reports_custom_ledger_parameter_as_emit_blocker_after_tag_equality_lowering() {
let source = "module demo;\n\ntype Action { Spend }\n\nvalidator gate(action: Action, ctx: Tx) { action == action }\n";
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
let error = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect_err("custom ledger parameter blocks emission");
assert!(error.blockers.iter().any(|blocker| {
blocker
.contains("validator parameter 'action: Action' needs Plutus Data decoder lowering")
}));
}
#[test]
fn emits_custom_plutus_data_decoder_for_ledger_param_with_layout() {
let source = r#"module demo;
type Action {
Open,
Close
}
validator gate(action: Action, ctx: Tx) {
match action {
Open => true,
Close => false
}
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let validator = &output.lowered_validators[0];
let ledger = emit_ledger_program_with_layouts(
&validator.program,
&validator.param_types,
&validator.custom_types,
)
.expect("custom ledger parameter emits with layout");
assert!(ledger.text.contains("builtin unConstrData"));
assert!(ledger.text.contains("builtin fstPair"));
assert!(ledger.text.contains("builtin sndPair"));
assert!(ledger.text.contains("builtin nullList"));
assert!(ledger.text.contains("builtin equalsInteger"));
assert!(ledger.text.contains("(con integer 0)"));
assert!(ledger.text.contains("(con integer 1)"));
assert!(ledger.text.contains("(delay (con unit ()))"));
assert_eq!(ledger.script_json.script_type, "PlutusScriptV2");
assert_eq!(ledger.script_hash.len(), 56);
}
#[test]
fn custom_plutus_data_decoder_validates_field_counts_for_tag_only_matches() {
let source = r#"module demo;
type Action {
Spend(owner: ByteArray),
Close
}
validator gate(action: Action, ctx: Tx) {
match action {
Close => true,
_ => false
}
}
"#;
let output = compile_source("demo.aeri", source).expect("source compiles");
let validator = &output.lowered_validators[0];
let ledger = emit_ledger_program_with_layouts(
&validator.program,
&validator.param_types,
&validator.custom_types,
)
.expect("tag-only custom ledger parameter emits with layout");
assert!(ledger.text.contains("builtin tailList"));
assert!(ledger.text.contains("builtin nullList"));
assert!(ledger.text.contains("(con integer 0)"));
assert!(ledger.text.contains("(con integer 1)"));
assert_eq!(ledger.script_hash.len(), 56);
}
#[test]
fn lowers_unary_not_and_negation_to_uplc_core() {
let source = "module demo;\n\nvalidator gate(datum: Data, redeemer: ByteArray, ctx: Tx) { let rebate = -1; require !(rebate > 0); redeemer == #01 }\n";
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin subtractInteger"));
assert!(rendered.contains("builtin ifThenElse"));
assert!(rendered.contains("builtin lessThanInteger"));
}
#[test]
fn lowers_trace_statement_to_uplc_core() {
let source = "module demo;\n\nvalidator gate(redeemer: ByteArray, ctx: Tx) { trace \"checking gate\"; redeemer == #01 }\n";
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin trace"));
assert!(rendered.contains("(force (builtin trace))"));
assert!(rendered.contains("(con string \"checking gate\")"));
assert!(
validation
.builtin_applications
.iter()
.any(|application| application.name == "trace" && application.applied_args == 2)
);
}
#[test]
fn lowers_byte_array_match_with_binding_catch_all_to_uplc_core() {
let source = "module demo;\n\nvalidator gate(redeemer: ByteArray, ctx: Tx) { match redeemer { #01 => true, other => other == #02 } }\n";
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("(lam aeri_match_subject"));
assert!(rendered.contains("builtin equalsByteString"));
assert!(rendered.contains("(lam other"));
}
#[test]
fn lowers_exhaustive_bool_match_without_catch_all_to_uplc_core() {
let source = "module demo;\n\nvalidator gate(flag: Bool, ctx: Tx) { match flag { true => true, false => false } }\n";
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("(lam aeri_match_subject"));
assert!(rendered.contains("builtin ifThenElse"));
}
#[test]
fn reports_public_primitive_match_without_catch_all_as_core_blocker() {
let program = AeriProgram::new(AeriTerm::Match {
subject: Box::new(AeriTerm::Int(1)),
arms: vec![AeriMatchArm {
pattern: AeriPattern::Int(1),
body: AeriTerm::Bool(true),
}],
});
let report = lower_program(&program);
assert!(!report.lowered());
assert!(report.program.is_none());
assert!(
report
.blockers
.iter()
.any(|blocker| blocker.contains("catch-all arm"))
);
}
#[test]
fn reports_public_match_with_non_final_catch_all_as_core_blocker() {
let program = AeriProgram::new(AeriTerm::Match {
subject: Box::new(AeriTerm::ByteArray("01".to_string())),
arms: vec![
AeriMatchArm {
pattern: AeriPattern::Wildcard,
body: AeriTerm::Bool(false),
},
AeriMatchArm {
pattern: AeriPattern::ByteArray("01".to_string()),
body: AeriTerm::Bool(true),
},
],
});
let report = lower_program(&program);
assert!(!report.lowered());
assert!(report.program.is_none());
assert!(
report
.blockers
.iter()
.any(|blocker| blocker.contains("must be last"))
);
}
#[test]
fn reports_public_match_with_duplicate_primitive_patterns_as_core_blocker() {
let program = AeriProgram::new(AeriTerm::Match {
subject: Box::new(AeriTerm::Int(1)),
arms: vec![
AeriMatchArm {
pattern: AeriPattern::Int(1),
body: AeriTerm::Bool(true),
},
AeriMatchArm {
pattern: AeriPattern::Int(1),
body: AeriTerm::Bool(false),
},
AeriMatchArm {
pattern: AeriPattern::Wildcard,
body: AeriTerm::Bool(false),
},
],
});
let report = lower_program(&program);
assert!(!report.lowered());
assert!(report.program.is_none());
assert!(
report
.blockers
.iter()
.any(|blocker| blocker.contains("duplicate primitive match pattern '1'"))
);
}
#[test]
fn sequence_lowering_does_not_capture_user_underscore_binding() {
let source = "module demo;\n\nvalidator gate(redeemer: ByteArray, ctx: Tx) { let _ = redeemer; true; _ == #01 }\n";
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("(lam aeri_seq_discard "));
assert!(rendered.contains("(apply (apply (builtin equalsByteString) _)"));
}
#[test]
fn sequence_lowering_freshens_discard_binder_for_public_ir() {
let program = AeriProgram::new(AeriTerm::Lambda {
param: SEQUENCE_DISCARD_BINDER.to_string(),
body: Box::new(AeriTerm::Sequence {
first: Box::new(AeriTerm::Unit),
then_term: Box::new(AeriTerm::Var(SEQUENCE_DISCARD_BINDER.to_string())),
}),
});
let report = lower_program(&program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("(lam aeri_seq_discard "));
assert!(rendered.contains("(lam aeri_seq_discard_1 aeri_seq_discard)"));
}
#[test]
fn validation_reports_unbound_variables() {
let program = Program {
version: (1, 0, 0),
term: Term::Var("missing".to_string()),
};
let validation = validate_program(&program);
assert!(!validation.valid);
assert!(
validation
.errors
.iter()
.any(|error| error.contains("unbound variable 'missing'"))
);
}
#[test]
fn validation_reports_unsupported_program_versions() {
let program = Program {
version: (9, 9, 9),
term: Term::Constant(Constant::Bool(true)),
};
let validation = validate_program(&program);
assert!(!validation.valid);
assert!(
validation
.errors
.iter()
.any(|error| error.contains("unsupported UPLC version 9.9.9"))
);
}
#[test]
fn validation_reports_wrong_builtin_arity() {
let program = Program {
version: (1, 0, 0),
term: Term::Builtin("equalsData".to_string()),
};
let validation = validate_program(&program);
assert!(!validation.valid);
assert!(
validation
.errors
.iter()
.any(|error| error.contains("expects 2 argument(s), got 0"))
);
}
#[test]
fn validation_reports_undelayed_if_then_else_branches() {
let program = Program {
version: (1, 0, 0),
term: apply_builtin(
"ifThenElse",
vec![
Term::Constant(Constant::Bool(true)),
Term::Constant(Constant::Bool(true)),
Term::Error,
],
),
};
let validation = validate_program(&program);
assert!(!validation.valid);
assert!(
validation
.errors
.iter()
.any(|error| error.contains("ifThenElse then branch must be delayed"))
);
assert!(
validation
.errors
.iter()
.any(|error| error.contains("ifThenElse else branch must be delayed"))
);
}
#[test]
fn validation_reports_unforced_if_then_else() {
let program = Program {
version: (1, 0, 0),
term: apply_builtin(
"ifThenElse",
vec![
Term::Constant(Constant::Bool(true)),
Term::Delay(Box::new(Term::Constant(Constant::Bool(true)))),
Term::Delay(Box::new(Term::Error)),
],
),
};
let validation = validate_program(&program);
assert!(!validation.valid);
assert!(validation.errors.iter().any(|error| {
error.contains("builtin 'ifThenElse' requires 1 force(s) before arguments, got 0")
}));
assert!(validation.errors.iter().any(|error| error.contains(
"fully applied ifThenElse result must be forced to select a delayed branch"
)));
}
#[test]
fn validation_reports_unforced_trace() {
let program = Program {
version: (1, 0, 0),
term: apply_builtin(
"trace",
vec![
Term::Constant(Constant::String("checking".to_string())),
Term::Constant(Constant::Bool(true)),
],
),
};
let validation = validate_program(&program);
assert!(!validation.valid);
assert!(validation.errors.iter().any(|error| {
error.contains("builtin 'trace' requires 1 force(s) before arguments, got 0")
}));
}
#[test]
fn validation_reports_invalid_identifiers_and_byte_literals() {
let program = Program {
version: (1, 0, 0),
term: Term::Lambda {
param: "bad-name".to_string(),
body: Box::new(Term::Apply {
function: Box::new(Term::Apply {
function: Box::new(Term::Builtin("equalsByteString".to_string())),
argument: Box::new(Term::Var("bad-name".to_string())),
}),
argument: Box::new(Term::Constant(Constant::ByteArray("0g".to_string()))),
}),
},
};
let validation = validate_program(&program);
assert!(!validation.valid);
assert!(
validation
.errors
.iter()
.any(|error| error.contains("lambda parameter 'bad-name'"))
);
assert!(
validation
.errors
.iter()
.any(|error| error.contains("variable 'bad-name'"))
);
assert!(
validation
.errors
.iter()
.any(|error| error.contains("byte string constant '#0g'"))
);
}
#[test]
fn lowers_tx_signed_by_to_script_context_signatory_lookup() {
let source = "module demo;\n\nvalidator gate(signer: ByteArray, ctx: Tx) { tx_signed_by(ctx, signer) }\n";
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin unConstrData"));
assert!(rendered.contains("builtin unListData"));
assert!(rendered.contains("builtin headList"));
assert!(rendered.contains("builtin tailList"));
assert!(rendered.contains("builtin nullList"));
assert!(rendered.contains("builtin unBData"));
assert!(rendered.contains("builtin equalsByteString"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("tx_signed_by ledger program emits");
assert_eq!(artifact.script_json.script_type, "PlutusScriptV2");
assert_eq!(artifact.script_hash.len(), 56);
}
#[test]
fn lowers_tx_has_datum_to_script_context_datum_lookup() {
let source =
"module demo;\n\nvalidator gate(datum: Data, ctx: Tx) { tx_has_datum(ctx, datum) }\n";
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = lower_program(&output.lowered_validators[0].program);
let program = report.program.expect("uplc core program");
let validation = validate_program(&program);
let rendered = program.render();
assert!(
validation.valid,
"unexpected errors: {:?}",
validation.errors
);
assert!(rendered.contains("builtin unConstrData"));
assert!(rendered.contains("builtin unMapData"));
assert!(rendered.contains("builtin headList"));
assert!(rendered.contains("builtin tailList"));
assert!(rendered.contains("builtin nullList"));
assert!(rendered.contains("builtin sndPair"));
assert!(rendered.contains("builtin equalsData"));
let artifact = emit_ledger_program(
&output.lowered_validators[0].program,
&output.lowered_validators[0].param_types,
)
.expect("tx_has_datum ledger program emits");
assert_eq!(artifact.script_json.script_type, "PlutusScriptV2");
assert_eq!(artifact.script_hash.len(), 56);
}
#[test]
fn reports_unsupported_transaction_builtin_as_lowering_blocker() {
let program = AeriProgram::new(AeriTerm::BuiltinCall {
name: "tx_paid_to".to_string(),
args: vec![
AeriTerm::Var("ctx".to_string()),
AeriTerm::Var("address".to_string()),
AeriTerm::Int(1),
],
});
let report = lower_program(&program);
assert!(!report.lowered());
assert!(report.program.is_none());
assert!(
report
.blockers
.iter()
.any(|blocker| blocker.contains("tx_paid_to"))
);
assert!(
report
.blockers
.iter()
.any(|blocker| blocker.contains("Plutus V2 outputs, addresses, and values"))
);
let program = AeriProgram::new(AeriTerm::BuiltinCall {
name: "tx_spends".to_string(),
args: vec![
AeriTerm::Var("ctx".to_string()),
AeriTerm::Var("output_ref".to_string()),
],
});
let report = lower_program(&program);
assert!(!report.lowered());
assert!(report.program.is_none());
assert!(report.blockers.iter().any(|blocker| {
blocker.contains("full TxOutRef ABI") && blocker.contains("output index")
}));
}
#[test]
fn reports_supported_aeri_builtins_for_core_inspection() {
assert!(supports_aeri_builtin("sha2_256"));
assert!(supports_aeri_builtin("equalsByteString"));
assert!(supports_aeri_builtin("notEqualsInteger"));
assert!(supports_aeri_builtin("greaterThanInteger"));
assert!(supports_aeri_builtin("append_bytes"));
assert!(supports_aeri_builtin("not"));
assert!(supports_aeri_builtin("negateInteger"));
assert!(supports_aeri_builtin("list_has_bytes"));
assert!(supports_aeri_builtin("list_has_bool"));
assert!(supports_aeri_builtin("list_has_string"));
assert!(supports_aeri_builtin("list_has_data"));
assert!(supports_aeri_builtin("list_has_unit"));
assert!(supports_aeri_builtin("list_has_custom_tag"));
assert!(supports_aeri_builtin("list_len_int"));
assert!(supports_aeri_builtin("list_len_bool"));
assert!(supports_aeri_builtin("list_len_string"));
assert!(supports_aeri_builtin("list_len_data"));
assert!(supports_aeri_builtin("list_len_unit"));
assert!(supports_aeri_builtin("list_len"));
assert!(supports_aeri_builtin("equalsListInteger"));
assert!(supports_aeri_builtin("notEqualsListByteString"));
assert!(supports_aeri_builtin("equalsListUnit"));
assert!(supports_aeri_builtin("notEqualsListUnit"));
assert!(supports_aeri_builtin("equalsListCustomTag"));
assert!(supports_aeri_builtin("notEqualsListCustomTag"));
assert!(supports_aeri_builtin("equalsCustomTag"));
assert!(supports_aeri_builtin("notEqualsCustomTag"));
assert!(!supports_aeri_builtin("tx_signed_by"));
}
}