use std::collections::{HashMap, HashSet};
use serde::Serialize;
use sha2::{Digest, Sha256};
use crate::{
Result,
ast::{
BinaryOp, Block, Const, Expr, ExprKind, Function, Item, MatchArm as AstMatchArm, Module,
Param, Pattern as AstPattern, Statement, Test, TypeDecl, UnaryOp, Validator,
},
backend::{
PREVIEW_CARDANO_DEPLOYABLE, PREVIEW_COMPILER_TARGET, UPLC_CARDANO_DEPLOYABLE,
UPLC_COMPILER_TARGET, UPLC_PLUTUS_VERSION, render_preview_program,
},
diagnostic::{AeriError, Span},
ir::{MatchArm as IrMatchArm, Pattern as IrPattern, Program, Term, validate_program},
parser::parse_module,
types::{
Signature, Type, builtin_signature, is_builtin_name, is_builtin_type_name,
is_test_fixture_builtin,
},
uplc::{
CustomTypeLayout, CustomTypeVariantLayout, LedgerProgram, emit_ledger_program_with_layouts,
},
};
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct Blueprint {
pub preamble: Preamble,
pub validators: Vec<BlueprintValidator>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub definitions: Vec<BlueprintDefinition>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Preamble {
pub title: String,
pub version: String,
pub plutus_version: String,
pub compiler: Compiler,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Compiler {
pub name: String,
pub version: String,
pub target: String,
pub cardano_deployable: bool,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct BlueprintValidator {
pub title: String,
pub script_purpose: String,
pub parameters: Vec<BlueprintParameter>,
pub compiled_code: String,
pub hash: String,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct BlueprintParameter {
pub name: String,
pub schema: Schema,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Schema {
pub data_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub items: Option<Box<Schema>>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct BlueprintDefinition {
pub title: String,
pub any_of: Vec<BlueprintVariant>,
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct BlueprintVariant {
pub title: String,
pub index: usize,
pub fields: Vec<BlueprintParameter>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompileOutput {
pub module: Module,
pub script: String,
pub blueprint: Blueprint,
pub lowered_validators: Vec<LoweredValidator>,
pub validator_count: usize,
pub function_count: usize,
pub test_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoweredValidator {
pub title: String,
pub script_purpose: String,
pub param_types: Vec<(String, Type)>,
pub custom_types: Vec<CustomTypeLayout>,
pub program: Program,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UplcValidatorArtifact {
pub title: String,
pub script_purpose: String,
pub parameters: Vec<BlueprintParameter>,
pub ledger: LedgerProgram,
}
struct CheckedModule {
constants: HashMap<String, ConstSignature>,
functions: HashMap<String, Signature>,
function_defs: HashMap<String, Function>,
constructors: HashMap<String, ConstructorSignature>,
type_names: HashSet<String>,
type_variants: HashMap<String, Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ConstSignature {
ty: Type,
value: Expr,
span: Span,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ConstructorSignature {
type_name: String,
fields: Vec<Type>,
tag: usize,
span: Span,
}
struct FunctionArtifact<'a> {
function: &'a Function,
program: Program,
}
struct ConstArtifact<'a> {
constant: &'a Const,
program: Program,
}
struct ValidatorArtifact<'a> {
validator: &'a Validator,
param_types: Vec<(String, Type)>,
program: Program,
}
struct TestArtifact<'a> {
test: &'a Test,
program: Program,
}
pub fn compile_source(file: &str, source: &str) -> Result<CompileOutput> {
let module = parse_module(file, source).map_err(|error| error.with_source(source))?;
let checked = check_module(file, &module).map_err(|error| error.with_source(source))?;
let const_artifacts =
lower_constants(file, &module, &checked).map_err(|error| error.with_source(source))?;
let function_artifacts =
lower_functions(file, &module, &checked).map_err(|error| error.with_source(source))?;
let validator_artifacts =
lower_validators(file, &module, &checked).map_err(|error| error.with_source(source))?;
let lowered_validators = collect_lowered_validators(&module, &checked, &validator_artifacts);
let test_artifacts =
lower_tests(file, &module, &checked).map_err(|error| error.with_source(source))?;
validate_lowered_ir(
file,
&const_artifacts,
&function_artifacts,
&validator_artifacts,
&test_artifacts,
)
.map_err(|error| error.with_source(source))?;
let script = render_module(
&module,
&const_artifacts,
&function_artifacts,
&validator_artifacts,
&test_artifacts,
);
let blueprint = build_blueprint(&module, &lowered_validators);
Ok(CompileOutput {
function_count: function_artifacts.len(),
validator_count: validator_artifacts.len(),
test_count: test_artifacts.len(),
module,
script,
blueprint,
lowered_validators,
})
}
pub fn merge_blueprints(title: &str, outputs: &[CompileOutput]) -> Blueprint {
try_merge_blueprints(title, outputs)
.expect("merge_blueprints called with conflicting blueprint outputs")
}
pub fn try_merge_blueprints(title: &str, outputs: &[CompileOutput]) -> Result<Blueprint> {
let blueprints = outputs
.iter()
.map(|output| output.blueprint.clone())
.collect::<Vec<_>>();
try_merge_blueprint_documents(title, &blueprints)
}
pub fn build_uplc_blueprint(
file: &str,
output: &CompileOutput,
) -> Result<(Blueprint, Vec<UplcValidatorArtifact>)> {
let artifacts = emit_uplc_validators(file, output)?;
let mut blueprint = output.blueprint.clone();
blueprint.preamble.plutus_version = UPLC_PLUTUS_VERSION.to_string();
blueprint.preamble.compiler.target = UPLC_COMPILER_TARGET.to_string();
blueprint.preamble.compiler.cardano_deployable = UPLC_CARDANO_DEPLOYABLE;
for validator in &mut blueprint.validators {
let artifact = artifacts
.iter()
.find(|artifact| artifact.title == validator.title)
.expect("UPLC artifact exists for every blueprint validator");
validator.compiled_code = artifact.ledger.cbor_hex.clone();
validator.hash = artifact.ledger.script_hash.clone();
}
Ok((blueprint, artifacts))
}
pub fn build_uplc_project_blueprint(
title: &str,
files: &[String],
outputs: &[CompileOutput],
) -> Result<(Blueprint, Vec<UplcValidatorArtifact>)> {
let mut blueprints = Vec::new();
let mut artifacts = Vec::new();
for (file, output) in files.iter().zip(outputs) {
let (blueprint, file_artifacts) = build_uplc_blueprint(file, output)?;
blueprints.push(blueprint);
artifacts.extend(file_artifacts);
}
Ok((
try_merge_blueprint_documents(title, &blueprints)?,
artifacts,
))
}
pub fn try_merge_blueprint_documents(title: &str, blueprints: &[Blueprint]) -> Result<Blueprint> {
let mut validators = Vec::new();
let mut definitions = Vec::new();
let mut seen_validators = HashSet::new();
let mut seen_definitions = HashSet::new();
let mut definition_shapes = HashMap::<String, BlueprintDefinition>::new();
for blueprint in blueprints {
for validator in &blueprint.validators {
if !seen_validators.insert(validator.title.clone()) {
return Err(AeriError::at(
title,
1,
1,
format!("duplicate validator title '{}'", validator.title),
));
}
validators.push(validator.clone());
}
for definition in &blueprint.definitions {
if let Some(first_definition) = definition_shapes.get(&definition.title) {
if first_definition != definition {
return Err(AeriError::at(
title,
1,
1,
format!("conflicting blueprint definition '{}'", definition.title),
));
}
} else {
definition_shapes.insert(definition.title.clone(), definition.clone());
}
if seen_definitions.insert(definition.title.clone()) {
definitions.push(definition.clone());
}
}
}
let mut preamble = blueprints
.first()
.map(|blueprint| blueprint.preamble.clone())
.unwrap_or_else(|| preamble(title));
preamble.title = title.to_string();
Ok(Blueprint {
preamble,
validators,
definitions,
})
}
fn emit_uplc_validators(file: &str, output: &CompileOutput) -> Result<Vec<UplcValidatorArtifact>> {
output
.lowered_validators
.iter()
.map(|validator| {
let ledger = emit_ledger_program_with_layouts(
&validator.program,
&validator.param_types,
&validator.custom_types,
)
.map_err(|error| {
AeriError::at(
file,
1,
1,
format!(
"validator '{}' cannot be emitted as UPLC/CBOR: {}",
validator.title,
error.blockers.join("; ")
),
)
})?;
Ok(UplcValidatorArtifact {
title: validator.title.clone(),
script_purpose: validator.script_purpose.clone(),
parameters: validator
.param_types
.iter()
.map(|(name, ty)| BlueprintParameter {
name: name.clone(),
schema: schema_for_type(ty),
})
.collect(),
ledger,
})
})
.collect()
}
fn validate_lowered_ir(
file: &str,
constants: &[ConstArtifact<'_>],
functions: &[FunctionArtifact<'_>],
validators: &[ValidatorArtifact<'_>],
tests: &[TestArtifact<'_>],
) -> Result<()> {
for artifact in constants {
validate_lowered_program(file, "const", &artifact.constant.name, &artifact.program)?;
}
for artifact in functions {
validate_lowered_program(file, "function", &artifact.function.name, &artifact.program)?;
}
for artifact in validators {
validate_lowered_program(
file,
"validator",
&artifact.validator.name,
&artifact.program,
)?;
}
for artifact in tests {
validate_lowered_program(file, "test", &artifact.test.name, &artifact.program)?;
}
Ok(())
}
fn validate_lowered_program(file: &str, kind: &str, name: &str, program: &Program) -> Result<()> {
let report = validate_program(program);
if report.valid {
return Ok(());
}
Err(AeriError::at(
file,
1,
1,
format!(
"internal IR validation failed for {kind} '{name}': {}",
report.errors.join("; ")
),
))
}
fn check_module(file: &str, module: &Module) -> Result<CheckedModule> {
let mut names = HashMap::<String, Span>::new();
let mut type_names = HashSet::new();
let mut type_variants = HashMap::new();
let mut constructors = HashMap::new();
let mut constants = HashMap::new();
let mut functions = HashMap::new();
let mut function_defs = HashMap::new();
for item in &module.items {
if let Some(first_span) = names.insert(item.name().to_string(), item_span(item)) {
return Err(AeriError::at_span(
file,
item_span(item),
format!(
"duplicate declaration '{}'; first declared at line {}",
item.name(),
first_span.line
),
));
}
if is_builtin_name(item.name()) {
return Err(AeriError::at_span(
file,
item_span(item),
format!("'{}' is reserved for a builtin", item.name()),
));
}
if matches!(item, Item::Type(_)) && is_builtin_type_name(item.name()) {
return Err(AeriError::at_span(
file,
item_span(item),
format!("'{}' is reserved for a built-in type", item.name()),
));
}
}
for item in &module.items {
if let Item::Type(type_decl) = item {
type_names.insert(type_decl.name.clone());
}
}
for item in &module.items {
if let Item::Type(type_decl) = item {
check_type_decl(
file,
type_decl,
&type_names,
&names,
&mut constructors,
&mut type_variants,
)?;
}
}
for item in &module.items {
if let Item::Const(constant) = item {
if constructors.contains_key(&constant.name) {
return Err(AeriError::at_span(
file,
constant.span,
format!("constant '{}' conflicts with a constructor", constant.name),
));
}
constants.insert(
constant.name.clone(),
ConstSignature {
ty: Type::from_ref(file, &constant.ty, &type_names)?,
value: constant.value.clone(),
span: constant.span,
},
);
}
}
for item in &module.items {
if let Item::Function(function) = item {
if constructors.contains_key(&function.name) {
return Err(AeriError::at_span(
file,
function.span,
format!("function '{}' conflicts with a constructor", function.name),
));
}
let params = param_types(file, &function.params, &type_names)?
.into_iter()
.map(|(_, ty)| ty)
.collect();
let result = Type::from_ref(file, &function.return_type, &type_names)?;
functions.insert(
function.name.clone(),
Signature {
params,
result,
span: function.span,
},
);
function_defs.insert(function.name.clone(), function.clone());
}
}
let checked = CheckedModule {
constants,
functions,
function_defs,
constructors,
type_names,
type_variants,
};
check_function_cycles(file, module, &checked.functions)?;
check_top_level_cycles(file, module, &checked.constants, &checked.functions)?;
for item in &module.items {
match item {
Item::Type(_) => (),
Item::Const(constant) => check_const(file, constant, &checked)?,
Item::Function(function) => check_function(file, function, &checked)?,
Item::Validator(validator) => check_validator(file, validator, &checked)?,
Item::Test(test) => check_test(file, test, &checked)?,
}
}
Ok(checked)
}
fn check_const(file: &str, constant: &Const, checked: &CheckedModule) -> Result<()> {
reject_test_fixture_expr(file, &constant.value, "constant")?;
let expected = Type::from_ref(file, &constant.ty, &checked.type_names)?;
let env = HashMap::new();
let actual = check_expr_with_hint(file, &constant.value, &env, checked, Some(&expected))?;
expect_type(file, constant.span, &actual, &expected, "constant value")
}
fn check_test(file: &str, test: &Test, checked: &CheckedModule) -> Result<()> {
let mut env = HashMap::new();
let actual = check_block(file, &test.body, &mut env, checked, Some(&Type::Bool))?;
expect_type(file, test.body.span, &actual, &Type::Bool, "test body")
}
fn reject_test_fixture_block(file: &str, block: &Block, context: &str) -> Result<()> {
for statement in &block.statements {
match statement {
Statement::Let { value, .. }
| Statement::Trace { message: value, .. }
| Statement::Return { value, .. }
| Statement::Expr { value, .. } => reject_test_fixture_expr(file, value, context)?,
Statement::Require { condition, .. } => {
reject_test_fixture_expr(file, condition, context)?;
}
}
}
Ok(())
}
fn reject_test_fixture_expr(file: &str, expr: &Expr, context: &str) -> Result<()> {
match &expr.kind {
ExprKind::Call { callee, args } => {
if is_test_fixture_builtin(callee) {
return Err(AeriError::at_span(
file,
expr.span,
format!(
"'{callee}' is a test fixture builtin and can only be used in test blocks, not in a {context}"
),
));
}
for arg in args {
reject_test_fixture_expr(file, arg, context)?;
}
}
ExprKind::Unary { expr, .. } => reject_test_fixture_expr(file, expr, context)?,
ExprKind::Binary { left, right, .. } => {
reject_test_fixture_expr(file, left, context)?;
reject_test_fixture_expr(file, right, context)?;
}
ExprKind::If {
condition,
then_branch,
else_branch,
} => {
reject_test_fixture_expr(file, condition, context)?;
reject_test_fixture_block(file, then_branch, context)?;
reject_test_fixture_block(file, else_branch, context)?;
}
ExprKind::Match { subject, arms } => {
reject_test_fixture_expr(file, subject, context)?;
for arm in arms {
reject_test_fixture_block(file, &arm.body, context)?;
}
}
ExprKind::List(items) => {
for item in items {
reject_test_fixture_expr(file, item, context)?;
}
}
ExprKind::Bool(_)
| ExprKind::Int(_)
| ExprKind::String(_)
| ExprKind::ByteArray(_)
| ExprKind::Unit
| ExprKind::Variable(_)
| ExprKind::Fail => (),
}
Ok(())
}
fn check_type_decl(
file: &str,
type_decl: &TypeDecl,
type_names: &HashSet<String>,
item_names: &HashMap<String, Span>,
constructors: &mut HashMap<String, ConstructorSignature>,
type_variants: &mut HashMap<String, Vec<String>>,
) -> Result<()> {
if type_decl.variants.is_empty() {
return Err(AeriError::at_span(
file,
type_decl.span,
format!(
"type '{}' must declare at least one variant",
type_decl.name
),
));
}
let mut local_variants = HashSet::new();
let mut variant_names = Vec::new();
for (tag, variant) in type_decl.variants.iter().enumerate() {
if !local_variants.insert(variant.name.clone()) {
return Err(AeriError::at_span(
file,
variant.span,
format!("duplicate variant '{}'", variant.name),
));
}
if !starts_with_uppercase(&variant.name) {
return Err(AeriError::at_span(
file,
variant.span,
format!(
"constructor '{}' must start with an uppercase letter",
variant.name
),
));
}
if is_builtin_name(&variant.name) {
return Err(AeriError::at_span(
file,
variant.span,
format!("'{}' is reserved for a builtin", variant.name),
));
}
if let Some(first_span) = item_names.get(&variant.name) {
return Err(AeriError::at_span(
file,
variant.span,
format!(
"constructor '{}' conflicts with a top-level declaration at line {}",
variant.name, first_span.line
),
));
}
if constructors.contains_key(&variant.name) {
return Err(AeriError::at_span(
file,
variant.span,
format!("duplicate constructor '{}'", variant.name),
));
}
let mut field_names = HashSet::new();
for field in &variant.fields {
if let Some(name) = &field.name {
if !field_names.insert(name.clone()) {
return Err(AeriError::at_span(
file,
field.span,
format!(
"duplicate field name '{name}' in constructor '{}'",
variant.name
),
));
}
}
}
let fields = variant
.fields
.iter()
.map(|field| Type::from_ref(file, &field.ty, type_names))
.collect::<Result<Vec<_>>>()?;
constructors.insert(
variant.name.clone(),
ConstructorSignature {
type_name: type_decl.name.clone(),
fields,
tag,
span: variant.span,
},
);
variant_names.push(variant.name.clone());
}
type_variants.insert(type_decl.name.clone(), variant_names);
Ok(())
}
fn check_function(file: &str, function: &Function, checked: &CheckedModule) -> Result<()> {
reject_test_fixture_block(file, &function.body, "function")?;
let mut env = env_from_params(file, &function.params, &checked.type_names)?;
let expected = Type::from_ref(file, &function.return_type, &checked.type_names)?;
let actual = check_block(file, &function.body, &mut env, checked, Some(&expected))?;
expect_type(
file,
function.body.span,
&actual,
&expected,
"function body",
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum VisitState {
Visiting,
Done,
}
fn check_function_cycles(
file: &str,
module: &Module,
functions: &HashMap<String, Signature>,
) -> Result<()> {
let mut graph = HashMap::<String, Vec<(String, Span)>>::new();
for item in &module.items {
if let Item::Function(function) = item {
let mut calls = Vec::new();
collect_function_calls(&function.body, functions, &mut calls);
graph.insert(function.name.clone(), calls);
}
}
let mut states = HashMap::new();
for name in graph.keys() {
visit_function(file, name, &graph, &mut states)?;
}
Ok(())
}
fn visit_function(
file: &str,
name: &str,
graph: &HashMap<String, Vec<(String, Span)>>,
states: &mut HashMap<String, VisitState>,
) -> Result<()> {
match states.get(name) {
Some(VisitState::Done) => return Ok(()),
Some(VisitState::Visiting) => return Ok(()),
None => (),
}
states.insert(name.to_string(), VisitState::Visiting);
if let Some(calls) = graph.get(name) {
for (callee, span) in calls {
if states.get(callee) == Some(&VisitState::Visiting) {
return Err(AeriError::at_span(
file,
*span,
format!("recursive helper call involving '{callee}' cannot be lowered"),
));
}
visit_function(file, callee, graph, states)?;
}
}
states.insert(name.to_string(), VisitState::Done);
Ok(())
}
fn check_top_level_cycles(
file: &str,
module: &Module,
constants: &HashMap<String, ConstSignature>,
functions: &HashMap<String, Signature>,
) -> Result<()> {
let mut graph = HashMap::<String, Vec<(String, Span)>>::new();
let mut roots = Vec::new();
for item in &module.items {
match item {
Item::Const(constant) => {
let mut dependencies = Vec::new();
let locals = HashSet::new();
collect_top_level_expr_deps(
&constant.value,
constants,
functions,
&locals,
&mut dependencies,
);
roots.push(constant.name.clone());
graph.insert(constant.name.clone(), dependencies);
}
Item::Function(function) => {
let mut dependencies = Vec::new();
let mut locals = locals_from_params(&function.params);
collect_top_level_block_deps(
&function.body,
constants,
functions,
&mut locals,
&mut dependencies,
);
roots.push(function.name.clone());
graph.insert(function.name.clone(), dependencies);
}
Item::Type(_) | Item::Validator(_) | Item::Test(_) => (),
}
}
let mut states = HashMap::new();
for name in roots {
visit_top_level(file, &name, &graph, &mut states)?;
}
Ok(())
}
fn visit_top_level(
file: &str,
name: &str,
graph: &HashMap<String, Vec<(String, Span)>>,
states: &mut HashMap<String, VisitState>,
) -> Result<()> {
match states.get(name) {
Some(VisitState::Done) => return Ok(()),
Some(VisitState::Visiting) => return Ok(()),
None => (),
}
states.insert(name.to_string(), VisitState::Visiting);
if let Some(dependencies) = graph.get(name) {
for (dependency, span) in dependencies {
if states.get(dependency) == Some(&VisitState::Visiting) {
return Err(AeriError::at_span(
file,
*span,
format!(
"recursive top-level dependency involving '{dependency}' cannot be lowered"
),
));
}
visit_top_level(file, dependency, graph, states)?;
}
}
states.insert(name.to_string(), VisitState::Done);
Ok(())
}
fn collect_top_level_block_deps(
block: &Block,
constants: &HashMap<String, ConstSignature>,
functions: &HashMap<String, Signature>,
locals: &mut HashSet<String>,
dependencies: &mut Vec<(String, Span)>,
) {
for statement in &block.statements {
match statement {
Statement::Let { name, value, .. } => {
collect_top_level_expr_deps(value, constants, functions, locals, dependencies);
locals.insert(name.clone());
}
Statement::Return { value, .. } | Statement::Expr { value, .. } => {
collect_top_level_expr_deps(value, constants, functions, locals, dependencies)
}
Statement::Require { condition, .. } => {
collect_top_level_expr_deps(condition, constants, functions, locals, dependencies)
}
Statement::Trace { message, .. } => {
collect_top_level_expr_deps(message, constants, functions, locals, dependencies)
}
}
}
}
fn collect_top_level_expr_deps(
expr: &Expr,
constants: &HashMap<String, ConstSignature>,
functions: &HashMap<String, Signature>,
locals: &HashSet<String>,
dependencies: &mut Vec<(String, Span)>,
) {
match &expr.kind {
ExprKind::Variable(name) => {
if constants.contains_key(name) && !locals.contains(name) {
dependencies.push((name.clone(), expr.span));
}
}
ExprKind::Call { callee, args } => {
if functions.contains_key(callee) {
dependencies.push((callee.clone(), expr.span));
}
for arg in args {
collect_top_level_expr_deps(arg, constants, functions, locals, dependencies);
}
}
ExprKind::Unary { expr, .. } => {
collect_top_level_expr_deps(expr, constants, functions, locals, dependencies)
}
ExprKind::Binary { left, right, .. } => {
collect_top_level_expr_deps(left, constants, functions, locals, dependencies);
collect_top_level_expr_deps(right, constants, functions, locals, dependencies);
}
ExprKind::If {
condition,
then_branch,
else_branch,
} => {
collect_top_level_expr_deps(condition, constants, functions, locals, dependencies);
let mut then_locals = locals.clone();
collect_top_level_block_deps(
then_branch,
constants,
functions,
&mut then_locals,
dependencies,
);
let mut else_locals = locals.clone();
collect_top_level_block_deps(
else_branch,
constants,
functions,
&mut else_locals,
dependencies,
);
}
ExprKind::Match { subject, arms } => {
collect_top_level_expr_deps(subject, constants, functions, locals, dependencies);
for arm in arms {
let mut arm_locals = locals.clone();
collect_pattern_bindings(&arm.pattern, &mut arm_locals);
collect_top_level_block_deps(
&arm.body,
constants,
functions,
&mut arm_locals,
dependencies,
);
}
}
ExprKind::List(items) => {
for item in items {
collect_top_level_expr_deps(item, constants, functions, locals, dependencies);
}
}
ExprKind::Bool(_)
| ExprKind::Int(_)
| ExprKind::String(_)
| ExprKind::ByteArray(_)
| ExprKind::Unit
| ExprKind::Fail => (),
}
}
fn collect_pattern_bindings(pattern: &AstPattern, locals: &mut HashSet<String>) {
match pattern {
AstPattern::Variable { name, .. } => {
locals.insert(name.clone());
}
AstPattern::Constructor { bindings, .. } => {
for binding in bindings {
if let Some(name) = &binding.name {
locals.insert(name.clone());
}
}
}
AstPattern::Wildcard { .. }
| AstPattern::Bool { .. }
| AstPattern::Int { .. }
| AstPattern::ByteArray { .. }
| AstPattern::String { .. }
| AstPattern::Unit { .. } => (),
}
}
fn locals_from_params(params: &[Param]) -> HashSet<String> {
params.iter().map(|param| param.name.clone()).collect()
}
fn collect_function_calls(
block: &Block,
functions: &HashMap<String, Signature>,
calls: &mut Vec<(String, Span)>,
) {
for statement in &block.statements {
match statement {
Statement::Let { value, .. }
| Statement::Return { value, .. }
| Statement::Expr { value, .. } => collect_expr_calls(value, functions, calls),
Statement::Require { condition, .. } => collect_expr_calls(condition, functions, calls),
Statement::Trace { message, .. } => collect_expr_calls(message, functions, calls),
}
}
}
fn collect_expr_calls(
expr: &Expr,
functions: &HashMap<String, Signature>,
calls: &mut Vec<(String, Span)>,
) {
match &expr.kind {
ExprKind::Call { callee, args } => {
if functions.contains_key(callee) {
calls.push((callee.clone(), expr.span));
}
for arg in args {
collect_expr_calls(arg, functions, calls);
}
}
ExprKind::Unary { expr, .. } => collect_expr_calls(expr, functions, calls),
ExprKind::Binary { left, right, .. } => {
collect_expr_calls(left, functions, calls);
collect_expr_calls(right, functions, calls);
}
ExprKind::If {
condition,
then_branch,
else_branch,
} => {
collect_expr_calls(condition, functions, calls);
collect_function_calls(then_branch, functions, calls);
collect_function_calls(else_branch, functions, calls);
}
ExprKind::Match { subject, arms } => {
collect_expr_calls(subject, functions, calls);
for arm in arms {
collect_function_calls(&arm.body, functions, calls);
}
}
ExprKind::List(items) => {
for item in items {
collect_expr_calls(item, functions, calls);
}
}
ExprKind::Bool(_)
| ExprKind::Int(_)
| ExprKind::String(_)
| ExprKind::ByteArray(_)
| ExprKind::Unit
| ExprKind::Fail
| ExprKind::Variable(_) => (),
}
}
fn check_validator(file: &str, validator: &Validator, checked: &CheckedModule) -> Result<()> {
reject_test_fixture_block(file, &validator.body, "validator")?;
let param_types = param_types(file, &validator.params, &checked.type_names)?;
if !(2..=3).contains(¶m_types.len()) {
return Err(AeriError::at_span(
file,
validator.span,
"validator must take 2 or 3 parameters ending with a Tx context",
));
}
if param_types.last().map(|(_, ty)| ty) != Some(&Type::Tx) {
return Err(AeriError::at_span(
file,
validator
.params
.last()
.map_or(validator.span, |param| param.span),
"validator's final parameter must be Tx",
));
}
for (index, (name, ty)) in param_types[..param_types.len() - 1].iter().enumerate() {
if *ty == Type::Tx {
return Err(AeriError::at_span(
file,
validator.params[index].span,
format!(
"validator parameter '{name}' has type Tx, but only the final parameter may be Tx"
),
));
}
}
let mut env = env_from_params(file, &validator.params, &checked.type_names)?;
let actual = check_block(file, &validator.body, &mut env, checked, Some(&Type::Bool))?;
expect_type(
file,
validator.body.span,
&actual,
&Type::Bool,
"validator body",
)
}
fn check_block(
file: &str,
block: &Block,
env: &mut HashMap<String, Type>,
checked: &CheckedModule,
expected: Option<&Type>,
) -> Result<Type> {
let mut result = Type::Unit;
for (index, statement) in block.statements.iter().enumerate() {
let is_last = index + 1 == block.statements.len();
match statement {
Statement::Let {
name,
ty,
value,
span,
} => {
ensure_fresh_binding(file, *span, env, name)?;
let binding_type = if let Some(ty) = ty {
let expected = Type::from_ref(file, ty, &checked.type_names)?;
let value_type =
check_expr_with_hint(file, value, env, checked, Some(&expected))?;
expect_type(file, value.span, &value_type, &expected, "let binding")?;
expected
} else {
check_expr(file, value, env, checked)?
};
env.insert(name.clone(), binding_type);
result = Type::Unit;
}
Statement::Require { condition, span } => {
let actual = check_expr(file, condition, env, checked)?;
expect_type(file, *span, &actual, &Type::Bool, "require condition")?;
result = Type::Unit;
}
Statement::Trace { message, span } => {
let actual = check_expr(file, message, env, checked)?;
expect_type(file, *span, &actual, &Type::String, "trace message")?;
result = Type::Unit;
}
Statement::Return { value, span } => {
if !is_last {
return Err(AeriError::at_span(
file,
*span,
"return must be the last statement in a block",
));
}
let actual = check_expr_with_hint(file, value, env, checked, expected)?;
if let Some(expected) = expected {
expect_type(file, *span, &actual, expected, "return value")?;
}
result = actual;
}
Statement::Expr {
value,
trailing_semicolon,
span,
} => {
let hint = if is_last && !trailing_semicolon {
expected
} else {
None
};
let actual = check_expr_with_hint(file, value, env, checked, hint)?;
if *trailing_semicolon {
result = Type::Unit;
} else if is_last {
result = actual;
} else {
return Err(AeriError::at_span(
file,
*span,
"expression statements need ';' unless they are the block result",
));
}
}
}
}
Ok(result)
}
fn check_expr(
file: &str,
expr: &Expr,
env: &HashMap<String, Type>,
checked: &CheckedModule,
) -> Result<Type> {
check_expr_with_hint(file, expr, env, checked, None)
}
fn check_expr_with_hint(
file: &str,
expr: &Expr,
env: &HashMap<String, Type>,
checked: &CheckedModule,
expected: Option<&Type>,
) -> Result<Type> {
match &expr.kind {
ExprKind::Bool(_) => Ok(Type::Bool),
ExprKind::Int(_) => Ok(Type::Int),
ExprKind::String(_) => Ok(Type::String),
ExprKind::ByteArray(_) => Ok(Type::ByteArray),
ExprKind::Unit => Ok(Type::Unit),
ExprKind::Fail => Ok(Type::Never),
ExprKind::List(items) => {
if let Some(Type::List(item_type)) = expected {
let item_type = item_type.as_ref().clone();
for item in items {
let actual = check_expr_with_hint(file, item, env, checked, Some(&item_type))?;
expect_type(file, item.span, &actual, &item_type, "list element")?;
}
return Ok(Type::List(Box::new(item_type)));
}
let Some(first) = items.first() else {
return Err(AeriError::at_span(
file,
expr.span,
"empty list literals need a List<T> annotation or return type",
));
};
let item_type = check_expr(file, first, env, checked)?;
for item in &items[1..] {
let actual = check_expr(file, item, env, checked)?;
expect_type(file, item.span, &actual, &item_type, "list element")?;
}
Ok(Type::List(Box::new(item_type)))
}
ExprKind::Variable(name) => {
if let Some(ty) = env.get(name) {
Ok(ty.clone())
} else if let Some(constant) = checked.constants.get(name) {
Ok(constant.ty.clone())
} else if let Some(constructor) = checked.constructors.get(name) {
if constructor.fields.is_empty() {
Ok(Type::Custom(constructor.type_name.clone()))
} else {
Err(AeriError::at_span(
file,
expr.span,
format!(
"constructor '{name}' expects {} field(s)",
constructor.fields.len()
),
))
}
} else {
Err(AeriError::at_span(
file,
expr.span,
format!("unknown variable '{name}'"),
))
}
}
ExprKind::Unary { op, expr: inner } => {
let actual = check_expr(file, inner, env, checked)?;
match op {
UnaryOp::Not => {
expect_type(file, expr.span, &actual, &Type::Bool, "unary '!' operand")?;
Ok(Type::Bool)
}
UnaryOp::Negate => {
expect_type(file, expr.span, &actual, &Type::Int, "unary '-' operand")?;
Ok(Type::Int)
}
}
}
ExprKind::Binary { left, op, right } => {
if matches!(op, BinaryOp::Equal | BinaryOp::NotEqual)
&& is_empty_list(left)
&& is_empty_list(right)
{
return Ok(Type::Bool);
}
let (left_type, right_type) =
check_binary_operands(file, left, *op, right, env, checked)?;
check_binary(file, expr.span, *op, &left_type, &right_type)
}
ExprKind::Call { callee, args } => check_call(file, expr.span, callee, args, env, checked),
ExprKind::If {
condition,
then_branch,
else_branch,
} => {
let condition_type = check_expr(file, condition, env, checked)?;
expect_type(
file,
condition.span,
&condition_type,
&Type::Bool,
"if condition",
)?;
let mut then_env = env.clone();
let then_type = check_block(file, then_branch, &mut then_env, checked, expected)?;
let mut else_env = env.clone();
let else_type = check_block(file, else_branch, &mut else_env, checked, expected)?;
if then_type != else_type && then_type != Type::Never && else_type != Type::Never {
return Err(AeriError::at_span(
file,
expr.span,
format!("if branches return different types: {then_type} and {else_type}"),
));
}
Ok(if then_type == Type::Never {
else_type
} else {
then_type
})
}
ExprKind::Match { subject, arms } => {
check_match(file, expr.span, subject, arms, env, checked, expected)
}
}
}
fn check_binary_operands(
file: &str,
left: &Expr,
op: BinaryOp,
right: &Expr,
env: &HashMap<String, Type>,
checked: &CheckedModule,
) -> Result<(Type, Type)> {
if matches!(op, BinaryOp::Equal | BinaryOp::NotEqual) {
match (is_empty_list(left), is_empty_list(right)) {
(true, false) => {
let right_type = check_expr(file, right, env, checked)?;
let left_type = check_expr_with_hint(file, left, env, checked, Some(&right_type))?;
return Ok((left_type, right_type));
}
(false, true) => {
let left_type = check_expr(file, left, env, checked)?;
let right_type = check_expr_with_hint(file, right, env, checked, Some(&left_type))?;
return Ok((left_type, right_type));
}
_ => (),
}
}
let left_type = check_expr(file, left, env, checked)?;
let right_type = check_expr(file, right, env, checked)?;
Ok((left_type, right_type))
}
fn is_empty_list(expr: &Expr) -> bool {
matches!(&expr.kind, ExprKind::List(items) if items.is_empty())
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum CoverageMark {
CatchAll,
Unit,
Constructor(String),
Bool(bool),
Int(i64),
ByteArray(String),
String(String),
}
fn check_match(
file: &str,
span: Span,
subject: &Expr,
arms: &[AstMatchArm],
env: &HashMap<String, Type>,
checked: &CheckedModule,
expected: Option<&Type>,
) -> Result<Type> {
if arms.is_empty() {
return Err(AeriError::at_span(
file,
span,
"match expression needs at least one arm",
));
}
let subject_type = check_expr(file, subject, env, checked)?;
let mut result_type = None;
let mut catch_all_span: Option<Span> = None;
let mut covered_constructors = HashSet::new();
let mut covered_bools = HashSet::new();
let mut covered_ints = HashSet::new();
let mut covered_bytes = HashSet::new();
let mut covered_strings = HashSet::new();
let mut covered_unit = false;
for arm in arms {
if let Some(first_span) = catch_all_span {
return Err(AeriError::at_span(
file,
arm.pattern.span(),
format!(
"match arm is unreachable because a catch-all arm at line {} already covers remaining values",
first_span.line
),
));
}
let mut arm_env = env.clone();
match check_pattern(file, &arm.pattern, &subject_type, &mut arm_env, checked)? {
CoverageMark::CatchAll => {
if finite_subject_covered(
&subject_type,
&covered_constructors,
&covered_bools,
covered_unit,
checked,
) {
return Err(AeriError::at_span(
file,
arm.pattern.span(),
"match arm is unreachable because previous arms already cover all values",
));
}
catch_all_span = Some(arm.pattern.span());
}
CoverageMark::Unit => {
if covered_unit {
return Err(AeriError::at_span(
file,
arm.pattern.span(),
"duplicate match arm for '()'",
));
}
covered_unit = true;
}
CoverageMark::Constructor(name) => {
if !covered_constructors.insert(name.clone()) {
return Err(AeriError::at_span(
file,
arm.pattern.span(),
format!("duplicate match arm for constructor '{name}'"),
));
}
}
CoverageMark::Bool(value) => {
if !covered_bools.insert(value) {
return Err(AeriError::at_span(
file,
arm.pattern.span(),
format!("duplicate match arm for '{value}'"),
));
}
}
CoverageMark::Int(value) => {
if !covered_ints.insert(value) {
return Err(AeriError::at_span(
file,
arm.pattern.span(),
format!("duplicate match arm for '{value}'"),
));
}
}
CoverageMark::ByteArray(hex) => {
if !covered_bytes.insert(hex.clone()) {
return Err(AeriError::at_span(
file,
arm.pattern.span(),
format!("duplicate match arm for '#{hex}'"),
));
}
}
CoverageMark::String(value) => {
if !covered_strings.insert(value.clone()) {
return Err(AeriError::at_span(
file,
arm.pattern.span(),
format!("duplicate match arm for string '{value}'"),
));
}
}
}
let arm_type = check_block(file, &arm.body, &mut arm_env, checked, expected)?;
if let Some(expected) = &result_type {
if *expected == Type::Never {
result_type = Some(arm_type);
} else {
expect_type(file, arm.span, &arm_type, expected, "match arm")?;
}
} else {
result_type = Some(arm_type);
}
}
if catch_all_span.is_none() {
match &subject_type {
Type::Custom(type_name) => {
let known_variants = checked.type_variants.get(type_name).ok_or_else(|| {
AeriError::at_span(file, span, format!("unknown type '{type_name}'"))
})?;
let missing = known_variants
.iter()
.filter(|variant| !covered_constructors.contains(*variant))
.cloned()
.collect::<Vec<_>>();
if !missing.is_empty() {
return Err(AeriError::at_span(
file,
span,
format!("match is missing arm(s): {}", missing.join(", ")),
));
}
}
Type::Bool => {
if covered_bools.len() != 2 {
return Err(AeriError::at_span(
file,
span,
"match on Bool must cover true and false or include '_'",
));
}
}
Type::Unit => {
if !covered_unit {
return Err(AeriError::at_span(
file,
span,
"match on Unit must cover () or include '_'",
));
}
}
_ => {
return Err(AeriError::at_span(
file,
span,
format!("match on {subject_type} must include a '_' catch-all arm"),
));
}
}
}
Ok(result_type.expect("non-empty match produced a type"))
}
fn finite_subject_covered(
subject_type: &Type,
covered_constructors: &HashSet<String>,
covered_bools: &HashSet<bool>,
covered_unit: bool,
checked: &CheckedModule,
) -> bool {
match subject_type {
Type::Bool => covered_bools.len() == 2,
Type::Unit => covered_unit,
Type::Custom(type_name) => {
checked
.type_variants
.get(type_name)
.is_some_and(|known_variants| {
known_variants
.iter()
.all(|variant| covered_constructors.contains(variant))
})
}
_ => false,
}
}
fn check_pattern(
file: &str,
pattern: &AstPattern,
subject_type: &Type,
env: &mut HashMap<String, Type>,
checked: &CheckedModule,
) -> Result<CoverageMark> {
match pattern {
AstPattern::Wildcard { .. } => Ok(CoverageMark::CatchAll),
AstPattern::Variable { name, span } => {
ensure_fresh_binding(file, *span, env, name)?;
env.insert(name.clone(), subject_type.clone());
Ok(CoverageMark::CatchAll)
}
AstPattern::Constructor {
name,
bindings,
span,
} => {
let constructor = checked.constructors.get(name).ok_or_else(|| {
AeriError::at_span(file, *span, format!("unknown constructor '{name}'"))
})?;
match subject_type {
Type::Custom(type_name) if type_name == &constructor.type_name => (),
_ => {
return Err(AeriError::at_span(
file,
*span,
format!(
"constructor '{name}' matches {}, not {subject_type}",
constructor.type_name
),
));
}
}
if bindings.len() != constructor.fields.len() {
return Err(AeriError::at_span(
file,
*span,
format!(
"constructor '{name}' has {} field(s), pattern binds {}",
constructor.fields.len(),
bindings.len()
),
));
}
let mut seen = HashSet::new();
for (binding, ty) in bindings.iter().zip(constructor.fields.iter()) {
if let Some(name) = &binding.name {
if !seen.insert(name.clone()) {
return Err(AeriError::at_span(
file,
binding.span,
format!("duplicate binding '{name}' in pattern"),
));
}
ensure_fresh_binding(file, binding.span, env, name)?;
env.insert(name.clone(), ty.clone());
}
}
Ok(CoverageMark::Constructor(name.clone()))
}
AstPattern::Bool { value, span } => {
expect_type(file, *span, subject_type, &Type::Bool, "boolean pattern")?;
Ok(CoverageMark::Bool(*value))
}
AstPattern::Unit { span } => {
expect_type(file, *span, subject_type, &Type::Unit, "unit pattern")?;
Ok(CoverageMark::Unit)
}
AstPattern::Int { value, span } => {
expect_type(file, *span, subject_type, &Type::Int, "integer pattern")?;
Ok(CoverageMark::Int(*value))
}
AstPattern::ByteArray { hex, span } => {
expect_type(
file,
*span,
subject_type,
&Type::ByteArray,
"byte array pattern",
)?;
Ok(CoverageMark::ByteArray(hex.clone()))
}
AstPattern::String { value, span } => {
expect_type(file, *span, subject_type, &Type::String, "string pattern")?;
Ok(CoverageMark::String(value.clone()))
}
}
}
fn check_binary(file: &str, span: Span, op: BinaryOp, left: &Type, right: &Type) -> Result<Type> {
match op {
BinaryOp::Or | BinaryOp::And => {
expect_type(file, span, left, &Type::Bool, "left boolean operand")?;
expect_type(file, span, right, &Type::Bool, "right boolean operand")?;
Ok(Type::Bool)
}
BinaryOp::Equal | BinaryOp::NotEqual => {
if left != right {
return Err(AeriError::at_span(
file,
span,
format!("cannot compare {left} with {right}"),
));
}
if *left == Type::Unit {
return Err(AeriError::at_span(file, span, "cannot compare Unit values"));
}
if !is_equatable_type(left) {
return Err(AeriError::at_span(
file,
span,
format!("cannot compare {left} values"),
));
}
Ok(Type::Bool)
}
BinaryOp::Less | BinaryOp::LessEqual | BinaryOp::Greater | BinaryOp::GreaterEqual => {
expect_type(file, span, left, &Type::Int, "left comparison operand")?;
expect_type(file, span, right, &Type::Int, "right comparison operand")?;
Ok(Type::Bool)
}
BinaryOp::Add
| BinaryOp::Subtract
| BinaryOp::Multiply
| BinaryOp::Divide
| BinaryOp::Remainder => {
expect_type(file, span, left, &Type::Int, "left arithmetic operand")?;
expect_type(file, span, right, &Type::Int, "right arithmetic operand")?;
Ok(Type::Int)
}
}
}
fn check_call(
file: &str,
span: Span,
callee: &str,
args: &[Expr],
env: &HashMap<String, Type>,
checked: &CheckedModule,
) -> Result<Type> {
if env.contains_key(callee) {
return Err(AeriError::at_span(
file,
span,
format!("local binding '{callee}' is not callable"),
));
}
if let Some(signature) = builtin_signature(callee) {
check_arity(file, span, callee, signature.params.len(), args.len())?;
for (index, (arg, expected)) in args.iter().zip(signature.params.iter()).enumerate() {
let actual = check_expr_with_hint(file, arg, env, checked, Some(expected))?;
expect_type(
file,
arg.span,
&actual,
expected,
&format!("argument {} to '{callee}'", index + 1),
)?;
}
return Ok(signature.result);
}
if callee == "list_has" {
let item_type = check_generic_list_has_call(file, span, args, env, checked)?;
if generic_list_has_builtin(&item_type, checked).is_some()
|| direct_constructor_list_has_supported(args, &item_type, checked, env)
{
return Ok(Type::Bool);
}
return Err(AeriError::at_span(
file,
args[0].span,
format!(
"list_has supports primitive, nullary custom, or direct constructor list literal item types today, found List<{item_type}>"
),
));
}
if callee == "list_len" {
check_arity(file, span, callee, 1, args.len())?;
if is_empty_list(&args[0]) {
return Ok(Type::Int);
}
let actual = check_expr(file, &args[0], env, checked)?;
if matches!(actual, Type::List(_)) {
return Ok(Type::Int);
}
return Err(AeriError::at_span(
file,
args[0].span,
format!("expected List<T> for argument 1 to 'list_len', found {actual}"),
));
}
if let Some(constructor) = checked.constructors.get(callee) {
check_arity(file, span, callee, constructor.fields.len(), args.len())?;
for (index, (arg, expected)) in args.iter().zip(constructor.fields.iter()).enumerate() {
let actual = check_expr_with_hint(file, arg, env, checked, Some(expected))?;
expect_type(
file,
arg.span,
&actual,
expected,
&format!("field {} to '{callee}'", index + 1),
)?;
}
return Ok(Type::Custom(constructor.type_name.clone()));
}
let signature = checked
.functions
.get(callee)
.ok_or_else(|| AeriError::at_span(file, span, format!("unknown function '{callee}'")))?;
check_arity(file, span, callee, signature.params.len(), args.len())?;
for (index, (arg, expected)) in args.iter().zip(signature.params.iter()).enumerate() {
let actual = check_expr_with_hint(file, arg, env, checked, Some(expected))?;
expect_type(
file,
arg.span,
&actual,
expected,
&format!("argument {} to '{callee}'", index + 1),
)?;
}
Ok(signature.result.clone())
}
fn check_arity(file: &str, span: Span, callee: &str, expected: usize, actual: usize) -> Result<()> {
if expected == actual {
return Ok(());
}
Err(AeriError::at_span(
file,
span,
format!("'{callee}' expects {expected} argument(s), found {actual}"),
))
}
fn is_equatable_type(ty: &Type) -> bool {
!matches!(ty, Type::Tx | Type::Unit | Type::Never)
}
fn starts_with_uppercase(name: &str) -> bool {
name.chars()
.next()
.is_some_and(|ch| ch.is_ascii_uppercase())
}
fn env_from_params(
file: &str,
params: &[Param],
type_names: &HashSet<String>,
) -> Result<HashMap<String, Type>> {
Ok(param_types(file, params, type_names)?.into_iter().collect())
}
fn param_types(
file: &str,
params: &[Param],
type_names: &HashSet<String>,
) -> Result<Vec<(String, Type)>> {
let mut seen = HashSet::new();
let mut typed = Vec::with_capacity(params.len());
for param in params {
if !seen.insert(param.name.clone()) {
return Err(AeriError::at_span(
file,
param.span,
format!("duplicate parameter '{}'", param.name),
));
}
typed.push((
param.name.clone(),
Type::from_ref(file, ¶m.ty, type_names)?,
));
}
Ok(typed)
}
fn expect_type(
file: &str,
span: Span,
actual: &Type,
expected: &Type,
context: &str,
) -> Result<()> {
if actual == expected || *actual == Type::Never {
Ok(())
} else {
Err(AeriError::at_span(
file,
span,
format!("expected {expected} for {context}, found {actual}"),
))
}
}
fn ensure_fresh_binding(
file: &str,
span: Span,
env: &HashMap<String, Type>,
name: &str,
) -> Result<()> {
if env.contains_key(name) {
Err(AeriError::at_span(
file,
span,
format!("binding '{name}' already exists in this scope"),
))
} else {
Ok(())
}
}
fn lower_functions<'a>(
file: &str,
module: &'a Module,
checked: &CheckedModule,
) -> Result<Vec<FunctionArtifact<'a>>> {
module
.items
.iter()
.filter_map(|item| match item {
Item::Function(function) => Some(function),
Item::Type(_) | Item::Const(_) | Item::Validator(_) | Item::Test(_) => None,
})
.map(|function| {
let env = env_from_params(file, &function.params, &checked.type_names)?;
Ok(FunctionArtifact {
function,
program: Program::new(wrap_params(
lower_block(file, &function.body, checked, env)?,
function.params.iter().map(|param| param.name.clone()),
)),
})
})
.collect()
}
fn lower_constants<'a>(
file: &str,
module: &'a Module,
checked: &CheckedModule,
) -> Result<Vec<ConstArtifact<'a>>> {
module
.items
.iter()
.filter_map(|item| match item {
Item::Const(constant) => Some(constant),
Item::Type(_) | Item::Function(_) | Item::Validator(_) | Item::Test(_) => None,
})
.map(|constant| {
Ok(ConstArtifact {
constant,
program: Program::new(lower_expr(file, &constant.value, checked)?),
})
})
.collect()
}
fn lower_validators<'a>(
file: &str,
module: &'a Module,
checked: &CheckedModule,
) -> Result<Vec<ValidatorArtifact<'a>>> {
module
.items
.iter()
.filter_map(|item| match item {
Item::Validator(validator) => Some(validator),
Item::Type(_) | Item::Const(_) | Item::Function(_) | Item::Test(_) => None,
})
.map(|validator| {
let param_types = param_types(file, &validator.params, &checked.type_names)?;
let env = param_types.iter().cloned().collect();
let program = Program::new(wrap_params(
lower_block(file, &validator.body, checked, env)?,
validator.params.iter().map(|param| param.name.clone()),
));
Ok(ValidatorArtifact {
validator,
param_types,
program,
})
})
.collect()
}
fn collect_lowered_validators(
module: &Module,
checked: &CheckedModule,
validators: &[ValidatorArtifact<'_>],
) -> Vec<LoweredValidator> {
let custom_types = custom_type_layouts(checked);
validators
.iter()
.map(|artifact| LoweredValidator {
title: format!("{}.{}", module.name, artifact.validator.name),
script_purpose: script_purpose_for_params(artifact.param_types.len()).to_string(),
param_types: artifact.param_types.clone(),
custom_types: custom_types.clone(),
program: artifact.program.clone(),
})
.collect()
}
fn custom_type_layouts(checked: &CheckedModule) -> Vec<CustomTypeLayout> {
let mut type_names = checked.type_variants.keys().cloned().collect::<Vec<_>>();
type_names.sort();
type_names
.into_iter()
.map(|name| {
let variants = checked
.type_variants
.get(&name)
.expect("type name comes from type_variants keys")
.iter()
.map(|variant_name| {
let constructor = checked
.constructors
.get(variant_name)
.expect("type variant names have constructor signatures");
CustomTypeVariantLayout {
tag: constructor.tag,
field_count: constructor.fields.len(),
}
})
.collect();
CustomTypeLayout { name, variants }
})
.collect()
}
fn script_purpose_for_params(param_count: usize) -> &'static str {
if param_count == 2 { "mint" } else { "spend" }
}
fn lower_tests<'a>(
file: &str,
module: &'a Module,
checked: &CheckedModule,
) -> Result<Vec<TestArtifact<'a>>> {
module
.items
.iter()
.filter_map(|item| match item {
Item::Test(test) => Some(test),
Item::Type(_) | Item::Const(_) | Item::Function(_) | Item::Validator(_) => None,
})
.map(|test| {
Ok(TestArtifact {
test,
program: Program::new(lower_block(file, &test.body, checked, HashMap::new())?),
})
})
.collect()
}
fn lower_block(
file: &str,
block: &Block,
checked: &CheckedModule,
env: HashMap<String, Type>,
) -> Result<Term> {
let mut inlining = HashSet::new();
lower_block_inner(file, block, checked, &env, &mut inlining)
}
fn lower_block_inner(
file: &str,
block: &Block,
checked: &CheckedModule,
env: &HashMap<String, Type>,
inlining: &mut HashSet<String>,
) -> Result<Term> {
lower_statements(file, &block.statements, 0, checked, env, inlining)
}
fn lower_statements(
file: &str,
statements: &[Statement],
index: usize,
checked: &CheckedModule,
env: &HashMap<String, Type>,
inlining: &mut HashSet<String>,
) -> Result<Term> {
let Some(statement) = statements.get(index) else {
return Ok(Term::Unit);
};
let is_last = index + 1 == statements.len();
match statement {
Statement::Let {
name, ty, value, ..
} => {
let value_term = lower_expr_inner(file, value, checked, env, inlining)?;
let binding_type = if let Some(ty) = ty {
Type::from_ref(file, ty, &checked.type_names)?
} else {
check_expr(file, value, env, checked)?
};
let mut body_env = env.clone();
body_env.insert(name.clone(), binding_type);
let body = lower_statements(file, statements, index + 1, checked, &body_env, inlining)?;
Ok(Term::Let {
name: name.clone(),
value: Box::new(value_term),
body: Box::new(body),
})
}
Statement::Require { condition, .. } => {
let condition = lower_expr_inner(file, condition, checked, env, inlining)?;
let then_term = lower_statements(file, statements, index + 1, checked, env, inlining)?;
Ok(Term::If {
condition: Box::new(condition),
then_term: Box::new(then_term),
else_term: Box::new(Term::Error("require failed".to_string())),
})
}
Statement::Trace { message, .. } => {
let message = lower_expr_inner(file, message, checked, env, inlining)?;
let then_term = lower_statements(file, statements, index + 1, checked, env, inlining)?;
Ok(Term::Trace {
message: Box::new(message),
then_term: Box::new(then_term),
})
}
Statement::Return { value, .. } => lower_expr_inner(file, value, checked, env, inlining),
Statement::Expr {
value,
trailing_semicolon,
..
} if is_last && !trailing_semicolon => {
lower_expr_inner(file, value, checked, env, inlining)
}
Statement::Expr { value, .. } => {
let first = lower_expr_inner(file, value, checked, env, inlining)?;
let then_term = lower_statements(file, statements, index + 1, checked, env, inlining)?;
Ok(Term::Sequence {
first: Box::new(first),
then_term: Box::new(then_term),
})
}
}
}
fn lower_expr(file: &str, expr: &Expr, checked: &CheckedModule) -> Result<Term> {
let mut inlining = HashSet::new();
let env = HashMap::new();
lower_expr_inner(file, expr, checked, &env, &mut inlining)
}
fn lower_expr_inner(
file: &str,
expr: &Expr,
checked: &CheckedModule,
env: &HashMap<String, Type>,
inlining: &mut HashSet<String>,
) -> Result<Term> {
match &expr.kind {
ExprKind::Bool(value) => Ok(Term::Bool(*value)),
ExprKind::Int(value) => Ok(Term::Int(*value)),
ExprKind::String(value) => Ok(Term::String(value.clone())),
ExprKind::ByteArray(hex) => Ok(Term::ByteArray(hex.clone())),
ExprKind::Unit => Ok(Term::Unit),
ExprKind::Fail => Ok(Term::Error("fail".to_string())),
ExprKind::List(items) => Ok(Term::List(
items
.iter()
.map(|item| lower_expr_inner(file, item, checked, env, inlining))
.collect::<Result<Vec<_>>>()?,
)),
ExprKind::Variable(name) => {
if env.contains_key(name) {
Ok(Term::Var(name.clone()))
} else if let Some(constant) = checked.constants.get(name) {
if inlining.insert(name.clone()) {
let constant_env = HashMap::new();
let term =
lower_expr_inner(file, &constant.value, checked, &constant_env, inlining)?;
inlining.remove(name);
Ok(term)
} else {
Ok(Term::Var(name.clone()))
}
} else {
Ok(checked
.constructors
.get(name)
.filter(|constructor| constructor.fields.is_empty())
.map_or_else(
|| Term::Var(name.clone()),
|constructor| Term::Constr {
name: name.clone(),
tag: constructor.tag,
fields: Vec::new(),
},
))
}
}
ExprKind::Unary { op, expr } => match op {
UnaryOp::Not => Ok(Term::BuiltinCall {
name: "not".to_string(),
args: vec![lower_expr_inner(file, expr, checked, env, inlining)?],
}),
UnaryOp::Negate => Ok(Term::BuiltinCall {
name: "negateInteger".to_string(),
args: vec![lower_expr_inner(file, expr, checked, env, inlining)?],
}),
},
ExprKind::Binary { left, op, right } => match op {
BinaryOp::And => Ok(Term::If {
condition: Box::new(lower_expr_inner(file, left, checked, env, inlining)?),
then_term: Box::new(lower_expr_inner(file, right, checked, env, inlining)?),
else_term: Box::new(Term::Bool(false)),
}),
BinaryOp::Or => Ok(Term::If {
condition: Box::new(lower_expr_inner(file, left, checked, env, inlining)?),
then_term: Box::new(Term::Bool(true)),
else_term: Box::new(lower_expr_inner(file, right, checked, env, inlining)?),
}),
BinaryOp::Equal | BinaryOp::NotEqual => {
if is_empty_list(left) && is_empty_list(right) {
return Ok(Term::Bool(matches!(op, BinaryOp::Equal)));
}
let (left_type, _) = check_binary_operands(file, left, *op, right, env, checked)?;
if let Some(term) = lower_direct_constructor_list_equality(
DirectConstructorListEquality {
file,
left,
right,
equal: matches!(op, BinaryOp::Equal),
ty: &left_type,
checked,
env,
},
inlining,
)? {
return Ok(term);
}
if let Some(term) = lower_direct_constructor_equality(
file,
left,
right,
matches!(op, BinaryOp::Equal),
checked,
env,
inlining,
)? {
return Ok(term);
}
let name = equality_builtin(*op, &left_type, checked);
Ok(Term::BuiltinCall {
name: name.to_string(),
args: vec![
lower_expr_inner(file, left, checked, env, inlining)?,
lower_expr_inner(file, right, checked, env, inlining)?,
],
})
}
op => {
let name = binary_builtin(*op);
Ok(Term::BuiltinCall {
name: name.to_string(),
args: vec![
lower_expr_inner(file, left, checked, env, inlining)?,
lower_expr_inner(file, right, checked, env, inlining)?,
],
})
}
},
ExprKind::Call { callee, args } => {
if callee == "list_has" {
let item_type = check_generic_list_has_call(file, expr.span, args, env, checked)?;
if let Some(term) = lower_direct_constructor_list_has(
file, args, &item_type, checked, env, inlining,
)? {
return Ok(term);
}
if let Some(name) = generic_list_has_builtin(&item_type, checked) {
let lowered_args = args
.iter()
.map(|arg| lower_expr_inner(file, arg, checked, env, inlining))
.collect::<Result<Vec<_>>>()?;
return Ok(Term::BuiltinCall {
name: name.to_string(),
args: lowered_args,
});
}
return Ok(Term::Error(format!(
"unsupported list_has item type {item_type}"
)));
}
let lowered_args = args
.iter()
.map(|arg| lower_expr_inner(file, arg, checked, env, inlining))
.collect::<Result<Vec<_>>>()?;
if builtin_signature(callee).is_some() || callee == "list_len" {
Ok(Term::BuiltinCall {
name: callee.clone(),
args: lowered_args,
})
} else if let Some(constructor) = checked.constructors.get(callee) {
Ok(Term::Constr {
name: callee.clone(),
tag: constructor.tag,
fields: lowered_args,
})
} else if let Some(function) = checked.function_defs.get(callee) {
inline_function_call(file, function, lowered_args, checked, inlining)
} else {
Ok(Term::Error(format!("unresolved call to {callee}")))
}
}
ExprKind::If {
condition,
then_branch,
else_branch,
} => Ok(Term::If {
condition: Box::new(lower_expr_inner(file, condition, checked, env, inlining)?),
then_term: Box::new(lower_block_inner(
file,
then_branch,
checked,
env,
inlining,
)?),
else_term: Box::new(lower_block_inner(
file,
else_branch,
checked,
env,
inlining,
)?),
}),
ExprKind::Match { subject, arms } => {
let subject_type = check_expr(file, subject, env, checked)?;
let subject = lower_expr_inner(file, subject, checked, env, inlining)?;
let mut arms = arms
.iter()
.map(|arm| {
let mut arm_env = env.clone();
collect_typed_pattern_bindings(
&arm.pattern,
&subject_type,
checked,
&mut arm_env,
);
Ok(IrMatchArm {
pattern: lower_pattern(&arm.pattern, checked),
body: lower_block_inner(file, &arm.body, checked, &arm_env, inlining)?,
})
})
.collect::<Result<Vec<_>>>()?;
add_custom_match_fallback(&subject_type, &mut arms);
Ok(Term::Match {
subject: Box::new(subject),
arms,
})
}
}
}
fn check_generic_list_has_call(
file: &str,
span: Span,
args: &[Expr],
env: &HashMap<String, Type>,
checked: &CheckedModule,
) -> Result<Type> {
check_arity(file, span, "list_has", 2, args.len())?;
let item_type = if is_empty_list(&args[0]) {
check_expr(file, &args[1], env, checked)?
} else {
match check_expr(file, &args[0], env, checked)? {
Type::List(item) => *item,
actual => {
return Err(AeriError::at_span(
file,
args[0].span,
format!("expected List<T> for argument 1 to 'list_has', found {actual}"),
));
}
}
};
let expected_list = Type::List(Box::new(item_type.clone()));
let actual_list = check_expr_with_hint(file, &args[0], env, checked, Some(&expected_list))?;
expect_type(
file,
args[0].span,
&actual_list,
&expected_list,
"argument 1 to 'list_has'",
)?;
let actual_item = check_expr_with_hint(file, &args[1], env, checked, Some(&item_type))?;
expect_type(
file,
args[1].span,
&actual_item,
&item_type,
"argument 2 to 'list_has'",
)?;
Ok(item_type)
}
fn generic_list_has_builtin(item_type: &Type, checked: &CheckedModule) -> Option<&'static str> {
Some(match item_type {
Type::Bool => "list_has_bool",
Type::Int => "list_has_int",
Type::ByteArray => "list_has_bytes",
Type::Data => "list_has_data",
Type::String => "list_has_string",
Type::Unit => "list_has_unit",
Type::Custom(name) if custom_type_has_only_nullary_constructors(name, checked) => {
"list_has_custom_tag"
}
Type::Tx | Type::List(_) | Type::Custom(_) | Type::Never => return None,
})
}
fn direct_constructor_list_has_supported(
args: &[Expr],
item_type: &Type,
checked: &CheckedModule,
env: &HashMap<String, Type>,
) -> bool {
direct_constructor_list_has_parts(args, item_type, checked, env).is_some()
|| empty_direct_constructor_list_has_parts(args, item_type, checked, env).is_some()
}
fn direct_constructor_list_has_parts<'c, 'e, 't>(
args: &'e [Expr],
item_type: &'t Type,
checked: &'c CheckedModule,
env: &HashMap<String, Type>,
) -> Option<(&'t str, &'e [Expr], DirectConstructorExpr<'c, 'e>)> {
let Type::Custom(type_name) = item_type else {
return None;
};
let type_name = type_name.as_str();
if custom_type_has_only_nullary_constructors(type_name, checked) {
return None;
}
let [list, needle] = args else {
return None;
};
let ExprKind::List(items) = &list.kind else {
return None;
};
if items.is_empty() {
return None;
}
let needle_constructor = direct_constructor_expr(needle, checked, env)?;
if needle_constructor.signature.type_name != type_name
|| !direct_constructor_expr_supported(&needle_constructor, checked, env)
{
return None;
}
let all_items_supported = items.iter().all(|item| {
let Some(constructor) = direct_constructor_expr(item, checked, env) else {
return false;
};
constructor.signature.type_name == type_name
&& direct_constructor_expr_supported(&constructor, checked, env)
});
if !all_items_supported {
return None;
}
Some((type_name, items, needle_constructor))
}
fn empty_direct_constructor_list_has_parts<'c, 'e>(
args: &'e [Expr],
item_type: &Type,
checked: &'c CheckedModule,
env: &HashMap<String, Type>,
) -> Option<DirectConstructorExpr<'c, 'e>> {
let Type::Custom(type_name) = item_type else {
return None;
};
let type_name = type_name.as_str();
if custom_type_has_only_nullary_constructors(type_name, checked) {
return None;
}
let [list, needle] = args else {
return None;
};
let ExprKind::List(items) = &list.kind else {
return None;
};
if !items.is_empty() {
return None;
}
let needle_constructor = direct_constructor_expr(needle, checked, env)?;
if needle_constructor.signature.type_name != type_name
|| !direct_constructor_expr_supported(&needle_constructor, checked, env)
{
return None;
}
Some(needle_constructor)
}
struct DirectConstructorExpr<'c, 'e> {
signature: &'c ConstructorSignature,
args: &'e [Expr],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DirectFieldEquality {
Builtin(&'static str),
AlwaysTrue,
}
struct DirectConstructorBindingState {
names: HashSet<String>,
bindings: Vec<(String, Term)>,
}
struct LoweredDirectConstructorElement<'a> {
signature: &'a ConstructorSignature,
fields: Vec<LoweredDirectConstructorField<'a>>,
}
enum LoweredDirectConstructorField<'a> {
Value(Term),
Constructor(LoweredDirectConstructorElement<'a>),
}
struct DirectConstructorListEquality<'a> {
file: &'a str,
left: &'a Expr,
right: &'a Expr,
equal: bool,
ty: &'a Type,
checked: &'a CheckedModule,
env: &'a HashMap<String, Type>,
}
fn lower_direct_constructor_list_equality(
input: DirectConstructorListEquality<'_>,
inlining: &mut HashSet<String>,
) -> Result<Option<Term>> {
let Type::List(item_type) = input.ty else {
return Ok(None);
};
let Type::Custom(type_name) = item_type.as_ref() else {
return Ok(None);
};
if custom_type_has_only_nullary_constructors(type_name, input.checked) {
return Ok(None);
}
let (ExprKind::List(left_items), ExprKind::List(right_items)) =
(&input.left.kind, &input.right.kind)
else {
return Ok(None);
};
let mut binding_state = DirectConstructorBindingState {
names: input.env.keys().cloned().collect(),
bindings: Vec::new(),
};
let Some(left_elements) = lower_direct_constructor_list_items(
input.file,
left_items,
type_name,
input.checked,
input.env,
inlining,
&mut binding_state,
)?
else {
return Ok(None);
};
let Some(right_elements) = lower_direct_constructor_list_items(
input.file,
right_items,
type_name,
input.checked,
input.env,
inlining,
&mut binding_state,
)?
else {
return Ok(None);
};
let mut body =
lower_direct_constructor_list_equality_body(left_elements, right_elements, input.checked);
if !input.equal {
body = invert_bool_term(body);
}
Ok(Some(bind_ir_terms_in_order(binding_state.bindings, body)))
}
fn lower_direct_constructor_list_has(
file: &str,
args: &[Expr],
item_type: &Type,
checked: &CheckedModule,
env: &HashMap<String, Type>,
inlining: &mut HashSet<String>,
) -> Result<Option<Term>> {
let Some((type_name, items, needle_constructor)) =
direct_constructor_list_has_parts(args, item_type, checked, env)
else {
return lower_empty_direct_constructor_list_has(
file, args, item_type, checked, env, inlining,
);
};
let mut binding_state = DirectConstructorBindingState {
names: env.keys().cloned().collect(),
bindings: Vec::new(),
};
let Some(elements) = lower_direct_constructor_list_has_items(
file,
items,
type_name,
checked,
env,
inlining,
&mut binding_state,
)?
else {
return Ok(None);
};
let Some(needle) = lower_and_bind_direct_constructor(
file,
needle_constructor,
checked,
env,
inlining,
&mut binding_state,
"aeri_list_has_needle_field",
)?
else {
return Ok(None);
};
let body = lower_direct_constructor_list_has_body(elements, needle, checked);
Ok(Some(bind_ir_terms_in_order(binding_state.bindings, body)))
}
fn lower_empty_direct_constructor_list_has(
file: &str,
args: &[Expr],
item_type: &Type,
checked: &CheckedModule,
env: &HashMap<String, Type>,
inlining: &mut HashSet<String>,
) -> Result<Option<Term>> {
let Some(needle_constructor) =
empty_direct_constructor_list_has_parts(args, item_type, checked, env)
else {
return Ok(None);
};
let mut binding_state = DirectConstructorBindingState {
names: env.keys().cloned().collect(),
bindings: Vec::new(),
};
let Some(_) = lower_and_bind_direct_constructor(
file,
needle_constructor,
checked,
env,
inlining,
&mut binding_state,
"aeri_empty_list_has_needle_field",
)?
else {
return Ok(None);
};
Ok(Some(bind_ir_terms_in_order(
binding_state.bindings,
Term::Bool(false),
)))
}
fn lower_direct_constructor_list_items<'a>(
file: &str,
items: &[Expr],
type_name: &str,
checked: &'a CheckedModule,
env: &HashMap<String, Type>,
inlining: &mut HashSet<String>,
state: &mut DirectConstructorBindingState,
) -> Result<Option<Vec<LoweredDirectConstructorElement<'a>>>> {
let mut lowered = Vec::with_capacity(items.len());
for item in items {
let Some(constructor) = direct_constructor_expr(item, checked, env) else {
return Ok(None);
};
if constructor.signature.type_name != type_name {
return Ok(None);
}
let Some(element) = lower_and_bind_direct_constructor(
file,
constructor,
checked,
env,
inlining,
state,
"aeri_list_constructor_field",
)?
else {
return Ok(None);
};
lowered.push(element);
}
Ok(Some(lowered))
}
fn lower_direct_constructor_list_has_items<'a>(
file: &str,
items: &[Expr],
type_name: &str,
checked: &'a CheckedModule,
env: &HashMap<String, Type>,
inlining: &mut HashSet<String>,
state: &mut DirectConstructorBindingState,
) -> Result<Option<Vec<LoweredDirectConstructorElement<'a>>>> {
let mut lowered = Vec::with_capacity(items.len());
for item in items {
let Some(constructor) = direct_constructor_expr(item, checked, env) else {
return Ok(None);
};
if constructor.signature.type_name != type_name {
return Ok(None);
}
let Some(element) = lower_and_bind_direct_constructor(
file,
constructor,
checked,
env,
inlining,
state,
"aeri_list_has_constructor_field",
)?
else {
return Ok(None);
};
lowered.push(element);
}
Ok(Some(lowered))
}
fn lower_direct_constructor_list_has_body(
items: Vec<LoweredDirectConstructorElement<'_>>,
needle: LoweredDirectConstructorElement<'_>,
checked: &CheckedModule,
) -> Term {
items
.iter()
.rev()
.fold(Term::Bool(false), |body, item| Term::If {
condition: Box::new(lower_direct_constructor_equality_body(
item, &needle, checked,
)),
then_term: Box::new(Term::Bool(true)),
else_term: Box::new(body),
})
}
fn lower_direct_constructor_list_equality_body(
left: Vec<LoweredDirectConstructorElement<'_>>,
right: Vec<LoweredDirectConstructorElement<'_>>,
checked: &CheckedModule,
) -> Term {
if left.len() != right.len() {
return Term::Bool(false);
}
left.iter()
.zip(&right)
.rev()
.fold(Term::Bool(true), |body, (left, right)| Term::If {
condition: Box::new(lower_direct_constructor_equality_body(left, right, checked)),
then_term: Box::new(body),
else_term: Box::new(Term::Bool(false)),
})
}
fn lower_direct_constructor_equality(
file: &str,
left: &Expr,
right: &Expr,
equal: bool,
checked: &CheckedModule,
env: &HashMap<String, Type>,
inlining: &mut HashSet<String>,
) -> Result<Option<Term>> {
let Some(left_constructor) = direct_constructor_expr(left, checked, env) else {
return Ok(None);
};
let Some(right_constructor) = direct_constructor_expr(right, checked, env) else {
return Ok(None);
};
if left_constructor.signature.type_name != right_constructor.signature.type_name {
return Ok(None);
}
if left_constructor.signature.fields.is_empty() && right_constructor.signature.fields.is_empty()
{
return Ok(None);
}
if !direct_constructor_expr_supported(&left_constructor, checked, env)
|| !direct_constructor_expr_supported(&right_constructor, checked, env)
{
return Ok(None);
}
let mut binding_state = DirectConstructorBindingState {
names: env.keys().cloned().collect(),
bindings: Vec::new(),
};
let Some(left_element) = lower_and_bind_direct_constructor(
file,
left_constructor,
checked,
env,
inlining,
&mut binding_state,
"aeri_eq_left",
)?
else {
return Ok(None);
};
let Some(right_element) = lower_and_bind_direct_constructor(
file,
right_constructor,
checked,
env,
inlining,
&mut binding_state,
"aeri_eq_right",
)?
else {
return Ok(None);
};
let mut body = lower_direct_constructor_equality_body(&left_element, &right_element, checked);
if !equal {
body = invert_bool_term(body);
}
Ok(Some(bind_ir_terms_in_order(binding_state.bindings, body)))
}
fn direct_constructor_expr<'c, 'e>(
expr: &'e Expr,
checked: &'c CheckedModule,
env: &HashMap<String, Type>,
) -> Option<DirectConstructorExpr<'c, 'e>> {
let shadows_constructor = match &expr.kind {
ExprKind::Call { callee, .. } => env.contains_key(callee),
ExprKind::Variable(name) => env.contains_key(name),
ExprKind::Bool(_)
| ExprKind::Int(_)
| ExprKind::String(_)
| ExprKind::ByteArray(_)
| ExprKind::Unit
| ExprKind::List(_)
| ExprKind::Fail
| ExprKind::Unary { .. }
| ExprKind::Binary { .. }
| ExprKind::If { .. }
| ExprKind::Match { .. } => false,
};
if shadows_constructor {
return None;
}
match &expr.kind {
ExprKind::Call { callee, args } => checked
.constructors
.get(callee)
.map(|signature| DirectConstructorExpr { signature, args }),
ExprKind::Variable(name) => checked
.constructors
.get(name)
.filter(|signature| signature.fields.is_empty())
.map(|signature| DirectConstructorExpr {
signature,
args: &[],
}),
ExprKind::Bool(_)
| ExprKind::Int(_)
| ExprKind::String(_)
| ExprKind::ByteArray(_)
| ExprKind::Unit
| ExprKind::List(_)
| ExprKind::Fail
| ExprKind::Unary { .. }
| ExprKind::Binary { .. }
| ExprKind::If { .. }
| ExprKind::Match { .. } => None,
}
}
fn direct_constructor_expr_supported(
constructor: &DirectConstructorExpr<'_, '_>,
checked: &CheckedModule,
env: &HashMap<String, Type>,
) -> bool {
constructor.signature.fields.len() == constructor.args.len()
&& constructor
.signature
.fields
.iter()
.zip(constructor.args)
.all(|(field_type, field)| {
if direct_field_equality(field_type, checked).is_some() {
return true;
}
let Type::Custom(type_name) = field_type else {
return false;
};
let Some(nested) = direct_constructor_expr(field, checked, env) else {
return false;
};
nested.signature.type_name == *type_name
&& direct_constructor_expr_supported(&nested, checked, env)
})
}
fn direct_field_equality(ty: &Type, checked: &CheckedModule) -> Option<DirectFieldEquality> {
Some(match ty {
Type::Bool => DirectFieldEquality::Builtin("equalsBool"),
Type::Int => DirectFieldEquality::Builtin("equalsInteger"),
Type::ByteArray => DirectFieldEquality::Builtin("equalsByteString"),
Type::Data => DirectFieldEquality::Builtin("equalsData"),
Type::String => DirectFieldEquality::Builtin("equalsString"),
Type::Unit => DirectFieldEquality::AlwaysTrue,
Type::Custom(name) if custom_type_has_only_nullary_constructors(name, checked) => {
DirectFieldEquality::Builtin("equalsCustomTag")
}
Type::List(_) | Type::Custom(_) | Type::Tx | Type::Never => return None,
})
}
fn lower_and_bind_direct_constructor<'c>(
file: &str,
constructor: DirectConstructorExpr<'c, '_>,
checked: &'c CheckedModule,
env: &HashMap<String, Type>,
inlining: &mut HashSet<String>,
state: &mut DirectConstructorBindingState,
base: &str,
) -> Result<Option<LoweredDirectConstructorElement<'c>>> {
if constructor.signature.fields.len() != constructor.args.len() {
return Ok(None);
}
let mut fields = Vec::with_capacity(constructor.args.len());
for (field_type, field) in constructor.signature.fields.iter().zip(constructor.args) {
if direct_field_equality(field_type, checked).is_some() {
let value = lower_expr_inner(file, field, checked, env, inlining)?;
collect_ir_names(&value, &mut state.names);
let name = fresh_ir_name(base, &state.names);
state.names.insert(name.clone());
state.bindings.push((name.clone(), value));
fields.push(LoweredDirectConstructorField::Value(Term::Var(name)));
continue;
}
let Type::Custom(type_name) = field_type else {
return Ok(None);
};
let Some(nested) = direct_constructor_expr(field, checked, env) else {
return Ok(None);
};
if nested.signature.type_name != *type_name {
return Ok(None);
}
let Some(nested) =
lower_and_bind_direct_constructor(file, nested, checked, env, inlining, state, base)?
else {
return Ok(None);
};
fields.push(LoweredDirectConstructorField::Constructor(nested));
}
Ok(Some(LoweredDirectConstructorElement {
signature: constructor.signature,
fields,
}))
}
fn lower_direct_constructor_equality_body(
left: &LoweredDirectConstructorElement<'_>,
right: &LoweredDirectConstructorElement<'_>,
checked: &CheckedModule,
) -> Term {
if left.signature.tag != right.signature.tag {
return Term::Bool(false);
}
left.signature
.fields
.iter()
.zip(left.fields.iter().zip(&right.fields))
.rev()
.fold(
Term::Bool(true),
|body, (field_type, (left, right))| match (left, right) {
(
LoweredDirectConstructorField::Value(left),
LoweredDirectConstructorField::Value(right),
) => match direct_field_equality(field_type, checked)
.expect("direct constructor leaf fields are checked before lowering")
{
DirectFieldEquality::AlwaysTrue => body,
DirectFieldEquality::Builtin(name) => Term::If {
condition: Box::new(Term::BuiltinCall {
name: name.to_string(),
args: vec![left.clone(), right.clone()],
}),
then_term: Box::new(body),
else_term: Box::new(Term::Bool(false)),
},
},
(
LoweredDirectConstructorField::Constructor(left),
LoweredDirectConstructorField::Constructor(right),
) => Term::If {
condition: Box::new(lower_direct_constructor_equality_body(
left, right, checked,
)),
then_term: Box::new(body),
else_term: Box::new(Term::Bool(false)),
},
_ => unreachable!("direct constructor field shapes match their declared type"),
},
)
}
fn invert_bool_term(term: Term) -> Term {
Term::If {
condition: Box::new(term),
then_term: Box::new(Term::Bool(false)),
else_term: Box::new(Term::Bool(true)),
}
}
fn bind_ir_terms_in_order(bindings: Vec<(String, Term)>, body: Term) -> Term {
bindings
.into_iter()
.rev()
.fold(body, |body, (name, value)| Term::Let {
name,
value: Box::new(value),
body: Box::new(body),
})
}
fn fresh_ir_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_ir_names(term: &Term, names: &mut HashSet<String>) {
match term {
Term::Var(name) => {
names.insert(name.clone());
}
Term::Bool(_)
| Term::Int(_)
| Term::String(_)
| Term::ByteArray(_)
| Term::Unit
| Term::Error(_) => (),
Term::List(items) => {
for item in items {
collect_ir_names(item, names);
}
}
Term::Lambda { param, body } => {
names.insert(param.clone());
collect_ir_names(body, names);
}
Term::Let { name, value, body } => {
names.insert(name.clone());
collect_ir_names(value, names);
collect_ir_names(body, names);
}
Term::If {
condition,
then_term,
else_term,
} => {
collect_ir_names(condition, names);
collect_ir_names(then_term, names);
collect_ir_names(else_term, names);
}
Term::BuiltinCall { args, .. } => {
for arg in args {
collect_ir_names(arg, names);
}
}
Term::Constr { name, fields, .. } => {
names.insert(name.clone());
for field in fields {
collect_ir_names(field, names);
}
}
Term::Match { subject, arms } => {
collect_ir_names(subject, names);
for arm in arms {
collect_ir_pattern_names(&arm.pattern, names);
collect_ir_names(&arm.body, names);
}
}
Term::Trace { message, then_term } => {
collect_ir_names(message, names);
collect_ir_names(then_term, names);
}
Term::Sequence { first, then_term } => {
collect_ir_names(first, names);
collect_ir_names(then_term, names);
}
}
}
fn collect_ir_pattern_names(pattern: &IrPattern, names: &mut HashSet<String>) {
match pattern {
IrPattern::Binding(name) => {
names.insert(name.clone());
}
IrPattern::Constructor { name, bindings, .. } => {
names.insert(name.clone());
for binding in bindings.iter().flatten() {
names.insert(binding.clone());
}
}
IrPattern::Wildcard
| IrPattern::Bool(_)
| IrPattern::Unit
| IrPattern::Int(_)
| IrPattern::ByteArray(_)
| IrPattern::String(_) => (),
}
}
fn add_custom_match_fallback(subject_type: &Type, arms: &mut Vec<IrMatchArm>) {
if !matches!(subject_type, Type::Custom(_)) {
return;
}
if arms
.iter()
.any(|arm| matches!(arm.pattern, IrPattern::Wildcard | IrPattern::Binding(_)))
{
return;
}
arms.push(IrMatchArm {
pattern: IrPattern::Wildcard,
body: Term::Error("fail".to_string()),
});
}
fn inline_function_call(
file: &str,
function: &Function,
args: Vec<Term>,
checked: &CheckedModule,
inlining: &mut HashSet<String>,
) -> Result<Term> {
if !inlining.insert(function.name.clone()) {
return Ok(Term::Error(format!(
"recursive helper call to {}",
function.name
)));
}
let function_env = env_from_params(file, &function.params, &checked.type_names)?;
let mut body = lower_block_inner(file, &function.body, checked, &function_env, inlining)?;
inlining.remove(&function.name);
for (param, arg) in function.params.iter().rev().zip(args.into_iter().rev()) {
if is_identity_binding(¶m.name, &arg) {
continue;
}
body = Term::Let {
name: param.name.clone(),
value: Box::new(arg),
body: Box::new(body),
};
}
Ok(body)
}
fn collect_typed_pattern_bindings(
pattern: &AstPattern,
subject_type: &Type,
checked: &CheckedModule,
env: &mut HashMap<String, Type>,
) {
match pattern {
AstPattern::Variable { name, .. } => {
env.insert(name.clone(), subject_type.clone());
}
AstPattern::Constructor { name, bindings, .. } => {
if let Some(constructor) = checked.constructors.get(name) {
for (binding, ty) in bindings.iter().zip(constructor.fields.iter()) {
if let Some(name) = &binding.name {
env.insert(name.clone(), ty.clone());
}
}
}
}
AstPattern::Wildcard { .. }
| AstPattern::Bool { .. }
| AstPattern::Int { .. }
| AstPattern::ByteArray { .. }
| AstPattern::String { .. }
| AstPattern::Unit { .. } => (),
}
}
fn is_identity_binding(name: &str, arg: &Term) -> bool {
matches!(arg, Term::Var(var) if var == name)
}
fn lower_pattern(pattern: &AstPattern, checked: &CheckedModule) -> IrPattern {
match pattern {
AstPattern::Wildcard { .. } => IrPattern::Wildcard,
AstPattern::Variable { name, .. } => IrPattern::Binding(name.clone()),
AstPattern::Constructor { name, bindings, .. } => {
let tag = checked
.constructors
.get(name)
.map_or(0, |constructor| constructor.tag);
IrPattern::Constructor {
name: name.clone(),
tag,
bindings: bindings
.iter()
.map(|binding| binding.name.clone())
.collect(),
}
}
AstPattern::Bool { value, .. } => IrPattern::Bool(*value),
AstPattern::Unit { .. } => IrPattern::Unit,
AstPattern::Int { value, .. } => IrPattern::Int(*value),
AstPattern::ByteArray { hex, .. } => IrPattern::ByteArray(hex.clone()),
AstPattern::String { value, .. } => IrPattern::String(value.clone()),
}
}
fn binary_builtin(op: BinaryOp) -> &'static str {
match op {
BinaryOp::Less => "lessThanInteger",
BinaryOp::LessEqual => "lessThanEqualsInteger",
BinaryOp::Greater => "greaterThanInteger",
BinaryOp::GreaterEqual => "greaterThanEqualsInteger",
BinaryOp::Add => "addInteger",
BinaryOp::Subtract => "subtractInteger",
BinaryOp::Multiply => "multiplyInteger",
BinaryOp::Divide => "quotientInteger",
BinaryOp::Remainder => "remainderInteger",
BinaryOp::And | BinaryOp::Or | BinaryOp::Equal | BinaryOp::NotEqual => {
unreachable!("short-circuit and equality operators lower separately")
}
}
}
fn equality_builtin(op: BinaryOp, ty: &Type, checked: &CheckedModule) -> &'static str {
let equal = matches!(op, BinaryOp::Equal);
match ty {
Type::Bool if equal => "equalsBool",
Type::Bool => "notEqualsBool",
Type::Int if equal => "equalsInteger",
Type::Int => "notEqualsInteger",
Type::ByteArray if equal => "equalsByteString",
Type::ByteArray => "notEqualsByteString",
Type::Data if equal => "equalsData",
Type::Data => "notEqualsData",
Type::String if equal => "equalsString",
Type::String => "notEqualsString",
Type::List(item) => list_equality_builtin(equal, item, checked).unwrap_or(if equal {
"equalsAeriValue"
} else {
"notEqualsAeriValue"
}),
Type::Custom(name) if custom_type_has_only_nullary_constructors(name, checked) && equal => {
"equalsCustomTag"
}
Type::Custom(name) if custom_type_has_only_nullary_constructors(name, checked) => {
"notEqualsCustomTag"
}
Type::Custom(_) if equal => "equalsAeriValue",
Type::Custom(_) => "notEqualsAeriValue",
Type::Tx | Type::Unit | Type::Never => {
unreachable!("non-equatable types are rejected before lowering")
}
}
}
fn list_equality_builtin(
equal: bool,
item: &Type,
checked: &CheckedModule,
) -> Option<&'static str> {
Some(match (equal, item) {
(true, Type::Bool) => "equalsListBool",
(false, Type::Bool) => "notEqualsListBool",
(true, Type::Int) => "equalsListInteger",
(false, Type::Int) => "notEqualsListInteger",
(true, Type::ByteArray) => "equalsListByteString",
(false, Type::ByteArray) => "notEqualsListByteString",
(true, Type::Data) => "equalsListData",
(false, Type::Data) => "notEqualsListData",
(true, Type::String) => "equalsListString",
(false, Type::String) => "notEqualsListString",
(true, Type::Unit) => "equalsListUnit",
(false, Type::Unit) => "notEqualsListUnit",
(true, Type::Custom(name)) if custom_type_has_only_nullary_constructors(name, checked) => {
"equalsListCustomTag"
}
(false, Type::Custom(name)) if custom_type_has_only_nullary_constructors(name, checked) => {
"notEqualsListCustomTag"
}
_ => return None,
})
}
fn custom_type_has_only_nullary_constructors(type_name: &str, checked: &CheckedModule) -> bool {
let mut found = false;
for constructor in checked.constructors.values() {
if constructor.type_name == type_name {
found = true;
if !constructor.fields.is_empty() {
return false;
}
}
}
found
}
fn wrap_params(term: Term, params: impl DoubleEndedIterator<Item = String>) -> Term {
params.rev().fold(term, |body, param| Term::Lambda {
param,
body: Box::new(body),
})
}
fn render_module(
module: &Module,
constants: &[ConstArtifact<'_>],
functions: &[FunctionArtifact<'_>],
validators: &[ValidatorArtifact<'_>],
tests: &[TestArtifact<'_>],
) -> String {
let mut rendered = format!("(module {}\n", module.name);
for item in &module.items {
if let Item::Type(type_decl) = item {
rendered.push_str(&format!(" {}\n", render_type_decl(type_decl)));
}
}
for artifact in constants {
rendered.push_str(&format!(
" (const {} {})\n",
artifact.constant.name,
render_preview_program(&artifact.program)
));
}
for artifact in functions {
rendered.push_str(&format!(
" (fn {} {})\n",
artifact.function.name,
render_preview_program(&artifact.program)
));
}
for artifact in validators {
rendered.push_str(&format!(
" (validator {} {})\n",
artifact.validator.name,
render_preview_program(&artifact.program)
));
}
for artifact in tests {
let expectation = if artifact.test.should_fail {
" fails"
} else {
""
};
rendered.push_str(&format!(
" (test {}{} {})\n",
artifact.test.name,
expectation,
render_preview_program(&artifact.program)
));
}
rendered.push(')');
rendered
}
fn render_type_decl(type_decl: &TypeDecl) -> String {
let variants = type_decl
.variants
.iter()
.map(|variant| {
let fields = variant
.fields
.iter()
.map(|field| format_type_ref(&field.ty))
.collect::<Vec<_>>()
.join(" ");
if fields.is_empty() {
variant.name.clone()
} else {
format!("({} {fields})", variant.name)
}
})
.collect::<Vec<_>>()
.join(" ");
format!("(type {} {variants})", type_decl.name)
}
fn build_blueprint(module: &Module, validators: &[LoweredValidator]) -> Blueprint {
Blueprint {
preamble: preamble(&module.name),
validators: validators
.iter()
.map(|validator| {
let compiled_code = render_preview_program(&validator.program);
BlueprintValidator {
title: validator.title.clone(),
script_purpose: validator.script_purpose.clone(),
parameters: validator
.param_types
.iter()
.map(|(name, ty)| BlueprintParameter {
name: name.clone(),
schema: schema_for_type(ty),
})
.collect(),
hash: hash_text(&compiled_code),
compiled_code,
}
})
.collect(),
definitions: module
.items
.iter()
.filter_map(|item| match item {
Item::Type(type_decl) => Some(BlueprintDefinition {
title: type_decl.name.clone(),
any_of: type_decl
.variants
.iter()
.enumerate()
.map(|(index, variant)| BlueprintVariant {
title: variant.name.clone(),
index,
fields: variant
.fields
.iter()
.enumerate()
.map(|(field_index, field)| BlueprintParameter {
name: field
.name
.clone()
.unwrap_or_else(|| format!("field{field_index}")),
schema: schema_for_type_ref(&field.ty),
})
.collect(),
})
.collect(),
}),
Item::Const(_) | Item::Function(_) | Item::Validator(_) | Item::Test(_) => None,
})
.collect(),
}
}
fn preamble(title: &str) -> Preamble {
Preamble {
title: title.to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
plutus_version: "PlutusV3".to_string(),
compiler: Compiler {
name: "aeri".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
target: PREVIEW_COMPILER_TARGET.to_string(),
cardano_deployable: PREVIEW_CARDANO_DEPLOYABLE,
},
}
}
fn schema_for_type(ty: &Type) -> Schema {
if let Type::List(item) = ty {
return Schema {
data_type: "list".to_string(),
title: Some(item.to_string()),
items: Some(Box::new(schema_for_type(item))),
};
}
Schema {
data_type: ty.schema_name().to_string(),
title: match ty {
Type::Custom(name) => Some(name.clone()),
_ => None,
},
items: None,
}
}
fn schema_for_type_ref(ty: &crate::ast::TypeRef) -> Schema {
let data_type = match ty.name.as_str() {
"Bool" => "boolean",
"Int" => "integer",
"ByteArray" => "bytes",
"Data" => "data",
"Tx" => "transaction",
"String" => "string",
"Unit" => "unit",
"List" => "list",
_ => "constructor",
};
Schema {
data_type: data_type.to_string(),
title: match data_type {
"constructor" => Some(ty.name.clone()),
"list" => ty.args.first().map(format_type_ref),
_ => None,
},
items: if data_type == "list" {
ty.args
.first()
.map(|item| Box::new(schema_for_type_ref(item)))
} else {
None
},
}
}
fn format_type_ref(ty: &crate::ast::TypeRef) -> String {
if ty.args.is_empty() {
ty.name.clone()
} else {
let args = ty
.args
.iter()
.map(format_type_ref)
.collect::<Vec<_>>()
.join(", ");
format!("{}<{args}>", ty.name)
}
}
fn hash_text(text: &str) -> String {
format!("aeri_sha256_{}", hex_encode(&Sha256::digest(text)))
}
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
}
fn item_span(item: &Item) -> Span {
match item {
Item::Type(type_decl) => type_decl.span,
Item::Const(constant) => constant.span,
Item::Function(function) => function.span,
Item::Validator(validator) => validator.span,
Item::Test(test) => test.span,
}
}