tx3_lang/
lowering.rs

1//! Lowers the Tx3 language to the intermediate representation.
2//!
3//! This module takes an AST and performs lowering on it. It converts the AST
4//! into the intermediate representation (IR) of the Tx3 language.
5
6use std::collections::HashSet;
7use std::ops::Deref;
8
9use crate::ast;
10use crate::ir;
11
12#[derive(Debug, thiserror::Error)]
13pub enum Error {
14    #[error("missing analyze phase")]
15    MissingAnalyzePhase,
16
17    #[error("symbol '{0}' expected to be '{1}'")]
18    InvalidSymbol(String, &'static str),
19
20    #[error("symbol '{0}' expected to be of type '{1}'")]
21    InvalidSymbolType(String, &'static str),
22
23    #[error("invalid ast: {0}")]
24    InvalidAst(String),
25
26    #[error("failed to decode hex string: {0}")]
27    DecodeHexError(#[from] hex::FromHexError),
28}
29
30fn expect_type_def(ident: &ast::Identifier) -> Result<&ast::TypeDef, Error> {
31    let symbol = ident.symbol.as_ref().ok_or(Error::MissingAnalyzePhase)?;
32
33    symbol
34        .as_type_def()
35        .ok_or(Error::InvalidSymbol(ident.value.clone(), "TypeDef"))
36}
37
38fn expect_case_def(ident: &ast::Identifier) -> Result<&ast::VariantCase, Error> {
39    let symbol = ident.symbol.as_ref().ok_or(Error::MissingAnalyzePhase)?;
40
41    symbol
42        .as_variant_case()
43        .ok_or(Error::InvalidSymbol(ident.value.clone(), "VariantCase"))
44}
45
46#[allow(dead_code)]
47fn expect_field_def(ident: &ast::Identifier) -> Result<&ast::RecordField, Error> {
48    let symbol = ident.symbol.as_ref().ok_or(Error::MissingAnalyzePhase)?;
49
50    symbol
51        .as_field_def()
52        .ok_or(Error::InvalidSymbol(ident.value.clone(), "FieldDef"))
53}
54
55fn coerce_identifier_into_asset_def(identifier: &ast::Identifier) -> Result<ast::AssetDef, Error> {
56    match identifier.try_symbol()? {
57        ast::Symbol::AssetDef(x) => Ok(x.as_ref().clone()),
58        _ => Err(Error::InvalidSymbol(identifier.value.clone(), "AssetDef")),
59    }
60}
61
62fn coerce_identifier_into_asset_expr(
63    identifier: &ast::Identifier,
64) -> Result<ir::Expression, Error> {
65    match identifier.try_symbol()? {
66        ast::Symbol::Input(x, _) => Ok(ir::Expression::EvalInputAssets(x.clone())),
67        ast::Symbol::Fees => Ok(ir::Expression::FeeQuery),
68        ast::Symbol::ParamVar(name, ty) => match ty.deref() {
69            ast::Type::AnyAsset => Ok(ir::Expression::EvalParameter(
70                name.to_lowercase().clone(),
71                ir::Type::AnyAsset,
72            )),
73            _ => Err(Error::InvalidSymbolType(
74                identifier.value.clone(),
75                "AnyAsset",
76            )),
77        },
78        _ => Err(Error::InvalidSymbol(identifier.value.clone(), "AssetExpr")),
79    }
80}
81
82fn lower_into_address_expr(identifier: &ast::Identifier) -> Result<ir::Expression, Error> {
83    match identifier.try_symbol()? {
84        ast::Symbol::PolicyDef(x) => Ok(x.into_lower()?.hash),
85        ast::Symbol::PartyDef(x) => Ok(ir::Expression::EvalParameter(
86            x.name.to_lowercase().clone(),
87            ir::Type::Address,
88        )),
89        _ => Err(Error::InvalidSymbol(
90            identifier.value.clone(),
91            "AddressExpr",
92        )),
93    }
94}
95
96pub(crate) trait IntoLower {
97    type Output;
98
99    fn into_lower(&self) -> Result<Self::Output, Error>;
100}
101
102impl<T> IntoLower for Option<&T>
103where
104    T: IntoLower,
105{
106    type Output = Option<T::Output>;
107
108    fn into_lower(&self) -> Result<Self::Output, Error> {
109        self.map(|x| x.into_lower()).transpose()
110    }
111}
112
113impl<T> IntoLower for Box<T>
114where
115    T: IntoLower,
116{
117    type Output = T::Output;
118
119    fn into_lower(&self) -> Result<Self::Output, Error> {
120        self.as_ref().into_lower()
121    }
122}
123
124impl IntoLower for ast::Identifier {
125    type Output = ir::Expression;
126
127    fn into_lower(&self) -> Result<Self::Output, Error> {
128        let symbol = self.symbol.as_ref().expect("analyze phase must be run");
129
130        match symbol {
131            ast::Symbol::ParamVar(n, ty) => Ok(ir::Expression::EvalParameter(
132                n.to_lowercase().clone(),
133                ty.into_lower()?,
134            )),
135            ast::Symbol::PartyDef(x) => Ok(ir::Expression::EvalParameter(
136                x.name.to_lowercase().clone(),
137                ir::Type::Address,
138            )),
139            ast::Symbol::Input(n, _) => Ok(ir::Expression::EvalInputDatum(n.clone())),
140            _ => {
141                dbg!(&self);
142                todo!();
143            }
144        }
145    }
146}
147
148impl IntoLower for ast::DatumConstructor {
149    type Output = ir::StructExpr;
150
151    fn into_lower(&self) -> Result<Self::Output, Error> {
152        let type_def = expect_type_def(&self.r#type)?;
153
154        let constructor = type_def
155            .find_case_index(&self.case.name.value)
156            .ok_or(Error::InvalidAst("case not found".to_string()))?;
157
158        let case_def = expect_case_def(&self.case.name)?;
159
160        let mut fields = vec![];
161
162        for field_def in case_def.fields.iter() {
163            let value = self.case.find_field_value(&field_def.name);
164
165            if let Some(value) = value {
166                fields.push(value.into_lower()?);
167            } else {
168                let spread_target = self
169                    .case
170                    .spread
171                    .as_ref()
172                    .expect("spread must be set for missing explicit field")
173                    .into_lower()?;
174
175                fields.push(ir::Expression::EvalProperty(Box::new(ir::PropertyAccess {
176                    object: Box::new(spread_target),
177                    field: field_def.name.clone(),
178                })));
179            }
180        }
181
182        Ok(ir::StructExpr {
183            constructor,
184            fields,
185        })
186    }
187}
188
189impl IntoLower for ast::PolicyField {
190    type Output = ir::Expression;
191
192    fn into_lower(&self) -> Result<Self::Output, Error> {
193        match self {
194            ast::PolicyField::Hash(x) => x.into_lower(),
195            ast::PolicyField::Script(x) => x.into_lower(),
196            ast::PolicyField::Ref(x) => x.into_lower(),
197        }
198    }
199}
200
201impl IntoLower for ast::PolicyDef {
202    type Output = ir::PolicyExpr;
203
204    fn into_lower(&self) -> Result<Self::Output, Error> {
205        match &self.value {
206            ast::PolicyValue::Assign(x) => Ok(ir::PolicyExpr {
207                name: self.name.clone(),
208                hash: ir::Expression::Hash(hex::decode(&x.value)?),
209                script: None,
210            }),
211            ast::PolicyValue::Constructor(x) => {
212                let hash = x
213                    .find_field("hash")
214                    .ok_or(Error::InvalidAst("Missing policy hash".to_string()))?
215                    .into_lower()?;
216
217                let ref_field = x.find_field("ref");
218                let script_field = x.find_field("script");
219
220                let script = match (ref_field, script_field) {
221                    (Some(x), None) => Some(ir::ScriptSource::UtxoRef {
222                        r#ref: x.into_lower()?,
223                        source: None,
224                    }),
225                    (None, Some(x)) => Some(ir::ScriptSource::Embedded(x.into_lower()?)),
226                    (Some(r#ref), Some(source)) => Some(ir::ScriptSource::UtxoRef {
227                        r#ref: r#ref.into_lower()?,
228                        source: Some(source.into_lower()?),
229                    }),
230                    (None, None) => None,
231                };
232
233                Ok(ir::PolicyExpr {
234                    name: self.name.clone(),
235                    hash,
236                    script,
237                })
238            }
239        }
240    }
241}
242
243impl IntoLower for ast::Type {
244    type Output = ir::Type;
245
246    fn into_lower(&self) -> Result<Self::Output, Error> {
247        match self {
248            ast::Type::Undefined => Ok(ir::Type::Undefined),
249            ast::Type::Unit => Ok(ir::Type::Unit),
250            ast::Type::Int => Ok(ir::Type::Int),
251            ast::Type::Bool => Ok(ir::Type::Bool),
252            ast::Type::Bytes => Ok(ir::Type::Bytes),
253            ast::Type::Address => Ok(ir::Type::Address),
254            ast::Type::UtxoRef => Ok(ir::Type::UtxoRef),
255            ast::Type::AnyAsset => Ok(ir::Type::AnyAsset),
256            ast::Type::Custom(x) => Ok(ir::Type::Custom(x.value.clone())),
257        }
258    }
259}
260
261impl IntoLower for ast::DataBinaryOp {
262    type Output = ir::BinaryOp;
263
264    fn into_lower(&self) -> Result<Self::Output, Error> {
265        let left = self.left.into_lower()?;
266        let right = self.right.into_lower()?;
267
268        Ok(ir::BinaryOp {
269            left,
270            right,
271            op: match self.operator {
272                ast::BinaryOperator::Add => ir::BinaryOpKind::Add,
273                ast::BinaryOperator::Subtract => ir::BinaryOpKind::Sub,
274            },
275        })
276    }
277}
278
279impl IntoLower for ast::PropertyAccess {
280    type Output = ir::Expression;
281
282    fn into_lower(&self) -> Result<Self::Output, Error> {
283        let mut object = self.object.into_lower()?;
284
285        for field in self.path.iter() {
286            object = ir::Expression::EvalProperty(Box::new(ir::PropertyAccess {
287                object: Box::new(object),
288                field: field.value.clone(),
289            }));
290        }
291
292        Ok(object)
293    }
294}
295
296impl IntoLower for ast::DataExpr {
297    type Output = ir::Expression;
298
299    fn into_lower(&self) -> Result<Self::Output, Error> {
300        let out = match self {
301            ast::DataExpr::None => ir::Expression::None,
302            ast::DataExpr::Number(x) => Self::Output::Number(*x as i128),
303            ast::DataExpr::Bool(x) => ir::Expression::Bool(*x),
304            ast::DataExpr::String(x) => ir::Expression::Bytes(x.value.as_bytes().to_vec()),
305            ast::DataExpr::HexString(x) => ir::Expression::Bytes(hex::decode(&x.value)?),
306            ast::DataExpr::Constructor(x) => ir::Expression::Struct(x.into_lower()?),
307            ast::DataExpr::Unit => ir::Expression::Struct(ir::StructExpr::unit()),
308            ast::DataExpr::Identifier(x) => x.into_lower()?,
309            ast::DataExpr::BinaryOp(x) => ir::Expression::EvalCustom(Box::new(x.into_lower()?)),
310            ast::DataExpr::PropertyAccess(x) => x.into_lower()?,
311        };
312
313        Ok(out)
314    }
315}
316
317impl IntoLower for ast::StaticAssetConstructor {
318    type Output = ir::Expression;
319
320    fn into_lower(&self) -> Result<Self::Output, Error> {
321        let asset_def = coerce_identifier_into_asset_def(&self.r#type)?;
322
323        let policy = match asset_def.policy {
324            Some(x) => ir::Expression::Bytes(hex::decode(&x.value)?),
325            None => ir::Expression::None,
326        };
327
328        let asset_name = match asset_def.asset_name {
329            Some(x) => ir::Expression::Bytes(x.as_bytes().to_vec()),
330            None => ir::Expression::None,
331        };
332
333        let amount = self.amount.into_lower()?;
334
335        Ok(ir::Expression::Assets(vec![ir::AssetExpr {
336            policy,
337            asset_name,
338            amount,
339        }]))
340    }
341}
342
343impl IntoLower for ast::AnyAssetConstructor {
344    type Output = ir::Expression;
345
346    fn into_lower(&self) -> Result<Self::Output, Error> {
347        let policy = self.policy.into_lower()?;
348        let asset_name = self.asset_name.into_lower()?;
349        let amount = self.amount.into_lower()?;
350
351        Ok(ir::Expression::Assets(vec![ir::AssetExpr {
352            policy,
353            asset_name,
354            amount,
355        }]))
356    }
357}
358
359impl IntoLower for ast::AssetBinaryOp {
360    type Output = ir::BinaryOp;
361
362    fn into_lower(&self) -> Result<Self::Output, Error> {
363        let left = self.left.into_lower()?;
364        let right = self.right.into_lower()?;
365
366        Ok(ir::BinaryOp {
367            left,
368            right,
369            op: match self.operator {
370                ast::BinaryOperator::Add => ir::BinaryOpKind::Add,
371                ast::BinaryOperator::Subtract => ir::BinaryOpKind::Sub,
372            },
373        })
374    }
375}
376
377impl IntoLower for ast::AssetExpr {
378    type Output = ir::Expression;
379
380    fn into_lower(&self) -> Result<Self::Output, Error> {
381        match self {
382            ast::AssetExpr::StaticConstructor(x) => x.into_lower(),
383            ast::AssetExpr::AnyConstructor(x) => x.into_lower(),
384            ast::AssetExpr::BinaryOp(x) => {
385                Ok(ir::Expression::EvalCustom(Box::new(x.into_lower()?)))
386            }
387            ast::AssetExpr::Identifier(x) => coerce_identifier_into_asset_expr(x),
388            ast::AssetExpr::PropertyAccess(_x) => todo!(),
389        }
390    }
391}
392
393impl IntoLower for ast::AddressExpr {
394    type Output = ir::Expression;
395
396    fn into_lower(&self) -> Result<Self::Output, Error> {
397        match self {
398            ast::AddressExpr::String(x) => Ok(ir::Expression::String(x.value.clone())),
399            ast::AddressExpr::HexString(x) => Ok(ir::Expression::Bytes(hex::decode(&x.value)?)),
400            ast::AddressExpr::Identifier(x) => lower_into_address_expr(x),
401        }
402    }
403}
404
405impl IntoLower for ast::InputBlockField {
406    type Output = ir::Expression;
407
408    fn into_lower(&self) -> Result<Self::Output, Error> {
409        match self {
410            ast::InputBlockField::From(x) => x.into_lower(),
411            ast::InputBlockField::DatumIs(_) => todo!(),
412            ast::InputBlockField::MinAmount(x) => x.into_lower(),
413            ast::InputBlockField::Redeemer(x) => x.into_lower(),
414            ast::InputBlockField::Ref(x) => x.into_lower(),
415        }
416    }
417}
418
419impl IntoLower for ast::InputBlock {
420    type Output = ir::Input;
421
422    fn into_lower(&self) -> Result<Self::Output, Error> {
423        let from = self.find("from");
424        let min_amount = self.find("min_amount");
425        let r#ref = self.find("ref");
426        let redeemer = self.find("redeemer");
427
428        let policy = from
429            .and_then(ast::InputBlockField::as_address_expr)
430            .and_then(ast::AddressExpr::as_identifier)
431            .and_then(|x| x.symbol.as_ref())
432            .and_then(|x| x.as_policy_def())
433            .map(|x| x.into_lower())
434            .transpose()?;
435
436        let input = ir::Input {
437            name: self.name.to_lowercase().clone(),
438            query: ir::InputQuery {
439                address: from.into_lower()?,
440                min_amount: min_amount.into_lower()?,
441                r#ref: r#ref.into_lower()?,
442            }
443            .into(),
444            refs: HashSet::new(),
445            redeemer: redeemer.into_lower()?,
446            policy,
447        };
448
449        Ok(input)
450    }
451}
452
453impl IntoLower for ast::OutputBlockField {
454    type Output = ir::Expression;
455
456    fn into_lower(&self) -> Result<Self::Output, Error> {
457        match self {
458            ast::OutputBlockField::To(x) => x.into_lower(),
459            ast::OutputBlockField::Amount(x) => x.into_lower(),
460            ast::OutputBlockField::Datum(x) => x.into_lower(),
461        }
462    }
463}
464
465impl IntoLower for ast::OutputBlock {
466    type Output = ir::Output;
467
468    fn into_lower(&self) -> Result<Self::Output, Error> {
469        Ok(ir::Output {
470            address: self.find("to").into_lower()?,
471            datum: self.find("datum").into_lower()?,
472            amount: self.find("amount").into_lower()?,
473        })
474    }
475}
476
477impl IntoLower for ast::MintBlockField {
478    type Output = ir::Expression;
479
480    fn into_lower(&self) -> Result<Self::Output, Error> {
481        match self {
482            ast::MintBlockField::Amount(x) => x.into_lower(),
483            ast::MintBlockField::Redeemer(x) => x.into_lower(),
484        }
485    }
486}
487
488impl IntoLower for ast::MintBlock {
489    type Output = ir::Mint;
490
491    fn into_lower(&self) -> Result<Self::Output, Error> {
492        Ok(ir::Mint {
493            amount: self.find("amount").into_lower()?,
494            redeemer: self.find("redeemer").into_lower()?,
495        })
496    }
497}
498
499impl IntoLower for ast::ChainSpecificBlock {
500    type Output = ir::AdHocDirective;
501
502    fn into_lower(&self) -> Result<Self::Output, Error> {
503        match self {
504            ast::ChainSpecificBlock::Cardano(x) => x.into_lower(),
505        }
506    }
507}
508
509pub fn lower_tx(ast: &ast::TxDef) -> Result<ir::Tx, Error> {
510    let ir = ir::Tx {
511        inputs: ast
512            .inputs
513            .iter()
514            .map(|x| x.into_lower())
515            .collect::<Result<Vec<_>, _>>()?,
516        outputs: ast
517            .outputs
518            .iter()
519            .map(|x| x.into_lower())
520            .collect::<Result<Vec<_>, _>>()?,
521        mint: ast.mint.as_ref().map(|x| x.into_lower()).transpose()?,
522        adhoc: ast
523            .adhoc
524            .iter()
525            .map(|x| x.into_lower())
526            .collect::<Result<Vec<_>, _>>()?,
527        fees: ir::Expression::FeeQuery,
528    };
529
530    Ok(ir)
531}
532
533/// Lowers the Tx3 language to the intermediate representation.
534///
535/// This function takes an AST and converts it into the intermediate
536/// representation (IR) of the Tx3 language.
537///
538/// # Arguments
539///
540/// * `ast` - The AST to lower
541///
542/// # Returns
543///
544/// * `Result<ir::Program, Error>` - The lowered intermediate representation
545pub fn lower(ast: &ast::Program, template: &str) -> Result<ir::Tx, Error> {
546    let tx = ast
547        .txs
548        .iter()
549        .find(|x| x.name == template)
550        .ok_or(Error::InvalidAst("tx not found".to_string()))?;
551
552    lower_tx(tx)
553}
554
555#[cfg(test)]
556mod tests {
557    use assert_json_diff::assert_json_eq;
558    use paste::paste;
559
560    use super::*;
561    use crate::{
562        ast::Span,
563        parsing::{self, AstNode},
564    };
565
566    fn make_snapshot_if_missing(example: &str, name: &str, tx: &ir::Tx) {
567        let manifest_dir = env!("CARGO_MANIFEST_DIR");
568
569        let path = format!("{}/../../examples/{}.{}.tir", manifest_dir, example, name);
570
571        if !std::fs::exists(&path).unwrap() {
572            let ir = serde_json::to_string_pretty(tx).unwrap();
573            std::fs::write(&path, ir).unwrap();
574        }
575    }
576
577    fn test_lowering_example(example: &str) {
578        let manifest_dir = env!("CARGO_MANIFEST_DIR");
579        let mut program = parsing::parse_well_known_example(example);
580
581        crate::analyzing::analyze(&mut program).ok().unwrap();
582
583        for tx in program.txs.iter() {
584            let tir = lower(&program, &tx.name).unwrap();
585
586            make_snapshot_if_missing(example, &tx.name, &tir);
587
588            let tir_file = format!(
589                "{}/../../examples/{}.{}.tir",
590                manifest_dir, example, tx.name
591            );
592
593            let expected = std::fs::read_to_string(tir_file).unwrap();
594            let expected: ir::Tx = serde_json::from_str(&expected).unwrap();
595
596            assert_json_eq!(tir, expected);
597        }
598    }
599
600    #[macro_export]
601    macro_rules! test_lowering {
602        ($name:ident) => {
603            paste! {
604                #[test]
605                fn [<test_example_ $name>]() {
606                    test_lowering_example(stringify!($name));
607                }
608            }
609        };
610    }
611
612    test_lowering!(lang_tour);
613
614    test_lowering!(transfer);
615
616    test_lowering!(swap);
617
618    test_lowering!(asteria);
619
620    test_lowering!(vesting);
621
622    test_lowering!(faucet);
623}