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::StructConstructor {
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::List(_) => Ok(ir::Type::List),
270            ast::Type::Custom(x) => Ok(ir::Type::Custom(x.value.clone())),
271        }
272    }
273}
274
275impl IntoLower for ast::DataBinaryOp {
276    type Output = ir::BinaryOp;
277
278    fn into_lower(&self) -> Result<Self::Output, Error> {
279        let left = self.left.into_lower()?;
280        let right = self.right.into_lower()?;
281
282        Ok(ir::BinaryOp {
283            left,
284            right,
285            op: match self.operator {
286                ast::BinaryOperator::Add => ir::BinaryOpKind::Add,
287                ast::BinaryOperator::Subtract => ir::BinaryOpKind::Sub,
288            },
289        })
290    }
291}
292
293impl IntoLower for ast::PropertyAccess {
294    type Output = ir::Expression;
295
296    fn into_lower(&self) -> Result<Self::Output, Error> {
297        let mut object = self.object.into_lower()?;
298
299        for field in self.path.iter() {
300            object = ir::Expression::EvalProperty(Box::new(ir::PropertyAccess {
301                object: Box::new(object),
302                field: field.value.clone(),
303            }));
304        }
305
306        Ok(object)
307    }
308}
309
310impl IntoLower for ast::ListConstructor {
311    type Output = Vec<ir::Expression>;
312
313    fn into_lower(&self) -> Result<Self::Output, Error> {
314        let elements = self
315            .elements
316            .iter()
317            .map(|x| x.into_lower())
318            .collect::<Result<Vec<_>, _>>()?;
319
320        Ok(elements)
321    }
322}
323
324impl IntoLower for ast::DataExpr {
325    type Output = ir::Expression;
326
327    fn into_lower(&self) -> Result<Self::Output, Error> {
328        let out = match self {
329            ast::DataExpr::None => ir::Expression::None,
330            ast::DataExpr::Number(x) => Self::Output::Number(*x as i128),
331            ast::DataExpr::Bool(x) => ir::Expression::Bool(*x),
332            ast::DataExpr::String(x) => ir::Expression::Bytes(x.value.as_bytes().to_vec()),
333            ast::DataExpr::HexString(x) => ir::Expression::Bytes(hex::decode(&x.value)?),
334            ast::DataExpr::StructConstructor(x) => ir::Expression::Struct(x.into_lower()?),
335            ast::DataExpr::ListConstructor(x) => ir::Expression::List(x.into_lower()?),
336            ast::DataExpr::Unit => ir::Expression::Struct(ir::StructExpr::unit()),
337            ast::DataExpr::Identifier(x) => x.into_lower()?,
338            ast::DataExpr::BinaryOp(x) => ir::Expression::EvalCustom(Box::new(x.into_lower()?)),
339            ast::DataExpr::PropertyAccess(x) => x.into_lower()?,
340            ast::DataExpr::UtxoRef(x) => x.into_lower()?,
341        };
342
343        Ok(out)
344    }
345}
346
347impl IntoLower for ast::StaticAssetConstructor {
348    type Output = ir::Expression;
349
350    fn into_lower(&self) -> Result<Self::Output, Error> {
351        let asset_def = coerce_identifier_into_asset_def(&self.r#type)?;
352
353        let policy = match asset_def.policy {
354            Some(x) => ir::Expression::Bytes(hex::decode(&x.value)?),
355            None => ir::Expression::None,
356        };
357
358        let asset_name = match asset_def.asset_name {
359            Some(x) => ir::Expression::Bytes(x.as_bytes().to_vec()),
360            None => ir::Expression::None,
361        };
362
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::AnyAssetConstructor {
374    type Output = ir::Expression;
375
376    fn into_lower(&self) -> Result<Self::Output, Error> {
377        let policy = self.policy.into_lower()?;
378        let asset_name = self.asset_name.into_lower()?;
379        let amount = self.amount.into_lower()?;
380
381        Ok(ir::Expression::Assets(vec![ir::AssetExpr {
382            policy,
383            asset_name,
384            amount,
385        }]))
386    }
387}
388
389impl IntoLower for ast::AssetBinaryOp {
390    type Output = ir::BinaryOp;
391
392    fn into_lower(&self) -> Result<Self::Output, Error> {
393        let left = self.left.into_lower()?;
394        let right = self.right.into_lower()?;
395
396        Ok(ir::BinaryOp {
397            left,
398            right,
399            op: match self.operator {
400                ast::BinaryOperator::Add => ir::BinaryOpKind::Add,
401                ast::BinaryOperator::Subtract => ir::BinaryOpKind::Sub,
402            },
403        })
404    }
405}
406
407impl IntoLower for ast::AssetExpr {
408    type Output = ir::Expression;
409
410    fn into_lower(&self) -> Result<Self::Output, Error> {
411        match self {
412            ast::AssetExpr::StaticConstructor(x) => x.into_lower(),
413            ast::AssetExpr::AnyConstructor(x) => x.into_lower(),
414            ast::AssetExpr::BinaryOp(x) => {
415                Ok(ir::Expression::EvalCustom(Box::new(x.into_lower()?)))
416            }
417            ast::AssetExpr::Identifier(x) => coerce_identifier_into_asset_expr(x),
418            ast::AssetExpr::PropertyAccess(_x) => todo!(),
419        }
420    }
421}
422
423impl IntoLower for ast::AddressExpr {
424    type Output = ir::Expression;
425
426    fn into_lower(&self) -> Result<Self::Output, Error> {
427        match self {
428            ast::AddressExpr::String(x) => Ok(ir::Expression::String(x.value.clone())),
429            ast::AddressExpr::HexString(x) => Ok(ir::Expression::Bytes(hex::decode(&x.value)?)),
430            ast::AddressExpr::Identifier(x) => lower_into_address_expr(x),
431        }
432    }
433}
434
435impl IntoLower for ast::InputBlockField {
436    type Output = ir::Expression;
437
438    fn into_lower(&self) -> Result<Self::Output, Error> {
439        match self {
440            ast::InputBlockField::From(x) => x.into_lower(),
441            ast::InputBlockField::DatumIs(_) => todo!(),
442            ast::InputBlockField::MinAmount(x) => x.into_lower(),
443            ast::InputBlockField::Redeemer(x) => x.into_lower(),
444            ast::InputBlockField::Ref(x) => x.into_lower(),
445        }
446    }
447}
448
449impl IntoLower for ast::InputBlock {
450    type Output = ir::Input;
451
452    fn into_lower(&self) -> Result<Self::Output, Error> {
453        let from = self.find("from");
454        let min_amount = self.find("min_amount");
455        let r#ref = self.find("ref");
456        let redeemer = self.find("redeemer");
457
458        let policy = from
459            .and_then(ast::InputBlockField::as_address_expr)
460            .and_then(ast::AddressExpr::as_identifier)
461            .and_then(|x| x.symbol.as_ref())
462            .and_then(|x| x.as_policy_def())
463            .map(|x| x.into_lower())
464            .transpose()?;
465
466        let input = ir::Input {
467            name: self.name.to_lowercase().clone(),
468            query: ir::InputQuery {
469                address: from.into_lower()?,
470                min_amount: min_amount.into_lower()?,
471                r#ref: r#ref.into_lower()?,
472            }
473            .into(),
474            refs: HashSet::new(),
475            redeemer: redeemer.into_lower()?,
476            policy,
477        };
478
479        Ok(input)
480    }
481}
482
483impl IntoLower for ast::OutputBlockField {
484    type Output = ir::Expression;
485
486    fn into_lower(&self) -> Result<Self::Output, Error> {
487        match self {
488            ast::OutputBlockField::To(x) => x.into_lower(),
489            ast::OutputBlockField::Amount(x) => x.into_lower(),
490            ast::OutputBlockField::Datum(x) => x.into_lower(),
491        }
492    }
493}
494
495impl IntoLower for ast::OutputBlock {
496    type Output = ir::Output;
497
498    fn into_lower(&self) -> Result<Self::Output, Error> {
499        Ok(ir::Output {
500            address: self.find("to").into_lower()?,
501            datum: self.find("datum").into_lower()?,
502            amount: self.find("amount").into_lower()?,
503        })
504    }
505}
506
507impl IntoLower for ast::MintBlockField {
508    type Output = ir::Expression;
509
510    fn into_lower(&self) -> Result<Self::Output, Error> {
511        match self {
512            ast::MintBlockField::Amount(x) => x.into_lower(),
513            ast::MintBlockField::Redeemer(x) => x.into_lower(),
514        }
515    }
516}
517
518impl IntoLower for ast::MintBlock {
519    type Output = ir::Mint;
520
521    fn into_lower(&self) -> Result<Self::Output, Error> {
522        Ok(ir::Mint {
523            amount: self.find("amount").into_lower()?,
524            redeemer: self.find("redeemer").into_lower()?,
525        })
526    }
527}
528
529impl IntoLower for ast::ChainSpecificBlock {
530    type Output = ir::AdHocDirective;
531
532    fn into_lower(&self) -> Result<Self::Output, Error> {
533        match self {
534            ast::ChainSpecificBlock::Cardano(x) => x.into_lower(),
535        }
536    }
537}
538
539impl IntoLower for ast::ReferenceBlock {
540    type Output = ir::Expression;
541
542    fn into_lower(&self) -> Result<Self::Output, Error> {
543        self.r#ref.into_lower()
544    }
545}
546
547impl IntoLower for ast::CollateralBlockField {
548    type Output = ir::Expression;
549
550    fn into_lower(&self) -> Result<Self::Output, Error> {
551        match self {
552            ast::CollateralBlockField::From(x) => x.into_lower(),
553            ast::CollateralBlockField::MinAmount(x) => x.into_lower(),
554            ast::CollateralBlockField::Ref(x) => x.into_lower(),
555        }
556    }
557}
558
559impl IntoLower for ast::CollateralBlock {
560    type Output = ir::Collateral;
561
562    fn into_lower(&self) -> Result<Self::Output, Error> {
563        let from = self.find("from");
564        let min_amount = self.find("min_amount");
565        let r#ref = self.find("ref");
566
567        let collateral = ir::Collateral {
568            query: ir::InputQuery {
569                address: from.into_lower()?,
570                min_amount: min_amount.into_lower()?,
571                r#ref: r#ref.into_lower()?,
572            }
573            .into(),
574        };
575
576        Ok(collateral)
577    }
578}
579
580pub fn lower_tx(ast: &ast::TxDef) -> Result<ir::Tx, Error> {
581    let ir = ir::Tx {
582        references: ast
583            .references
584            .iter()
585            .map(|x| x.into_lower())
586            .collect::<Result<Vec<_>, _>>()?,
587        inputs: ast
588            .inputs
589            .iter()
590            .map(|x| x.into_lower())
591            .collect::<Result<Vec<_>, _>>()?,
592        outputs: ast
593            .outputs
594            .iter()
595            .map(|x| x.into_lower())
596            .collect::<Result<Vec<_>, _>>()?,
597        mint: ast.mint.as_ref().map(|x| x.into_lower()).transpose()?,
598        adhoc: ast
599            .adhoc
600            .iter()
601            .map(|x| x.into_lower())
602            .collect::<Result<Vec<_>, _>>()?,
603        fees: ir::Expression::FeeQuery,
604        collateral: ast
605            .collateral
606            .iter()
607            .map(|x| x.into_lower())
608            .collect::<Result<Vec<_>, _>>()?,
609    };
610
611    Ok(ir)
612}
613
614/// Lowers the Tx3 language to the intermediate representation.
615///
616/// This function takes an AST and converts it into the intermediate
617/// representation (IR) of the Tx3 language.
618///
619/// # Arguments
620///
621/// * `ast` - The AST to lower
622///
623/// # Returns
624///
625/// * `Result<ir::Program, Error>` - The lowered intermediate representation
626pub fn lower(ast: &ast::Program, template: &str) -> Result<ir::Tx, Error> {
627    let tx = ast
628        .txs
629        .iter()
630        .find(|x| x.name == template)
631        .ok_or(Error::InvalidAst("tx not found".to_string()))?;
632
633    lower_tx(tx)
634}
635
636#[cfg(test)]
637mod tests {
638    use assert_json_diff::assert_json_eq;
639    use paste::paste;
640
641    use super::*;
642    use crate::parsing::{self};
643
644    fn make_snapshot_if_missing(example: &str, name: &str, tx: &ir::Tx) {
645        let manifest_dir = env!("CARGO_MANIFEST_DIR");
646
647        let path = format!("{}/../../examples/{}.{}.tir", manifest_dir, example, name);
648
649        if !std::fs::exists(&path).unwrap() {
650            let ir = serde_json::to_string_pretty(tx).unwrap();
651            std::fs::write(&path, ir).unwrap();
652        }
653    }
654
655    fn test_lowering_example(example: &str) {
656        let manifest_dir = env!("CARGO_MANIFEST_DIR");
657        let mut program = parsing::parse_well_known_example(example);
658
659        crate::analyzing::analyze(&mut program).ok().unwrap();
660
661        for tx in program.txs.iter() {
662            let tir = lower(&program, &tx.name).unwrap();
663
664            make_snapshot_if_missing(example, &tx.name, &tir);
665
666            let tir_file = format!(
667                "{}/../../examples/{}.{}.tir",
668                manifest_dir, example, tx.name
669            );
670
671            let expected = std::fs::read_to_string(tir_file).unwrap();
672            let expected: ir::Tx = serde_json::from_str(&expected).unwrap();
673
674            assert_json_eq!(tir, expected);
675        }
676    }
677
678    #[macro_export]
679    macro_rules! test_lowering {
680        ($name:ident) => {
681            paste! {
682                #[test]
683                fn [<test_example_ $name>]() {
684                    test_lowering_example(stringify!($name));
685                }
686            }
687        };
688    }
689
690    test_lowering!(lang_tour);
691
692    test_lowering!(transfer);
693
694    test_lowering!(swap);
695
696    test_lowering!(asteria);
697
698    test_lowering!(vesting);
699
700    test_lowering!(faucet);
701}