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