use std::{collections::HashMap, rc::Rc};
use miette::Diagnostic;
use crate::ast::*;
use crate::parsing::AstNode;
const METADATA_MAX_SIZE_BYTES: usize = 64;
#[derive(Debug, thiserror::Error, miette::Diagnostic, PartialEq, Eq, Clone)]
#[error("not in scope: {name}")]
#[diagnostic(code(tx3::not_in_scope))]
pub struct NotInScopeError {
pub name: String,
#[source_code]
src: Option<String>,
#[label]
span: Span,
}
#[derive(Debug, thiserror::Error, miette::Diagnostic, PartialEq, Eq, Clone)]
#[error("invalid symbol, expected {expected}, got {got}")]
#[diagnostic(code(tx3::invalid_symbol))]
pub struct InvalidSymbolError {
pub expected: &'static str,
pub got: String,
#[source_code]
src: Option<String>,
#[label]
span: Span,
}
#[derive(Debug, thiserror::Error, miette::Diagnostic, PartialEq, Eq, Clone)]
#[error("invalid type ({got}), expected: {expected}")]
#[diagnostic(code(tx3::invalid_type))]
pub struct InvalidTargetTypeError {
pub expected: String,
pub got: String,
#[source_code]
src: Option<String>,
#[label]
span: Span,
}
#[derive(Debug, thiserror::Error, miette::Diagnostic, PartialEq, Eq, Clone)]
#[error("optional output ({name}) cannot have a datum")]
#[diagnostic(code(tx3::optional_output_datum))]
pub struct OptionalOutputError {
pub name: String,
#[source_code]
src: Option<String>,
#[label]
span: Span,
}
#[derive(Debug, thiserror::Error, miette::Diagnostic, PartialEq, Eq, Clone)]
#[error("metadata value exceeds 64 bytes: {size} bytes found")]
#[diagnostic(code(tx3::metadata_size_limit_exceeded))]
pub struct MetadataSizeLimitError {
pub size: usize,
#[source_code]
src: Option<String>,
#[label("value too large")]
span: Span,
}
#[derive(Debug, thiserror::Error, miette::Diagnostic, PartialEq, Eq, Clone)]
#[error("metadata key must be an integer, got: {key_type}")]
#[diagnostic(code(tx3::metadata_invalid_key_type))]
pub struct MetadataInvalidKeyTypeError {
pub key_type: String,
#[source_code]
src: Option<String>,
#[label("expected integer key")]
span: Span,
}
#[derive(thiserror::Error, Debug, miette::Diagnostic, PartialEq, Eq, Clone)]
pub enum Error {
#[error("duplicate definition: {0}")]
#[diagnostic(code(tx3::duplicate_definition))]
DuplicateDefinition(String),
#[error(transparent)]
#[diagnostic(transparent)]
NotInScope(#[from] NotInScopeError),
#[error("needs parent scope")]
#[diagnostic(code(tx3::needs_parent_scope))]
NeedsParentScope,
#[error(transparent)]
#[diagnostic(transparent)]
InvalidSymbol(#[from] InvalidSymbolError),
#[error(transparent)]
#[diagnostic(transparent)]
InvalidTargetType(#[from] InvalidTargetTypeError),
#[error(transparent)]
#[diagnostic(transparent)]
MetadataSizeLimitExceeded(#[from] MetadataSizeLimitError),
#[error(transparent)]
#[diagnostic(transparent)]
MetadataInvalidKeyType(#[from] MetadataInvalidKeyTypeError),
#[error(transparent)]
#[diagnostic(transparent)]
InvalidOptionalOutput(#[from] OptionalOutputError),
}
impl Error {
pub fn span(&self) -> &Span {
match self {
Self::NotInScope(x) => &x.span,
Self::InvalidSymbol(x) => &x.span,
Self::InvalidTargetType(x) => &x.span,
Self::MetadataSizeLimitExceeded(x) => &x.span,
Self::MetadataInvalidKeyType(x) => &x.span,
Self::InvalidOptionalOutput(x) => &x.span,
_ => &Span::DUMMY,
}
}
pub fn src(&self) -> Option<&str> {
match self {
Self::NotInScope(x) => x.src.as_deref(),
Self::MetadataSizeLimitExceeded(x) => x.src.as_deref(),
Self::MetadataInvalidKeyType(x) => x.src.as_deref(),
_ => None,
}
}
pub fn not_in_scope(name: String, ast: &impl crate::parsing::AstNode) -> Self {
Self::NotInScope(NotInScopeError {
name,
src: None,
span: ast.span().clone(),
})
}
fn symbol_type_name(symbol: &Symbol) -> String {
match symbol {
Symbol::TypeDef(type_def) => format!("TypeDef({})", type_def.name.value),
Symbol::AliasDef(alias_def) => format!("AliasDef({})", alias_def.name.value),
Symbol::VariantCase(case) => format!("VariantCase({})", case.name.value),
Symbol::RecordField(field) => format!("RecordField({})", field.name.value),
Symbol::PartyDef(party) => format!("PartyDef({})", party.name.value),
Symbol::PolicyDef(policy) => format!("PolicyDef({})", policy.name.value),
Symbol::AssetDef(asset) => format!("AssetDef({})", asset.name.value),
Symbol::EnvVar(name, _) => format!("EnvVar({})", name),
Symbol::ParamVar(name, _) => format!("ParamVar({})", name),
Symbol::Function(name) => format!("Function({})", name),
Symbol::Input(block) => format!("Input({})", block.name),
Symbol::Reference(block) => format!("Reference({})", block.name),
Symbol::Output(idx) => format!("Output({})", idx),
Symbol::LocalExpr(_) => "LocalExpr".to_string(),
Symbol::Fees => "Fees".to_string(),
}
}
pub fn invalid_symbol(
expected: &'static str,
got: &Symbol,
ast: &impl crate::parsing::AstNode,
) -> Self {
Self::InvalidSymbol(InvalidSymbolError {
expected,
got: Self::symbol_type_name(got),
src: None,
span: ast.span().clone(),
})
}
pub fn invalid_target_type(
expected: &Type,
got: &Type,
ast: &impl crate::parsing::AstNode,
) -> Self {
Self::InvalidTargetType(InvalidTargetTypeError {
expected: expected.to_string(),
got: got.to_string(),
src: None,
span: ast.span().clone(),
})
}
}
#[derive(Debug, Default, thiserror::Error, Diagnostic, Clone)]
pub struct AnalyzeReport {
#[related]
pub errors: Vec<Error>,
}
impl AnalyzeReport {
pub fn is_empty(&self) -> bool {
self.errors.is_empty()
}
pub fn ok(self) -> Result<(), Self> {
if self.is_empty() {
Ok(())
} else {
Err(self)
}
}
pub fn expect_data_expr_type(expr: &DataExpr, expected: &Type) -> Self {
if expr.target_type().as_ref() != Some(expected) {
Self::from(Error::invalid_target_type(
expected,
expr.target_type().as_ref().unwrap_or(&Type::Undefined),
expr,
))
} else {
Self::default()
}
}
}
impl std::fmt::Display for AnalyzeReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.errors.is_empty() {
write!(f, "")
} else {
write!(f, "Failed with {} errors:", self.errors.len())?;
for error in &self.errors {
write!(f, "\n{} ({:?})", error, error)?;
}
Ok(())
}
}
}
impl std::ops::Add for Error {
type Output = AnalyzeReport;
fn add(self, other: Self) -> Self::Output {
Self::Output {
errors: vec![self, other],
}
}
}
impl From<Error> for AnalyzeReport {
fn from(error: Error) -> Self {
Self {
errors: vec![error],
}
}
}
impl From<Vec<Error>> for AnalyzeReport {
fn from(errors: Vec<Error>) -> Self {
Self { errors }
}
}
impl std::ops::Add for AnalyzeReport {
type Output = AnalyzeReport;
fn add(self, other: Self) -> Self::Output {
[self, other].into_iter().collect()
}
}
impl FromIterator<Error> for AnalyzeReport {
fn from_iter<T: IntoIterator<Item = Error>>(iter: T) -> Self {
Self {
errors: iter.into_iter().collect(),
}
}
}
impl FromIterator<AnalyzeReport> for AnalyzeReport {
fn from_iter<T: IntoIterator<Item = AnalyzeReport>>(iter: T) -> Self {
Self {
errors: iter.into_iter().flat_map(|r| r.errors).collect(),
}
}
}
macro_rules! bail_report {
($($args:expr),*) => {
{ return AnalyzeReport::from(vec![$($args),*]); }
};
}
const BUILTIN_FUNCTIONS: &[&str] = &["min_utxo", "tip_slot", "slot_to_time", "time_to_slot"];
impl Scope {
pub fn new(parent: Option<Rc<Scope>>) -> Self {
Self {
symbols: HashMap::new(),
parent,
}
}
pub fn track_env_var(&mut self, name: &str, ty: Type) {
self.symbols.insert(
name.to_string(),
Symbol::EnvVar(name.to_string(), Box::new(ty)),
);
}
pub fn track_type_def(&mut self, type_: &TypeDef) {
self.symbols.insert(
type_.name.value.clone(),
Symbol::TypeDef(Box::new(type_.clone())),
);
}
pub fn track_alias_def(&mut self, alias: &AliasDef) {
self.symbols.insert(
alias.name.value.clone(),
Symbol::AliasDef(Box::new(alias.clone())),
);
}
pub fn track_variant_case(&mut self, case: &VariantCase) {
self.symbols.insert(
case.name.value.clone(),
Symbol::VariantCase(Box::new(case.clone())),
);
}
pub fn track_record_field(&mut self, field: &RecordField) {
self.symbols.insert(
field.name.value.clone(),
Symbol::RecordField(Box::new(field.clone())),
);
}
pub fn track_party_def(&mut self, party: &PartyDef) {
self.symbols.insert(
party.name.value.clone(),
Symbol::PartyDef(Box::new(party.clone())),
);
}
pub fn track_policy_def(&mut self, policy: &PolicyDef) {
self.symbols.insert(
policy.name.value.clone(),
Symbol::PolicyDef(Box::new(policy.clone())),
);
}
pub fn track_asset_def(&mut self, asset: &AssetDef) {
self.symbols.insert(
asset.name.value.clone(),
Symbol::AssetDef(Box::new(asset.clone())),
);
}
pub fn track_param_var(&mut self, param: &str, ty: Type) {
self.symbols.insert(
param.to_string(),
Symbol::ParamVar(param.to_string(), Box::new(ty)),
);
}
pub fn track_local_expr(&mut self, name: &str, expr: DataExpr) {
self.symbols
.insert(name.to_string(), Symbol::LocalExpr(Box::new(expr)));
}
pub fn track_input(&mut self, name: &str, input: InputBlock) {
self.symbols
.insert(name.to_string(), Symbol::Input(Box::new(input)));
}
pub fn track_reference(&mut self, name: &str, reference: ReferenceBlock) {
self.symbols
.insert(name.to_string(), Symbol::Reference(Box::new(reference)));
}
pub fn track_output(&mut self, index: usize, output: OutputBlock) {
if let Some(n) = output.name {
self.symbols.insert(n.value, Symbol::Output(index));
}
}
pub fn track_record_fields_for_type(&mut self, ty: &Type) {
let schema = ty.properties();
for (name, subty) in schema {
self.track_record_field(&RecordField {
name: Identifier::new(name),
r#type: subty,
span: Span::DUMMY,
});
}
}
pub fn resolve(&self, name: &str) -> Option<Symbol> {
if let Some(symbol) = self.symbols.get(name) {
Some(symbol.clone())
} else if let Some(parent) = &self.parent {
parent.resolve(name)
} else if BUILTIN_FUNCTIONS.contains(&name) {
Some(Symbol::Function(name.to_string()))
} else {
None
}
}
}
pub trait Analyzable {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport;
fn is_resolved(&self) -> bool;
}
impl<T: Analyzable> Analyzable for Option<T> {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
if let Some(item) = self {
item.analyze(parent)
} else {
AnalyzeReport::default()
}
}
fn is_resolved(&self) -> bool {
self.as_ref().is_none_or(|x| x.is_resolved())
}
}
impl<T: Analyzable> Analyzable for Box<T> {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
self.as_mut().analyze(parent)
}
fn is_resolved(&self) -> bool {
self.as_ref().is_resolved()
}
}
impl<T: Analyzable> Analyzable for Vec<T> {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
self.iter_mut()
.map(|item| item.analyze(parent.clone()))
.collect()
}
fn is_resolved(&self) -> bool {
self.iter().all(|x| x.is_resolved())
}
}
impl Analyzable for PartyDef {
fn analyze(&mut self, _parent: Option<Rc<Scope>>) -> AnalyzeReport {
AnalyzeReport::default()
}
fn is_resolved(&self) -> bool {
true
}
}
impl Analyzable for PolicyField {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
match self {
PolicyField::Hash(x) => x.analyze(parent),
PolicyField::Script(x) => x.analyze(parent),
PolicyField::Ref(x) => x.analyze(parent),
}
}
fn is_resolved(&self) -> bool {
match self {
PolicyField::Hash(x) => x.is_resolved(),
PolicyField::Script(x) => x.is_resolved(),
PolicyField::Ref(x) => x.is_resolved(),
}
}
}
impl Analyzable for PolicyConstructor {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
self.fields.analyze(parent)
}
fn is_resolved(&self) -> bool {
self.fields.is_resolved()
}
}
impl Analyzable for PolicyDef {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
match &mut self.value {
PolicyValue::Constructor(x) => x.analyze(parent),
PolicyValue::Assign(_) => AnalyzeReport::default(),
}
}
fn is_resolved(&self) -> bool {
match &self.value {
PolicyValue::Constructor(x) => x.is_resolved(),
PolicyValue::Assign(_) => true,
}
}
}
impl Analyzable for AddOp {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
let left = self.lhs.analyze(parent.clone());
let right = self.rhs.analyze(parent.clone());
left + right
}
fn is_resolved(&self) -> bool {
self.lhs.is_resolved() && self.rhs.is_resolved()
}
}
impl Analyzable for ConcatOp {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
let left = self.lhs.analyze(parent.clone());
let right = self.rhs.analyze(parent.clone());
left + right
}
fn is_resolved(&self) -> bool {
self.lhs.is_resolved() && self.rhs.is_resolved()
}
}
impl Analyzable for SubOp {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
let left = self.lhs.analyze(parent.clone());
let right = self.rhs.analyze(parent.clone());
left + right
}
fn is_resolved(&self) -> bool {
self.lhs.is_resolved() && self.rhs.is_resolved()
}
}
impl Analyzable for NegateOp {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
self.operand.analyze(parent)
}
fn is_resolved(&self) -> bool {
self.operand.is_resolved()
}
}
impl Analyzable for RecordConstructorField {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
let name = self.name.analyze(parent.clone());
let outer = parent.as_ref().and_then(|p| p.parent.clone());
let value = self.value.analyze(outer);
name + value
}
fn is_resolved(&self) -> bool {
self.name.is_resolved() && self.value.is_resolved()
}
}
impl Analyzable for VariantCaseConstructor {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
let name = if self.name.symbol.is_some() {
AnalyzeReport::default()
} else {
self.name.analyze(parent.clone())
};
let mut scope = Scope::new(parent);
let case = match &self.name.symbol {
Some(Symbol::VariantCase(x)) => x,
Some(x) => bail_report!(Error::invalid_symbol("VariantCase", x, &self.name)),
None => bail_report!(Error::not_in_scope(self.name.value.clone(), &self.name)),
};
for field in case.fields.iter() {
scope.track_record_field(field);
}
self.scope = Some(Rc::new(scope));
let fields = self.fields.analyze(self.scope.clone());
let spread = self.spread.analyze(self.scope.clone());
name + fields + spread
}
fn is_resolved(&self) -> bool {
self.name.is_resolved() && self.fields.is_resolved() && self.spread.is_resolved()
}
}
impl Analyzable for StructConstructor {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
let r#type = self.r#type.analyze(parent.clone());
let mut scope = Scope::new(parent);
let type_def = match &self.r#type.symbol {
Some(Symbol::TypeDef(type_def)) => type_def.as_ref(),
Some(Symbol::AliasDef(alias_def)) => match alias_def.resolve_alias_chain() {
Some(resolved_type_def) => resolved_type_def,
None => {
bail_report!(Error::invalid_symbol(
"struct type",
&Symbol::AliasDef(alias_def.clone()),
&self.r#type
));
}
},
Some(symbol) => {
bail_report!(Error::invalid_symbol("struct type", symbol, &self.r#type));
}
_ => unreachable!(),
};
for case in type_def.cases.iter() {
scope.track_variant_case(case);
}
self.scope = Some(Rc::new(scope));
let case = self.case.analyze(self.scope.clone());
r#type + case
}
fn is_resolved(&self) -> bool {
self.r#type.is_resolved() && self.case.is_resolved()
}
}
impl Analyzable for ListConstructor {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
self.elements.analyze(parent)
}
fn is_resolved(&self) -> bool {
self.elements.is_resolved()
}
}
impl Analyzable for MapField {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
self.key.analyze(parent.clone()) + self.value.analyze(parent.clone())
}
fn is_resolved(&self) -> bool {
self.key.is_resolved() && self.value.is_resolved()
}
}
impl Analyzable for MapConstructor {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
self.fields.analyze(parent)
}
fn is_resolved(&self) -> bool {
self.fields.is_resolved()
}
}
impl Analyzable for DataExpr {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
match self {
DataExpr::StructConstructor(x) => x.analyze(parent),
DataExpr::ListConstructor(x) => x.analyze(parent),
DataExpr::MapConstructor(x) => x.analyze(parent),
DataExpr::Identifier(x) => x.analyze(parent),
DataExpr::AddOp(x) => x.analyze(parent),
DataExpr::SubOp(x) => x.analyze(parent),
DataExpr::NegateOp(x) => x.analyze(parent),
DataExpr::PropertyOp(x) => x.analyze(parent),
DataExpr::AnyAssetConstructor(x) => x.analyze(parent),
DataExpr::FnCall(x) => x.analyze(parent),
DataExpr::MinUtxo(x) => x.analyze(parent),
DataExpr::SlotToTime(x) => x.analyze(parent),
DataExpr::TimeToSlot(x) => x.analyze(parent),
DataExpr::ConcatOp(x) => x.analyze(parent),
_ => AnalyzeReport::default(),
}
}
fn is_resolved(&self) -> bool {
match self {
DataExpr::StructConstructor(x) => x.is_resolved(),
DataExpr::ListConstructor(x) => x.is_resolved(),
DataExpr::MapConstructor(x) => x.is_resolved(),
DataExpr::Identifier(x) => x.is_resolved(),
DataExpr::AddOp(x) => x.is_resolved(),
DataExpr::SubOp(x) => x.is_resolved(),
DataExpr::NegateOp(x) => x.is_resolved(),
DataExpr::PropertyOp(x) => x.is_resolved(),
DataExpr::AnyAssetConstructor(x) => x.is_resolved(),
DataExpr::FnCall(x) => x.is_resolved(),
DataExpr::MinUtxo(x) => x.is_resolved(),
DataExpr::SlotToTime(x) => x.is_resolved(),
DataExpr::TimeToSlot(x) => x.is_resolved(),
DataExpr::ConcatOp(x) => x.is_resolved(),
_ => true,
}
}
}
impl Analyzable for crate::ast::FnCall {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
let callee = self.callee.analyze(parent.clone());
let mut args_report = AnalyzeReport::default();
for arg in &mut self.args {
args_report = args_report + arg.analyze(parent.clone());
}
callee + args_report
}
fn is_resolved(&self) -> bool {
self.callee.is_resolved() && self.args.iter().all(|arg| arg.is_resolved())
}
}
impl Analyzable for AnyAssetConstructor {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
let policy = self.policy.analyze(parent.clone());
let asset_name = self.asset_name.analyze(parent.clone());
let amount = self.amount.analyze(parent.clone());
policy + asset_name + amount
}
fn is_resolved(&self) -> bool {
self.policy.is_resolved() && self.asset_name.is_resolved() && self.amount.is_resolved()
}
}
impl Analyzable for PropertyOp {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
let object = self.operand.analyze(parent.clone());
let mut scope = Scope::new(parent);
if let Some(ty) = self.operand.target_type() {
scope.track_record_fields_for_type(&ty);
}
self.scope = Some(Rc::new(scope));
let path = self.property.analyze(self.scope.clone());
object + path
}
fn is_resolved(&self) -> bool {
self.operand.is_resolved() && self.property.is_resolved()
}
}
impl Analyzable for AddressExpr {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
match self {
AddressExpr::Identifier(x) => x.analyze(parent),
_ => AnalyzeReport::default(),
}
}
fn is_resolved(&self) -> bool {
match self {
AddressExpr::Identifier(x) => x.is_resolved(),
_ => true,
}
}
}
impl Analyzable for AssetDef {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
let policy = self.policy.analyze(parent.clone());
let asset_name = self.asset_name.analyze(parent.clone());
let policy_type = AnalyzeReport::expect_data_expr_type(&self.policy, &Type::Bytes);
let asset_name_type = AnalyzeReport::expect_data_expr_type(&self.asset_name, &Type::Bytes);
policy + asset_name + policy_type + asset_name_type
}
fn is_resolved(&self) -> bool {
self.policy.is_resolved() && self.asset_name.is_resolved()
}
}
impl Analyzable for Identifier {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
let symbol = parent.and_then(|p| p.resolve(&self.value));
if symbol.is_none() {
bail_report!(Error::not_in_scope(self.value.clone(), self));
}
self.symbol = symbol;
AnalyzeReport::default()
}
fn is_resolved(&self) -> bool {
self.symbol.is_some()
}
}
impl Analyzable for Type {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
match self {
Type::Custom(x) => x.analyze(parent),
Type::List(x) => x.analyze(parent),
Type::Map(key_type, value_type) => {
key_type.analyze(parent.clone()) + value_type.analyze(parent)
}
_ => AnalyzeReport::default(),
}
}
fn is_resolved(&self) -> bool {
match self {
Type::Custom(x) => x.is_resolved(),
Type::List(x) => x.is_resolved(),
Type::Map(key_type, value_type) => key_type.is_resolved() && value_type.is_resolved(),
_ => true,
}
}
}
impl Analyzable for InputBlockField {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
match self {
InputBlockField::From(x) => x.analyze(parent),
InputBlockField::DatumIs(x) => x.analyze(parent),
InputBlockField::MinAmount(x) => x.analyze(parent),
InputBlockField::Redeemer(x) => x.analyze(parent),
InputBlockField::Ref(x) => x.analyze(parent),
}
}
fn is_resolved(&self) -> bool {
match self {
InputBlockField::From(x) => x.is_resolved(),
InputBlockField::DatumIs(x) => x.is_resolved(),
InputBlockField::MinAmount(x) => x.is_resolved(),
InputBlockField::Redeemer(x) => x.is_resolved(),
InputBlockField::Ref(x) => x.is_resolved(),
}
}
}
impl Analyzable for InputBlock {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
self.fields.analyze(parent)
}
fn is_resolved(&self) -> bool {
self.fields.is_resolved()
}
}
fn validate_metadata_value_size(expr: &DataExpr) -> Result<(), MetadataSizeLimitError> {
match expr {
DataExpr::String(string_literal) => {
let utf8_bytes = string_literal.value.as_bytes();
if utf8_bytes.len() > METADATA_MAX_SIZE_BYTES {
return Err(MetadataSizeLimitError {
size: utf8_bytes.len(),
src: None,
span: string_literal.span.clone(),
});
}
}
DataExpr::HexString(hex_literal) => {
let hex_str = &hex_literal.value;
let hex_str = hex_str.strip_prefix("0x").unwrap_or(hex_str);
let byte_length = hex_str.len() / 2;
if byte_length > METADATA_MAX_SIZE_BYTES {
return Err(MetadataSizeLimitError {
size: byte_length,
src: None,
span: hex_literal.span.clone(),
});
}
}
_ => {}
}
Ok(())
}
fn validate_metadata_key_type(expr: &DataExpr) -> Result<(), MetadataInvalidKeyTypeError> {
match expr {
DataExpr::Number(_) => Ok(()),
DataExpr::Identifier(id) => match id.target_type() {
Some(Type::Int) => Ok(()),
Some(other_type) => Err(MetadataInvalidKeyTypeError {
key_type: format!("identifier of type {}", other_type),
src: None,
span: id.span().clone(),
}),
None => Err(MetadataInvalidKeyTypeError {
key_type: "unresolved identifier".to_string(),
src: None,
span: id.span().clone(),
}),
},
_ => {
let key_type = match expr {
DataExpr::String(_) => "string",
DataExpr::HexString(_) => "hex string",
DataExpr::ListConstructor(_) => "list",
DataExpr::MapConstructor(_) => "map",
DataExpr::StructConstructor(_) => "struct",
_ => "unknown",
};
Err(MetadataInvalidKeyTypeError {
key_type: key_type.to_string(),
src: None,
span: expr.span().clone(),
})
}
}
}
impl Analyzable for MetadataBlockField {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
let mut report = self.key.analyze(parent.clone()) + self.value.analyze(parent.clone());
validate_metadata_key_type(&self.key)
.map_err(Error::MetadataInvalidKeyType)
.err()
.map(|e| report.errors.push(e));
validate_metadata_value_size(&self.value)
.map_err(Error::MetadataSizeLimitExceeded)
.err()
.map(|e| report.errors.push(e));
report
}
fn is_resolved(&self) -> bool {
self.key.is_resolved() && self.value.is_resolved()
}
}
impl Analyzable for MetadataBlock {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
self.fields.analyze(parent)
}
fn is_resolved(&self) -> bool {
self.fields.is_resolved()
}
}
impl Analyzable for ValidityBlockField {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
match self {
ValidityBlockField::SinceSlot(x) => x.analyze(parent),
ValidityBlockField::UntilSlot(x) => x.analyze(parent),
}
}
fn is_resolved(&self) -> bool {
match self {
ValidityBlockField::SinceSlot(x) => x.is_resolved(),
ValidityBlockField::UntilSlot(x) => x.is_resolved(),
}
}
}
impl Analyzable for ValidityBlock {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
self.fields.analyze(parent)
}
fn is_resolved(&self) -> bool {
self.fields.is_resolved()
}
}
impl Analyzable for OutputBlockField {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
match self {
OutputBlockField::To(x) => x.analyze(parent),
OutputBlockField::Amount(x) => x.analyze(parent),
OutputBlockField::Datum(x) => x.analyze(parent),
}
}
fn is_resolved(&self) -> bool {
match self {
OutputBlockField::To(x) => x.is_resolved(),
OutputBlockField::Amount(x) => x.is_resolved(),
OutputBlockField::Datum(x) => x.is_resolved(),
}
}
}
impl Analyzable for OutputBlock {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
validate_optional_output(self)
.map(AnalyzeReport::from)
.unwrap_or_else(|| AnalyzeReport::default())
+ self.fields.analyze(parent)
}
fn is_resolved(&self) -> bool {
self.fields.is_resolved()
}
}
fn validate_optional_output(output: &OutputBlock) -> Option<Error> {
if output.optional {
if let Some(_field) = output.find("datum") {
return Some(Error::InvalidOptionalOutput(OptionalOutputError {
name: output
.name
.as_ref()
.map(|i| i.value.clone())
.unwrap_or_else(|| "<anonymous>".to_string()),
src: None,
span: output.span.clone(),
}));
}
}
None
}
impl Analyzable for RecordField {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
self.r#type.analyze(parent)
}
fn is_resolved(&self) -> bool {
self.r#type.is_resolved()
}
}
impl Analyzable for VariantCase {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
self.fields.analyze(parent)
}
fn is_resolved(&self) -> bool {
self.fields.is_resolved()
}
}
impl Analyzable for AliasDef {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
self.alias_type.analyze(parent)
}
fn is_resolved(&self) -> bool {
self.alias_type.is_resolved() && self.is_alias_chain_resolved()
}
}
impl Analyzable for TypeDef {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
self.cases.analyze(parent)
}
fn is_resolved(&self) -> bool {
self.cases.is_resolved()
}
}
impl Analyzable for MintBlockField {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
match self {
MintBlockField::Amount(x) => x.analyze(parent),
MintBlockField::Redeemer(x) => x.analyze(parent),
}
}
fn is_resolved(&self) -> bool {
match self {
MintBlockField::Amount(x) => x.is_resolved(),
MintBlockField::Redeemer(x) => x.is_resolved(),
}
}
}
impl Analyzable for MintBlock {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
self.fields.analyze(parent)
}
fn is_resolved(&self) -> bool {
self.fields.is_resolved()
}
}
impl Analyzable for SignersBlock {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
self.signers.analyze(parent)
}
fn is_resolved(&self) -> bool {
self.signers.is_resolved()
}
}
impl Analyzable for ReferenceBlock {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
self.r#ref.analyze(parent.clone()) + self.datum_is.analyze(parent)
}
fn is_resolved(&self) -> bool {
self.r#ref.is_resolved() && self.datum_is.is_resolved()
}
}
impl Analyzable for CollateralBlockField {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
match self {
CollateralBlockField::From(x) => x.analyze(parent),
CollateralBlockField::MinAmount(x) => x.analyze(parent),
CollateralBlockField::Ref(x) => x.analyze(parent),
}
}
fn is_resolved(&self) -> bool {
match self {
CollateralBlockField::From(x) => x.is_resolved(),
CollateralBlockField::MinAmount(x) => x.is_resolved(),
CollateralBlockField::Ref(x) => x.is_resolved(),
}
}
}
impl Analyzable for CollateralBlock {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
self.fields.analyze(parent)
}
fn is_resolved(&self) -> bool {
self.fields.is_resolved()
}
}
impl Analyzable for ChainSpecificBlock {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
match self {
ChainSpecificBlock::Cardano(x) => x.analyze(parent),
}
}
fn is_resolved(&self) -> bool {
match self {
ChainSpecificBlock::Cardano(x) => x.is_resolved(),
}
}
}
impl Analyzable for LocalsAssign {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
self.value.analyze(parent)
}
fn is_resolved(&self) -> bool {
self.value.is_resolved()
}
}
impl Analyzable for LocalsBlock {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
self.assigns.analyze(parent)
}
fn is_resolved(&self) -> bool {
self.assigns.is_resolved()
}
}
impl Analyzable for ParamDef {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
self.r#type.analyze(parent)
}
fn is_resolved(&self) -> bool {
self.r#type.is_resolved()
}
}
impl Analyzable for ParameterList {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
self.parameters.analyze(parent)
}
fn is_resolved(&self) -> bool {
self.parameters.is_resolved()
}
}
impl TxDef {
fn best_effort_analyze_circular_dependencies(&mut self, mut scope: Scope) -> Scope {
if let Some(locals) = &self.locals {
for assign in locals.assigns.iter() {
scope.track_local_expr(&assign.name.value, assign.value.clone());
}
}
for (_, input) in self.inputs.iter().enumerate() {
scope.track_input(&input.name, input.clone())
}
for reference in self.references.iter() {
scope.track_reference(&reference.name, reference.clone());
}
for (index, output) in self.outputs.iter().enumerate() {
scope.track_output(index, output.clone())
}
let scope_snapshot = Rc::new(scope);
let _ = self.locals.analyze(Some(scope_snapshot.clone()));
let _ = self.references.analyze(Some(scope_snapshot.clone()));
let _ = self.inputs.analyze(Some(scope_snapshot.clone()));
let _ = self.outputs.analyze(Some(scope_snapshot.clone()));
Scope::new(Some(scope_snapshot))
}
}
impl Analyzable for TxDef {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
let params = self.parameters.analyze(parent.clone());
let mut scope = Scope::new(parent.clone());
scope.symbols.insert("fees".to_string(), Symbol::Fees);
for param in self.parameters.parameters.iter() {
scope.track_param_var(¶m.name.value, param.r#type.clone());
}
for _ in 0..9 {
scope = self.best_effort_analyze_circular_dependencies(scope);
}
let final_scope = Rc::new(scope);
let locals = self.locals.analyze(Some(final_scope.clone()));
let inputs = self.inputs.analyze(Some(final_scope.clone()));
let outputs = self.outputs.analyze(Some(final_scope.clone()));
let mints = self.mints.analyze(Some(final_scope.clone()));
let burns = self.burns.analyze(Some(final_scope.clone()));
let adhoc = self.adhoc.analyze(Some(final_scope.clone()));
let validity = self.validity.analyze(Some(final_scope.clone()));
let metadata = self.metadata.analyze(Some(final_scope.clone()));
let signers = self.signers.analyze(Some(final_scope.clone()));
let references = self.references.analyze(Some(final_scope.clone()));
let collateral = self.collateral.analyze(Some(final_scope.clone()));
self.scope = Some(final_scope);
params
+ locals
+ inputs
+ outputs
+ mints
+ burns
+ adhoc
+ validity
+ metadata
+ signers
+ references
+ collateral
}
fn is_resolved(&self) -> bool {
self.inputs.is_resolved()
&& self.outputs.is_resolved()
&& self.mints.is_resolved()
&& self.locals.is_resolved()
&& self.adhoc.is_resolved()
&& self.validity.is_resolved()
&& self.metadata.is_resolved()
&& self.signers.is_resolved()
&& self.references.is_resolved()
&& self.collateral.is_resolved()
}
}
fn ada_asset_def() -> AssetDef {
AssetDef {
name: Identifier {
value: "Ada".to_string(),
symbol: None,
span: Span::DUMMY,
},
policy: DataExpr::None,
asset_name: DataExpr::None,
span: Span::DUMMY,
}
}
fn resolve_types_and_aliases(
scope_rc: &mut Rc<Scope>,
types: &mut Vec<TypeDef>,
aliases: &mut Vec<AliasDef>,
) -> (AnalyzeReport, AnalyzeReport) {
let mut types_report = AnalyzeReport::default();
let mut aliases_report = AnalyzeReport::default();
let mut pass_count = 0usize;
let max_passes = 100usize;
while pass_count < max_passes && !(types.is_resolved() && aliases.is_resolved()) {
pass_count += 1;
let scope = Rc::get_mut(scope_rc).expect("scope should be unique during resolution");
for type_def in types.iter() {
scope.track_type_def(type_def);
}
for alias_def in aliases.iter() {
scope.track_alias_def(alias_def);
}
types_report = types.analyze(Some(scope_rc.clone()));
aliases_report = aliases.analyze(Some(scope_rc.clone()));
}
(types_report, aliases_report)
}
impl Analyzable for Program {
fn analyze(&mut self, parent: Option<Rc<Scope>>) -> AnalyzeReport {
let mut scope = Scope::new(parent);
if let Some(env) = self.env.as_ref() {
for field in env.fields.iter() {
scope.track_env_var(&field.name, field.r#type.clone());
}
}
for party in self.parties.iter() {
scope.track_party_def(party);
}
for policy in self.policies.iter() {
scope.track_policy_def(policy);
}
scope.track_asset_def(&ada_asset_def());
for asset in self.assets.iter() {
scope.track_asset_def(asset);
}
for type_def in self.types.iter() {
scope.track_type_def(type_def);
}
for alias_def in self.aliases.iter() {
scope.track_alias_def(alias_def);
}
self.scope = Some(Rc::new(scope));
let parties = self.parties.analyze(self.scope.clone());
let policies = self.policies.analyze(self.scope.clone());
let assets = self.assets.analyze(self.scope.clone());
let mut types = self.types.clone();
let mut aliases = self.aliases.clone();
let scope_rc = self.scope.as_mut().unwrap();
let (types, aliases) = resolve_types_and_aliases(scope_rc, &mut types, &mut aliases);
let txs = self.txs.analyze(self.scope.clone());
parties + policies + types + aliases + txs + assets
}
fn is_resolved(&self) -> bool {
self.policies.is_resolved()
&& self.types.is_resolved()
&& self.aliases.is_resolved()
&& self.txs.is_resolved()
&& self.assets.is_resolved()
}
}
pub fn analyze(ast: &mut Program) -> AnalyzeReport {
ast.analyze(None)
}
#[cfg(test)]
mod tests {
use crate::parsing::parse_well_known_example;
use super::*;
#[test]
fn test_program_with_semantic_errors() {
let mut ast = parse_well_known_example("semantic_errors");
let report = analyze(&mut ast);
assert_eq!(report.errors.len(), 3);
assert_eq!(
report.errors[0],
Error::NotInScope(NotInScopeError {
name: "missing_symbol".to_string(),
src: None,
span: Span::DUMMY,
})
);
assert_eq!(
report.errors[1],
Error::InvalidTargetType(InvalidTargetTypeError {
expected: "Bytes".to_string(),
got: "Int".to_string(),
src: None,
span: Span::DUMMY,
})
);
assert_eq!(
report.errors[2],
Error::InvalidTargetType(InvalidTargetTypeError {
expected: "Bytes".to_string(),
got: "Int".to_string(),
src: None,
span: Span::DUMMY,
})
);
}
#[test]
fn test_min_utxo_analysis() {
let mut ast = crate::parsing::parse_string(
r#"
party Alice;
tx test() {
output my_output {
to: Alice,
amount: min_utxo(my_output),
}
}
"#,
)
.unwrap();
let result = analyze(&mut ast);
assert!(result.errors.is_empty());
}
#[test]
fn test_alias_undefined_type_error() {
let mut ast = crate::parsing::parse_string(
r#"
type MyAlias = UndefinedType;
"#,
)
.unwrap();
let result = analyze(&mut ast);
assert!(!result.errors.is_empty());
assert!(result
.errors
.iter()
.any(|e| matches!(e, Error::NotInScope(_))));
}
#[test]
fn test_alias_valid_type_success() {
let mut ast = crate::parsing::parse_string(
r#"
type Address = Bytes;
type Amount = Int;
type ValidAlias = Address;
"#,
)
.unwrap();
let result = analyze(&mut ast);
assert!(result.errors.is_empty());
}
#[test]
fn test_min_utxo_undefined_output_error() {
let mut ast = crate::parsing::parse_string(
r#"
party Alice;
tx test() {
output {
to: Alice,
amount: min_utxo(nonexistent_output),
}
}
"#,
)
.unwrap();
let result = analyze(&mut ast);
assert!(!result.errors.is_empty());
}
#[test]
fn test_time_and_slot_conversion() {
let mut ast = crate::parsing::parse_string(
r#"
party Sender;
type TimestampDatum {
slot_time: Int,
time: Int,
}
tx create_timestamp_tx() {
input source {
from: Sender,
min_amount: Ada(2000000),
}
output timestamp_output {
to: Sender,
amount: source - fees,
datum: TimestampDatum {
slot_time: time_to_slot(1666716638000),
time: slot_to_time(60638),
},
}
}
"#,
)
.unwrap();
let result = analyze(&mut ast);
assert!(result.errors.is_empty());
}
#[test]
fn test_optional_output_with_datum_error() {
let mut ast = crate::parsing::parse_string(
r#"
party Alice;
type MyDatum {
field1: Int,
}
tx test() {
output ? my_output {
to: Alice,
amount: Ada(1),
datum: MyDatum { field1: 1, },
}
}
"#,
)
.unwrap();
let report = analyze(&mut ast);
assert!(!report.errors.is_empty());
assert!(report
.errors
.iter()
.any(|e| matches!(e, Error::InvalidOptionalOutput(_))));
}
#[test]
fn test_optional_output_ok() {
let mut ast = crate::parsing::parse_string(
r#"
party Alice;
tx test() {
output ? my_output {
to: Alice,
amount: Ada(0),
}
}
"#,
)
.unwrap();
let report = analyze(&mut ast);
assert!(report.errors.is_empty());
}
#[test]
fn test_metadata_value_size_validation_string_within_limit() {
let mut ast = crate::parsing::parse_string(
r#"
tx test() {
metadata {
123: "This is a short string that is within the 64-byte limit",
}
}
"#,
)
.unwrap();
let result = analyze(&mut ast);
assert!(
result.errors.is_empty(),
"Expected no errors for string within limit, but got: {:?}",
result.errors
);
}
#[test]
fn test_metadata_value_size_validation_string_exceeds_limit() {
let mut ast = crate::parsing::parse_string(
r#"
tx test() {
metadata {
123: "This is a very long string that definitely exceeds the 64-byte limit here",
}
}
"#,
)
.unwrap();
let result = analyze(&mut ast);
assert_eq!(result.errors.len(), 1);
match &result.errors[0] {
Error::MetadataSizeLimitExceeded(error) => {
assert_eq!(error.size, 73);
}
_ => panic!(
"Expected MetadataSizeLimitExceeded error, got: {:?}",
result.errors[0]
),
}
}
#[test]
fn test_metadata_value_size_validation_hex_string_within_limit() {
let mut ast = crate::parsing::parse_string(
r#"
tx test() {
metadata {
123: 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef,
}
}
"#,
)
.unwrap();
let result = analyze(&mut ast);
assert!(
result.errors.is_empty(),
"Expected no errors for hex string within limit, but got: {:?}",
result.errors
);
}
#[test]
fn test_metadata_value_size_validation_hex_string_exceeds_limit() {
let mut ast = crate::parsing::parse_string(
r#"
tx test() {
metadata {
123: 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12,
}
}
"#,
)
.unwrap();
let result = analyze(&mut ast);
assert_eq!(result.errors.len(), 1);
match &result.errors[0] {
Error::MetadataSizeLimitExceeded(error) => {
assert_eq!(error.size, 65);
}
_ => panic!(
"Expected MetadataSizeLimitExceeded error, got: {:?}",
result.errors[0]
),
}
}
#[test]
fn test_metadata_value_size_validation_multiple_fields() {
let mut ast = crate::parsing::parse_string(
r#"
tx test() {
metadata {
123: "Short string",
456: "This is a very long string that definitely exceeds the 64-byte limit here",
789: "Another short one",
}
}
"#,
)
.unwrap();
let result = analyze(&mut ast);
assert_eq!(result.errors.len(), 1);
match &result.errors[0] {
Error::MetadataSizeLimitExceeded(error) => {
assert_eq!(error.size, 73);
}
_ => panic!(
"Expected MetadataSizeLimitExceeded error, got: {:?}",
result.errors[0]
),
}
}
#[test]
fn test_metadata_value_size_validation_non_literal_expression() {
let mut ast = crate::parsing::parse_string(
r#"
party Alice;
tx test(my_param: Bytes) {
metadata {
123: my_param,
}
}
"#,
)
.unwrap();
let result = analyze(&mut ast);
let metadata_errors: Vec<_> = result
.errors
.iter()
.filter(|e| matches!(e, Error::MetadataSizeLimitExceeded(_)))
.collect();
assert!(
metadata_errors.is_empty(),
"Expected no metadata size errors for non-literal expressions"
);
}
#[test]
fn test_metadata_key_type_validation_string_key() {
let mut ast = crate::parsing::parse_string(
r#"
tx test() {
metadata {
"invalid_key": "some value",
}
}
"#,
)
.unwrap();
let result = analyze(&mut ast);
assert_eq!(result.errors.len(), 1);
match &result.errors[0] {
Error::MetadataInvalidKeyType(error) => {
assert_eq!(error.key_type, "string");
}
_ => panic!(
"Expected MetadataInvalidKeyType error, got: {:?}",
result.errors[0]
),
}
}
#[test]
fn test_metadata_key_type_validation_identifier_with_int_type() {
let mut ast = crate::parsing::parse_string(
r#"
tx test(my_key: Int) {
metadata {
my_key: "valid value",
}
}
"#,
)
.unwrap();
let result = analyze(&mut ast);
let key_type_errors: Vec<_> = result
.errors
.iter()
.filter(|e| matches!(e, Error::MetadataInvalidKeyType(_)))
.collect();
assert!(
key_type_errors.is_empty(),
"Expected no key type errors for Int parameter used as key"
);
}
#[test]
fn test_metadata_key_type_validation_identifier_with_wrong_type() {
let mut ast = crate::parsing::parse_string(
r#"
tx test(my_key: Bytes) {
metadata {
my_key: "some value",
}
}
"#,
)
.unwrap();
let result = analyze(&mut ast);
assert_eq!(result.errors.len(), 1);
match &result.errors[0] {
Error::MetadataInvalidKeyType(error) => {
assert!(error.key_type.contains("identifier of type Bytes"));
}
_ => panic!(
"Expected MetadataInvalidKeyType error, got: {:?}",
result.errors[0]
),
}
}
}