use std::collections::BTreeMap;
use serde::Serialize;
use crate::{
ast::{Item, Module, Param, TypeRef},
backend::{Target, render_preview_program},
compile::CompileOutput,
ir::{Program, Term},
types::{Type, is_transaction_builtin},
uplc::{
CustomTypeLayout, ValidationReport, emit_ledger_program_with_layouts, lower_program,
supports_aeri_builtin, validate_program,
},
};
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct InspectReport {
pub file: String,
pub module: String,
pub target: TargetInspection,
pub summary: ModuleSummary,
pub items: Vec<ItemInspection>,
pub validators: Vec<ValidatorInspection>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ProjectInspectReport {
pub root: String,
pub target: TargetInspection,
pub summary: ProjectInspectionSummary,
pub files: Vec<InspectReport>,
}
impl ProjectInspectReport {
pub fn has_blockers(&self) -> bool {
!self.target.blockers.is_empty() || self.files.iter().any(InspectReport::has_blockers)
}
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ProjectInspectionSummary {
pub files: usize,
pub types: usize,
pub constants: usize,
pub functions: usize,
pub validators: usize,
pub tests: usize,
pub blocked_validators: usize,
}
impl InspectReport {
pub fn has_blockers(&self) -> bool {
!self.target.blockers.is_empty()
|| self
.validators
.iter()
.any(|validator| !validator.readiness.ready)
}
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct TargetInspection {
pub requested: String,
pub implemented: bool,
pub compiler_target: Option<String>,
pub cardano_deployable: bool,
pub summary: String,
pub blockers: Vec<String>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ModuleSummary {
pub types: usize,
pub constants: usize,
pub functions: usize,
pub validators: usize,
pub tests: usize,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ItemInspection {
pub kind: String,
pub name: String,
pub details: Vec<String>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ValidatorInspection {
pub title: String,
pub script_purpose: String,
pub parameters: Vec<ParameterInspection>,
pub ir: IrInspection,
pub readiness: ReadinessInspection,
#[serde(skip_serializing_if = "Option::is_none")]
pub preview_ir: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uplc_core: Option<UplcCoreInspection>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ParameterInspection {
pub name: String,
pub ty: String,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct IrInspection {
pub nodes: usize,
pub lambdas: usize,
pub lets: usize,
pub branches: usize,
pub matches: usize,
pub match_arms: usize,
pub constructors: usize,
pub lists: usize,
pub traces: usize,
pub errors: usize,
pub builtins: Vec<BuiltinInspection>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct BuiltinInspection {
pub name: String,
pub count: usize,
pub class: String,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ReadinessInspection {
pub target: String,
pub ready: bool,
pub notes: Vec<String>,
pub blockers: Vec<String>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct UplcCoreInspection {
pub lowered: bool,
pub validated: bool,
pub summary: String,
pub blockers: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub validation: Option<ValidationReport>,
#[serde(skip_serializing_if = "Option::is_none")]
pub program: Option<String>,
}
#[derive(Debug, Default)]
struct IrStats {
nodes: usize,
lambdas: usize,
lets: usize,
branches: usize,
matches: usize,
match_arms: usize,
constructors: usize,
lists: usize,
traces: usize,
errors: usize,
builtins: BTreeMap<String, usize>,
}
pub fn inspect_output(
file: impl Into<String>,
output: &CompileOutput,
target: Target,
include_ir: bool,
) -> InspectReport {
let file = file.into();
let module = &output.module;
let validators = output
.lowered_validators
.iter()
.map(|validator| {
let ir = inspect_program(&validator.program);
let uplc_core =
(target == Target::Uplc).then(|| inspect_uplc_core(&validator.program, include_ir));
ValidatorInspection {
title: validator.title.clone(),
script_purpose: validator.script_purpose.clone(),
parameters: validator
.param_types
.iter()
.map(|(name, ty)| ParameterInspection {
name: name.clone(),
ty: ty.to_string(),
})
.collect(),
readiness: inspect_readiness(
target,
&validator.program,
&validator.param_types,
&validator.custom_types,
uplc_core.as_ref(),
),
preview_ir: include_ir.then(|| render_preview_program(&validator.program)),
uplc_core,
ir,
}
})
.collect();
InspectReport {
file,
module: module.name.clone(),
target: inspect_target(target),
summary: summarize_module(module),
items: inspect_items(module),
validators,
}
}
pub fn inspect_project_outputs(
root: impl Into<String>,
files: &[String],
outputs: &[CompileOutput],
target: Target,
include_ir: bool,
) -> ProjectInspectReport {
let reports = files
.iter()
.zip(outputs)
.map(|(file, output)| inspect_output(file.clone(), output, target, include_ir))
.collect::<Vec<_>>();
let summary = summarize_project(&reports);
ProjectInspectReport {
root: root.into(),
target: inspect_target(target),
summary,
files: reports,
}
}
pub fn render_terminal_report(report: &InspectReport) -> String {
let mut out = String::new();
push_header(&mut out, "AERI INSPECTOR");
push_line(&mut out, format!("file: {}", report.file));
push_line(&mut out, format!("module: {}", report.module));
push_line(
&mut out,
format!(
"target: {} | implemented: {} | cardano deployable: {}",
report.target.requested, report.target.implemented, report.target.cardano_deployable
),
);
if let Some(compiler_target) = &report.target.compiler_target {
push_line(&mut out, format!("compiler target: {compiler_target}"));
}
push_line(&mut out, format!("status: {}", report.target.summary));
for blocker in &report.target.blockers {
push_line(&mut out, format!("blocker: {blocker}"));
}
push_section(&mut out, "MODULE");
push_line(
&mut out,
format!(
"types: {} | consts: {} | functions: {} | validators: {} | tests: {}",
report.summary.types,
report.summary.constants,
report.summary.functions,
report.summary.validators,
report.summary.tests
),
);
for item in &report.items {
push_line(&mut out, format!("{} {}", item.kind, item.name));
for detail in &item.details {
push_line(&mut out, format!(" - {detail}"));
}
}
push_section(&mut out, "VALIDATORS");
for validator in &report.validators {
push_line(
&mut out,
format!(
"{} [{}] | ready for {}: {}",
validator.title,
validator.script_purpose,
validator.readiness.target,
validator.readiness.ready
),
);
let params = validator
.parameters
.iter()
.map(|param| format!("{}: {}", param.name, param.ty))
.collect::<Vec<_>>()
.join(", ");
push_line(&mut out, format!(" params: {params}"));
push_line(
&mut out,
format!(
" ir: nodes={} lambdas={} lets={} branches={} matches={} constructors={} lists={} traces={} errors={}",
validator.ir.nodes,
validator.ir.lambdas,
validator.ir.lets,
validator.ir.branches,
validator.ir.matches,
validator.ir.constructors,
validator.ir.lists,
validator.ir.traces,
validator.ir.errors
),
);
if validator.ir.builtins.is_empty() {
push_line(&mut out, " builtins: none");
} else {
let builtins = validator
.ir
.builtins
.iter()
.map(|builtin| format!("{} x{} ({})", builtin.name, builtin.count, builtin.class))
.collect::<Vec<_>>()
.join(", ");
push_line(&mut out, format!(" builtins: {builtins}"));
}
for note in &validator.readiness.notes {
push_line(&mut out, format!(" note: {note}"));
}
for blocker in &validator.readiness.blockers {
push_line(&mut out, format!(" blocker: {blocker}"));
}
if let Some(uplc_core) = &validator.uplc_core {
push_line(
&mut out,
format!(
" uplc core: lowered={} validated={} | {}",
uplc_core.lowered, uplc_core.validated, uplc_core.summary
),
);
if let Some(validation) = &uplc_core.validation {
push_line(
&mut out,
format!(
" uplc validation: nodes={} maxDepth={} builtinApplications={}",
validation.nodes,
validation.max_depth,
validation.builtin_applications.len()
),
);
push_line(
&mut out,
format!(
" uplc builtins: {}",
format_uplc_builtin_applications(validation)
),
);
}
for blocker in &uplc_core.blockers {
push_line(&mut out, format!(" uplc blocker: {blocker}"));
}
if let Some(program) = &uplc_core.program {
push_line(&mut out, " uplc core program:");
push_line(&mut out, format!(" {program}"));
}
}
if let Some(preview_ir) = &validator.preview_ir {
push_line(&mut out, " preview ir:");
push_line(&mut out, format!(" {preview_ir}"));
}
}
push_footer(&mut out);
out
}
pub fn render_project_terminal_report(report: &ProjectInspectReport) -> String {
let mut out = String::new();
push_header(&mut out, "AERI PROJECT INSPECTOR");
push_line(&mut out, format!("root: {}", report.root));
push_line(
&mut out,
format!(
"target: {} | implemented: {} | cardano deployable: {}",
report.target.requested, report.target.implemented, report.target.cardano_deployable
),
);
push_line(&mut out, format!("status: {}", report.target.summary));
for blocker in &report.target.blockers {
push_line(&mut out, format!("blocker: {blocker}"));
}
push_section(&mut out, "PROJECT SUMMARY");
push_line(
&mut out,
format!(
"files: {} | types: {} | consts: {} | functions: {} | validators: {} | tests: {} | blocked validators: {}",
report.summary.files,
report.summary.types,
report.summary.constants,
report.summary.functions,
report.summary.validators,
report.summary.tests,
report.summary.blocked_validators
),
);
for file in &report.files {
push_section(&mut out, &file.file);
push_line(&mut out, format!("module: {}", file.module));
push_line(
&mut out,
format!(
"types: {} | consts: {} | functions: {} | validators: {} | tests: {}",
file.summary.types,
file.summary.constants,
file.summary.functions,
file.summary.validators,
file.summary.tests
),
);
for validator in &file.validators {
push_line(
&mut out,
format!(
"{} [{}] | ready for {}: {}",
validator.title,
validator.script_purpose,
validator.readiness.target,
validator.readiness.ready
),
);
let builtins = if validator.ir.builtins.is_empty() {
"none".to_string()
} else {
validator
.ir
.builtins
.iter()
.map(|builtin| format!("{} x{}", builtin.name, builtin.count))
.collect::<Vec<_>>()
.join(", ")
};
push_line(
&mut out,
format!(" ir: nodes={} builtins={}", validator.ir.nodes, builtins),
);
for blocker in &validator.readiness.blockers {
push_line(&mut out, format!(" blocker: {blocker}"));
}
if let Some(uplc_core) = &validator.uplc_core {
push_line(
&mut out,
format!(
" uplc core: lowered={} validated={} | {}",
uplc_core.lowered, uplc_core.validated, uplc_core.summary
),
);
if let Some(validation) = &uplc_core.validation {
push_line(
&mut out,
format!(
" uplc validation: nodes={} maxDepth={} builtinApplications={}",
validation.nodes,
validation.max_depth,
validation.builtin_applications.len()
),
);
push_line(
&mut out,
format!(
" uplc builtins: {}",
format_uplc_builtin_applications(validation)
),
);
}
if let Some(program) = &uplc_core.program {
push_line(&mut out, " uplc core program:");
push_line(&mut out, format!(" {program}"));
}
}
if let Some(preview_ir) = &validator.preview_ir {
push_line(&mut out, " preview ir:");
push_line(&mut out, format!(" {preview_ir}"));
}
}
}
push_footer(&mut out);
out
}
fn inspect_target(target: Target) -> TargetInspection {
match target {
Target::Preview => TargetInspection {
requested: target.name().to_string(),
implemented: target.implemented(),
compiler_target: target.compiler_target().map(str::to_string),
cardano_deployable: target.cardano_deployable().unwrap_or(false),
summary: "preview textual IR is available for inspection".to_string(),
blockers: Vec::new(),
},
Target::Uplc => TargetInspection {
requested: target.name().to_string(),
implemented: target.implemented(),
compiler_target: target.compiler_target().map(str::to_string),
cardano_deployable: false,
summary: "UPLC/CBOR inspection reports readiness; build/verify mark artifacts deployable only after ledger emission succeeds".to_string(),
blockers: Vec::new(),
},
}
}
fn summarize_module(module: &Module) -> ModuleSummary {
let mut summary = ModuleSummary {
types: 0,
constants: 0,
functions: 0,
validators: 0,
tests: 0,
};
for item in &module.items {
match item {
Item::Type(_) => summary.types += 1,
Item::Const(_) => summary.constants += 1,
Item::Function(_) => summary.functions += 1,
Item::Validator(_) => summary.validators += 1,
Item::Test(_) => summary.tests += 1,
}
}
summary
}
fn summarize_project(reports: &[InspectReport]) -> ProjectInspectionSummary {
ProjectInspectionSummary {
files: reports.len(),
types: reports.iter().map(|report| report.summary.types).sum(),
constants: reports.iter().map(|report| report.summary.constants).sum(),
functions: reports.iter().map(|report| report.summary.functions).sum(),
validators: reports.iter().map(|report| report.summary.validators).sum(),
tests: reports.iter().map(|report| report.summary.tests).sum(),
blocked_validators: reports
.iter()
.flat_map(|report| &report.validators)
.filter(|validator| !validator.readiness.ready)
.count(),
}
}
fn inspect_items(module: &Module) -> Vec<ItemInspection> {
module
.items
.iter()
.map(|item| match item {
Item::Type(type_decl) => ItemInspection {
kind: "type".to_string(),
name: type_decl.name.clone(),
details: type_decl
.variants
.iter()
.map(|variant| {
let fields = variant
.fields
.iter()
.map(|field| {
let ty = format_type_ref(&field.ty);
field
.name
.as_ref()
.map_or(ty.clone(), |name| format!("{name}: {ty}"))
})
.collect::<Vec<_>>()
.join(", ");
if fields.is_empty() {
variant.name.clone()
} else {
format!("{}({fields})", variant.name)
}
})
.collect(),
},
Item::Const(constant) => ItemInspection {
kind: "const".to_string(),
name: constant.name.clone(),
details: vec![format!("type: {}", format_type_ref(&constant.ty))],
},
Item::Function(function) => ItemInspection {
kind: "fn".to_string(),
name: function.name.clone(),
details: vec![format!(
"({}) -> {}",
format_params(&function.params),
format_type_ref(&function.return_type)
)],
},
Item::Validator(validator) => ItemInspection {
kind: "validator".to_string(),
name: validator.name.clone(),
details: vec![format!("({})", format_params(&validator.params))],
},
Item::Test(test) => ItemInspection {
kind: "test".to_string(),
name: test.name.clone(),
details: if test.should_fail {
vec!["expected failure".to_string()]
} else {
Vec::new()
},
},
})
.collect()
}
fn inspect_program(program: &Program) -> IrInspection {
let mut stats = IrStats::default();
inspect_term(&program.term, &mut stats);
IrInspection {
nodes: stats.nodes,
lambdas: stats.lambdas,
lets: stats.lets,
branches: stats.branches,
matches: stats.matches,
match_arms: stats.match_arms,
constructors: stats.constructors,
lists: stats.lists,
traces: stats.traces,
errors: stats.errors,
builtins: stats
.builtins
.into_iter()
.map(|(name, count)| BuiltinInspection {
class: builtin_class(&name).to_string(),
name,
count,
})
.collect(),
}
}
fn inspect_term(term: &Term, stats: &mut IrStats) {
stats.nodes += 1;
match term {
Term::Var(_)
| Term::Bool(_)
| Term::Int(_)
| Term::String(_)
| Term::ByteArray(_)
| Term::Unit => (),
Term::List(items) => {
stats.lists += 1;
for item in items {
inspect_term(item, stats);
}
}
Term::Lambda { body, .. } => {
stats.lambdas += 1;
inspect_term(body, stats);
}
Term::Let { value, body, .. } => {
stats.lets += 1;
inspect_term(value, stats);
inspect_term(body, stats);
}
Term::If {
condition,
then_term,
else_term,
} => {
stats.branches += 1;
inspect_term(condition, stats);
inspect_term(then_term, stats);
inspect_term(else_term, stats);
}
Term::BuiltinCall { name, args } => {
*stats.builtins.entry(name.clone()).or_insert(0) += 1;
for arg in args {
inspect_term(arg, stats);
}
}
Term::Constr { fields, .. } => {
stats.constructors += 1;
for field in fields {
inspect_term(field, stats);
}
}
Term::Match { subject, arms } => {
stats.matches += 1;
stats.match_arms += arms.len();
inspect_term(subject, stats);
for arm in arms {
inspect_term(&arm.body, stats);
}
}
Term::Trace { message, then_term } => {
stats.traces += 1;
inspect_term(message, stats);
inspect_term(then_term, stats);
}
Term::Error(_) => stats.errors += 1,
Term::Sequence { first, then_term } => {
inspect_term(first, stats);
inspect_term(then_term, stats);
}
}
}
fn inspect_uplc_core(program: &Program, include_program: bool) -> UplcCoreInspection {
let lowered = lower_program(program);
let validation = lowered.program.as_ref().map(validate_program);
let validated = validation
.as_ref()
.is_some_and(|validation| validation.valid);
let lowered_ok = lowered.lowered();
let program = include_program
.then(|| lowered.program.as_ref().map(|program| program.render()))
.flatten();
let mut blockers = lowered.blockers;
if let Some(validation) = &validation {
blockers.extend(
validation
.errors
.iter()
.map(|error| format!("UPLC-core validation failed: {error}")),
);
}
blockers.sort();
blockers.dedup();
UplcCoreInspection {
lowered: lowered_ok,
validated,
summary: if lowered_ok && validated {
"UPLC core candidate rendered and structurally validated for the supported pure subset"
.to_string()
} else if lowered_ok {
"UPLC core candidate rendered but failed structural validation".to_string()
} else {
"UPLC core candidate blocked by unsupported Aeri IR features".to_string()
},
blockers,
validation,
program,
}
}
fn inspect_readiness(
target: Target,
program: &Program,
param_types: &[(String, Type)],
custom_types: &[CustomTypeLayout],
uplc_core: Option<&UplcCoreInspection>,
) -> ReadinessInspection {
match target {
Target::Preview => ReadinessInspection {
target: target.name().to_string(),
ready: true,
notes: vec!["preview IR can be rendered for inspection, tests, linting, and blueprint-shaped JSON".to_string(), "preview IR is not serialized Cardano UPLC/CBOR".to_string()],
blockers: Vec::new(),
},
Target::Uplc => {
let mut notes = vec!["UPLC readiness checks cover the current serializable subset; unsupported validators still report blockers".to_string()];
let mut blockers = Vec::new();
if let Some(uplc_core) = uplc_core {
if uplc_core.lowered && uplc_core.validated {
notes.push("a structurally validated UPLC core candidate was produced before ledger serialization checks".to_string());
} else {
blockers.extend(uplc_core.blockers.clone());
}
}
match emit_ledger_program_with_layouts(program, param_types, custom_types) {
Ok(_) => notes.push(
"ledger serialization checks passed for flat bytes and CBOR output".to_string(),
),
Err(error) => blockers.extend(error.blockers),
}
blockers.sort();
blockers.dedup();
ReadinessInspection {
target: target.name().to_string(),
ready: blockers.is_empty(),
notes,
blockers,
}
}
}
}
fn builtin_class(name: &str) -> &'static str {
if is_transaction_builtin(name) {
"tx-context"
} else if is_list_literal_helper_builtin(name) {
"uplc-list-literal-helper"
} else if is_custom_tag_equality_builtin(name) {
"uplc-nullary-constructor"
} else if supports_aeri_builtin(name) {
"uplc-core-supported"
} else {
"aeri-runtime"
}
}
fn is_custom_tag_equality_builtin(name: &str) -> bool {
matches!(name, "equalsCustomTag" | "notEqualsCustomTag")
}
fn is_list_literal_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"
| "equalsListBool"
| "notEqualsListBool"
| "equalsListInteger"
| "notEqualsListInteger"
| "equalsListByteString"
| "notEqualsListByteString"
| "equalsListData"
| "notEqualsListData"
| "equalsListString"
| "notEqualsListString"
| "equalsListUnit"
| "notEqualsListUnit"
| "equalsListCustomTag"
| "notEqualsListCustomTag"
)
}
fn format_uplc_builtin_applications(validation: &ValidationReport) -> String {
let mut counts = BTreeMap::new();
for application in &validation.builtin_applications {
*counts.entry(application.name.clone()).or_insert(0usize) += 1;
}
if counts.is_empty() {
return "none".to_string();
}
counts
.into_iter()
.map(|(name, count)| format!("{name} x{count}"))
.collect::<Vec<_>>()
.join(", ")
}
fn format_params(params: &[Param]) -> String {
params
.iter()
.map(|param| format!("{}: {}", param.name, format_type_ref(¶m.ty)))
.collect::<Vec<_>>()
.join(", ")
}
fn format_type_ref(ty: &TypeRef) -> String {
if ty.args.is_empty() {
return ty.name.clone();
}
let args = ty
.args
.iter()
.map(format_type_ref)
.collect::<Vec<_>>()
.join(", ");
format!("{}<{args}>", ty.name)
}
fn push_header(out: &mut String, title: &str) {
out.push_str(
"+------------------------------------------------------------------------------+\n",
);
out.push_str(&format!("| {:<76} |\n", title));
out.push_str(
"+------------------------------------------------------------------------------+\n",
);
}
fn push_section(out: &mut String, title: &str) {
out.push('\n');
out.push_str(&format!("-- {title} --\n"));
}
fn push_line(out: &mut String, line: impl AsRef<str>) {
out.push_str(line.as_ref());
out.push('\n');
}
fn push_footer(out: &mut String) {
out.push_str(
"+------------------------------------------------------------------------------+\n",
);
}
#[cfg(test)]
mod tests {
use crate::{
backend::{PREVIEW_CARDANO_DEPLOYABLE, PREVIEW_COMPILER_TARGET, UPLC_COMPILER_TARGET},
compile_source,
};
use super::*;
#[test]
fn reports_preview_validator_structure() {
let source = "module demo;\n\nvalidator spend(redeemer: ByteArray, ctx: Tx) { redeemer == #01 }\n\ntest rejects_wrong_redeemer fails { #02 == #01 }\n";
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = inspect_output("demo.aeri", &output, Target::Preview, true);
assert_eq!(report.module, "demo");
assert_eq!(
report.target.compiler_target.as_deref(),
Some(PREVIEW_COMPILER_TARGET)
);
assert_eq!(report.target.cardano_deployable, PREVIEW_CARDANO_DEPLOYABLE);
assert_eq!(report.summary.validators, 1);
assert_eq!(report.summary.tests, 1);
assert_eq!(
report.items[1].details,
vec!["expected failure".to_string()]
);
assert_eq!(report.validators[0].title, "demo.spend");
assert!(report.validators[0].readiness.ready);
assert!(report.validators[0].preview_ir.is_some());
}
#[test]
fn reports_uplc_readiness_blockers_without_fake_output() {
let source = "module demo;\n\nvalidator spend(redeemer: ByteArray, ctx: Tx) { tx_paid_to(ctx, redeemer, 1) }\n";
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = inspect_output("demo.aeri", &output, Target::Uplc, false);
assert!(report.has_blockers());
assert!(report.target.implemented);
assert_eq!(
report.target.compiler_target.as_deref(),
Some(UPLC_COMPILER_TARGET)
);
assert!(!report.target.cardano_deployable);
assert!(!report.validators[0].readiness.ready);
assert!(report.validators[0].preview_ir.is_none());
assert!(
report.validators[0]
.readiness
.blockers
.iter()
.any(|blocker| blocker.contains("tx_paid_to"))
);
}
#[test]
fn reports_uplc_core_candidate_for_supported_subset() {
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 = inspect_output("demo.aeri", &output, Target::Uplc, true);
let uplc_core = report.validators[0]
.uplc_core
.as_ref()
.expect("uplc core inspection is present");
assert!(
uplc_core.lowered,
"unexpected blockers: {:?}",
uplc_core.blockers
);
assert!(uplc_core.validated);
assert!(uplc_core.blockers.is_empty());
assert!(
uplc_core
.validation
.as_ref()
.is_some_and(|validation| validation.valid)
);
assert!(
uplc_core
.program
.as_ref()
.is_some_and(|program| program.contains("uplc-program 1.0.0"))
);
assert!(report.validators[0].readiness.ready);
assert!(report.validators[0].readiness.blockers.is_empty());
}
#[test]
fn reports_unsupported_ledger_decoder_blocker_in_uplc_readiness() {
let source = "module demo;\n\nvalidator gate(items: List<List<Int>>, ctx: Tx) { true }\n";
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = inspect_output("demo.aeri", &output, Target::Uplc, false);
let validator = &report.validators[0];
let uplc_core = validator
.uplc_core
.as_ref()
.expect("uplc core inspection is present");
assert!(uplc_core.lowered);
assert!(uplc_core.validated);
assert!(!validator.readiness.ready);
assert!(validator.readiness.blockers.iter().any(|blocker| {
blocker.contains(
"validator parameter 'items: List<List<Int>>' needs Plutus Data decoder lowering",
)
}));
}
#[test]
fn terminal_report_summarizes_uplc_core_builtins() {
let source = "module demo;\n\nvalidator gate(redeemer: ByteArray, ctx: Tx) { match redeemer { #01 => true, _ => false } }\n";
let output = compile_source("demo.aeri", source).expect("source compiles");
let report = inspect_output("demo.aeri", &output, Target::Uplc, false);
let rendered = render_terminal_report(&report);
assert!(rendered.contains("uplc builtins:"));
assert!(rendered.contains("equalsByteString x1"));
assert!(rendered.contains("ifThenElse x1"));
}
#[test]
fn aggregates_project_inspection_reports() {
let source = "module demo;\n\nvalidator spend(redeemer: ByteArray, ctx: Tx) { tx_paid_to(ctx, redeemer, 1) }\n";
let output = compile_source("demo.aeri", source).expect("source compiles");
let files = vec!["demo.aeri".to_string()];
let outputs = vec![output];
let report = inspect_project_outputs(".", &files, &outputs, Target::Uplc, false);
assert!(report.has_blockers());
assert_eq!(report.summary.files, 1);
assert_eq!(report.summary.validators, 1);
assert_eq!(report.summary.blocked_validators, 1);
}
#[test]
fn classifies_core_supported_builtins_from_uplc_source_of_truth() {
assert_eq!(builtin_class("sha2_256"), "uplc-core-supported");
assert_eq!(builtin_class("greaterThanInteger"), "uplc-core-supported");
assert_eq!(builtin_class("not"), "uplc-core-supported");
assert_eq!(builtin_class("list_has_bytes"), "uplc-list-literal-helper");
assert_eq!(builtin_class("list_has_unit"), "uplc-list-literal-helper");
assert_eq!(builtin_class("list_len_unit"), "uplc-list-literal-helper");
assert_eq!(
builtin_class("equalsListInteger"),
"uplc-list-literal-helper"
);
assert_eq!(builtin_class("equalsListUnit"), "uplc-list-literal-helper");
assert_eq!(
builtin_class("equalsListCustomTag"),
"uplc-list-literal-helper"
);
assert_eq!(builtin_class("equalsCustomTag"), "uplc-nullary-constructor");
assert_eq!(builtin_class("tx_signed_by"), "tx-context");
}
}