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::Unit => Ok(ir::Type::Unit),
249            ast::Type::Int => Ok(ir::Type::Int),
250            ast::Type::Bool => Ok(ir::Type::Bool),
251            ast::Type::Bytes => Ok(ir::Type::Bytes),
252            ast::Type::Address => Ok(ir::Type::Address),
253            ast::Type::UtxoRef => Ok(ir::Type::UtxoRef),
254            ast::Type::AnyAsset => Ok(ir::Type::AnyAsset),
255            ast::Type::Custom(x) => Ok(ir::Type::Custom(x.value.clone())),
256        }
257    }
258}
259
260impl IntoLower for ast::DataBinaryOp {
261    type Output = ir::BinaryOp;
262
263    fn into_lower(&self) -> Result<Self::Output, Error> {
264        let left = self.left.into_lower()?;
265        let right = self.right.into_lower()?;
266
267        Ok(ir::BinaryOp {
268            left,
269            right,
270            op: match self.operator {
271                ast::BinaryOperator::Add => ir::BinaryOpKind::Add,
272                ast::BinaryOperator::Subtract => ir::BinaryOpKind::Sub,
273            },
274        })
275    }
276}
277
278impl IntoLower for ast::PropertyAccess {
279    type Output = ir::Expression;
280
281    fn into_lower(&self) -> Result<Self::Output, Error> {
282        let mut object = self.object.into_lower()?;
283
284        for field in self.path.iter() {
285            object = ir::Expression::EvalProperty(Box::new(ir::PropertyAccess {
286                object: Box::new(object),
287                field: field.value.clone(),
288            }));
289        }
290
291        Ok(object)
292    }
293}
294
295impl IntoLower for ast::DataExpr {
296    type Output = ir::Expression;
297
298    fn into_lower(&self) -> Result<Self::Output, Error> {
299        let out = match self {
300            ast::DataExpr::None => ir::Expression::None,
301            ast::DataExpr::Number(x) => Self::Output::Number(*x as i128),
302            ast::DataExpr::Bool(x) => ir::Expression::Bool(*x),
303            ast::DataExpr::String(x) => ir::Expression::Bytes(x.value.as_bytes().to_vec()),
304            ast::DataExpr::HexString(x) => ir::Expression::Bytes(hex::decode(&x.value)?),
305            ast::DataExpr::Constructor(x) => ir::Expression::Struct(x.into_lower()?),
306            ast::DataExpr::Unit => ir::Expression::Struct(ir::StructExpr::unit()),
307            ast::DataExpr::Identifier(x) => x.into_lower()?,
308            ast::DataExpr::BinaryOp(x) => ir::Expression::EvalCustom(Box::new(x.into_lower()?)),
309            ast::DataExpr::PropertyAccess(x) => x.into_lower()?,
310        };
311
312        Ok(out)
313    }
314}
315
316impl IntoLower for ast::AssetConstructor {
317    type Output = ir::Expression;
318
319    fn into_lower(&self) -> Result<Self::Output, Error> {
320        let asset_def = coerce_identifier_into_asset_def(&self.r#type)?;
321
322        let asset_name = ir::Expression::Bytes(asset_def.asset_name.as_bytes().to_vec());
323
324        let amount = self.amount.into_lower()?;
325
326        Ok(ir::Expression::Assets(vec![ir::AssetExpr {
327            policy: hex::decode(&asset_def.policy.value)?,
328            asset_name,
329            amount,
330        }]))
331    }
332}
333
334impl IntoLower for ast::AssetBinaryOp {
335    type Output = ir::BinaryOp;
336
337    fn into_lower(&self) -> Result<Self::Output, Error> {
338        let left = self.left.into_lower()?;
339        let right = self.right.into_lower()?;
340
341        Ok(ir::BinaryOp {
342            left,
343            right,
344            op: match self.operator {
345                ast::BinaryOperator::Add => ir::BinaryOpKind::Add,
346                ast::BinaryOperator::Subtract => ir::BinaryOpKind::Sub,
347            },
348        })
349    }
350}
351
352impl IntoLower for ast::AssetExpr {
353    type Output = ir::Expression;
354
355    fn into_lower(&self) -> Result<Self::Output, Error> {
356        match self {
357            ast::AssetExpr::Constructor(x) => x.into_lower(),
358            ast::AssetExpr::BinaryOp(x) => {
359                Ok(ir::Expression::EvalCustom(Box::new(x.into_lower()?)))
360            }
361            ast::AssetExpr::Identifier(x) => coerce_identifier_into_asset_expr(x),
362            ast::AssetExpr::PropertyAccess(_x) => todo!(),
363        }
364    }
365}
366
367impl IntoLower for ast::AddressExpr {
368    type Output = ir::Expression;
369
370    fn into_lower(&self) -> Result<Self::Output, Error> {
371        match self {
372            ast::AddressExpr::String(x) => Ok(ir::Expression::String(x.value.clone())),
373            ast::AddressExpr::HexString(x) => Ok(ir::Expression::Bytes(hex::decode(&x.value)?)),
374            ast::AddressExpr::Identifier(x) => lower_into_address_expr(x),
375        }
376    }
377}
378
379impl IntoLower for ast::InputBlockField {
380    type Output = ir::Expression;
381
382    fn into_lower(&self) -> Result<Self::Output, Error> {
383        match self {
384            ast::InputBlockField::From(x) => x.into_lower(),
385            ast::InputBlockField::DatumIs(_) => todo!(),
386            ast::InputBlockField::MinAmount(x) => x.into_lower(),
387            ast::InputBlockField::Redeemer(x) => x.into_lower(),
388            ast::InputBlockField::Ref(x) => x.into_lower(),
389        }
390    }
391}
392
393impl IntoLower for ast::InputBlock {
394    type Output = ir::Input;
395
396    fn into_lower(&self) -> Result<Self::Output, Error> {
397        let from = self.find("from");
398        let min_amount = self.find("min_amount");
399        let r#ref = self.find("ref");
400
401        let policy = from
402            .and_then(ast::InputBlockField::as_address_expr)
403            .and_then(ast::AddressExpr::as_identifier)
404            .and_then(|x| x.symbol.as_ref())
405            .and_then(|x| x.as_policy_def())
406            .map(|x| x.into_lower())
407            .transpose()?;
408
409        let input = ir::Input {
410            name: self.name.to_lowercase().clone(),
411            query: ir::InputQuery {
412                address: from.into_lower()?,
413                min_amount: min_amount.into_lower()?,
414                r#ref: r#ref.into_lower()?,
415            }
416            .into(),
417            refs: HashSet::new(),
418            redeemer: None,
419            policy,
420        };
421
422        Ok(input)
423    }
424}
425
426impl IntoLower for ast::OutputBlockField {
427    type Output = ir::Expression;
428
429    fn into_lower(&self) -> Result<Self::Output, Error> {
430        match self {
431            ast::OutputBlockField::To(x) => x.into_lower(),
432            ast::OutputBlockField::Amount(x) => x.into_lower(),
433            ast::OutputBlockField::Datum(x) => x.into_lower(),
434        }
435    }
436}
437
438impl IntoLower for ast::OutputBlock {
439    type Output = ir::Output;
440
441    fn into_lower(&self) -> Result<Self::Output, Error> {
442        Ok(ir::Output {
443            address: self.find("to").into_lower()?,
444            datum: self.find("datum").into_lower()?,
445            amount: self.find("amount").into_lower()?,
446        })
447    }
448}
449
450impl IntoLower for ast::MintBlockField {
451    type Output = ir::Expression;
452
453    fn into_lower(&self) -> Result<Self::Output, Error> {
454        match self {
455            ast::MintBlockField::Amount(x) => x.into_lower(),
456            ast::MintBlockField::Redeemer(x) => x.into_lower(),
457        }
458    }
459}
460
461impl IntoLower for ast::MintBlock {
462    type Output = ir::Mint;
463
464    fn into_lower(&self) -> Result<Self::Output, Error> {
465        Ok(ir::Mint {
466            amount: self.find("amount").into_lower()?,
467            redeemer: self.find("redeemer").into_lower()?,
468        })
469    }
470}
471
472impl IntoLower for ast::ChainSpecificBlock {
473    type Output = ir::AdHocDirective;
474
475    fn into_lower(&self) -> Result<Self::Output, Error> {
476        match self {
477            ast::ChainSpecificBlock::Cardano(x) => x.into_lower(),
478        }
479    }
480}
481
482pub fn lower_tx(ast: &ast::TxDef) -> Result<ir::Tx, Error> {
483    let ir = ir::Tx {
484        inputs: ast
485            .inputs
486            .iter()
487            .map(|x| x.into_lower())
488            .collect::<Result<Vec<_>, _>>()?,
489        outputs: ast
490            .outputs
491            .iter()
492            .map(|x| x.into_lower())
493            .collect::<Result<Vec<_>, _>>()?,
494        mint: ast.mint.as_ref().map(|x| x.into_lower()).transpose()?,
495        adhoc: ast
496            .adhoc
497            .iter()
498            .map(|x| x.into_lower())
499            .collect::<Result<Vec<_>, _>>()?,
500        fees: ir::Expression::FeeQuery,
501    };
502
503    Ok(ir)
504}
505
506/// Lowers the Tx3 language to the intermediate representation.
507///
508/// This function takes an AST and converts it into the intermediate
509/// representation (IR) of the Tx3 language.
510///
511/// # Arguments
512///
513/// * `ast` - The AST to lower
514///
515/// # Returns
516///
517/// * `Result<ir::Program, Error>` - The lowered intermediate representation
518pub fn lower(ast: &ast::Program, template: &str) -> Result<ir::Tx, Error> {
519    let tx = ast
520        .txs
521        .iter()
522        .find(|x| x.name == template)
523        .ok_or(Error::InvalidAst("tx not found".to_string()))?;
524
525    lower_tx(tx)
526}
527
528#[cfg(test)]
529mod tests {
530    use assert_json_diff::assert_json_eq;
531    use paste::paste;
532
533    use super::*;
534    use crate::parsing;
535
536    fn make_snapshot_if_missing(example: &str, name: &str, tx: &ir::Tx) {
537        let manifest_dir = env!("CARGO_MANIFEST_DIR");
538
539        let path = format!("{}/../../examples/{}.{}.tir", manifest_dir, example, name);
540
541        if !std::fs::exists(&path).unwrap() {
542            let ir = serde_json::to_string_pretty(tx).unwrap();
543            std::fs::write(&path, ir).unwrap();
544        }
545    }
546
547    fn test_lowering_example(example: &str) {
548        let manifest_dir = env!("CARGO_MANIFEST_DIR");
549        let mut program = parsing::parse_well_known_example(example);
550
551        crate::analyzing::analyze(&mut program).ok().unwrap();
552
553        for tx in program.txs.iter() {
554            let tir = lower(&program, &tx.name).unwrap();
555
556            make_snapshot_if_missing(example, &tx.name, &tir);
557
558            let tir_file = format!(
559                "{}/../../examples/{}.{}.tir",
560                manifest_dir, example, tx.name
561            );
562
563            let expected = std::fs::read_to_string(tir_file).unwrap();
564            let expected: ir::Tx = serde_json::from_str(&expected).unwrap();
565
566            assert_json_eq!(tir, expected);
567        }
568    }
569
570    #[macro_export]
571    macro_rules! test_lowering {
572        ($name:ident) => {
573            paste! {
574                #[test]
575                fn [<test_example_ $name>]() {
576                    test_lowering_example(stringify!($name));
577                }
578            }
579        };
580    }
581
582    test_lowering!(lang_tour);
583
584    test_lowering!(transfer);
585
586    test_lowering!(swap);
587
588    test_lowering!(asteria);
589
590    test_lowering!(vesting);
591
592    test_lowering!(faucet);
593}