Skip to main content

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 crate::ast;
7use tx3_tir::model::core::{Type, UtxoRef};
8use tx3_tir::model::v1beta0 as ir;
9
10#[derive(Debug, thiserror::Error)]
11pub enum Error {
12    #[error("missing analyze phase for {0}")]
13    MissingAnalyzePhase(String),
14
15    #[error("symbol '{0}' expected to be '{1}'")]
16    InvalidSymbol(String, &'static str),
17
18    #[error("symbol '{0}' expected to be of type '{1}'")]
19    InvalidSymbolType(String, &'static str),
20
21    #[error("invalid ast: {0}")]
22    InvalidAst(String),
23
24    #[error("invalid property {0} on type {1:?}")]
25    InvalidProperty(String, String),
26
27    #[error("missing required field {0} for {1:?}")]
28    MissingRequiredField(String, &'static str),
29
30    #[error("failed to decode hex string {0}")]
31    DecodeHexError(String),
32}
33
34#[inline]
35fn hex_decode(s: &str) -> Result<Vec<u8>, Error> {
36    hex::decode(s).map_err(|_| Error::DecodeHexError(s.to_string()))
37}
38
39fn expect_type_def(ident: &ast::Identifier) -> Result<&ast::TypeDef, Error> {
40    let symbol = ident
41        .symbol
42        .as_ref()
43        .ok_or(Error::MissingAnalyzePhase(ident.value.clone()))?;
44
45    symbol
46        .as_type_def()
47        .ok_or(Error::InvalidSymbol(ident.value.clone(), "TypeDef"))
48}
49
50fn expect_alias_def(ident: &ast::Identifier) -> Result<&ast::AliasDef, Error> {
51    let symbol = ident
52        .symbol
53        .as_ref()
54        .ok_or(Error::MissingAnalyzePhase(ident.value.clone()))?;
55
56    symbol
57        .as_alias_def()
58        .ok_or(Error::InvalidSymbol(ident.value.clone(), "AliasDef"))
59}
60
61fn expect_case_def(ident: &ast::Identifier) -> Result<&ast::VariantCase, Error> {
62    let symbol = ident
63        .symbol
64        .as_ref()
65        .ok_or(Error::MissingAnalyzePhase(ident.value.clone()))?;
66
67    symbol
68        .as_variant_case()
69        .ok_or(Error::InvalidSymbol(ident.value.clone(), "VariantCase"))
70}
71
72#[allow(dead_code)]
73fn expect_field_def(ident: &ast::Identifier) -> Result<&ast::RecordField, Error> {
74    let symbol = ident
75        .symbol
76        .as_ref()
77        .ok_or(Error::MissingAnalyzePhase(ident.value.clone()))?;
78
79    symbol
80        .as_field_def()
81        .ok_or(Error::InvalidSymbol(ident.value.clone(), "FieldDef"))
82}
83
84fn coerce_identifier_into_asset_def(identifier: &ast::Identifier) -> Result<ast::AssetDef, Error> {
85    match identifier.try_symbol()? {
86        ast::Symbol::AssetDef(x) => Ok(x.as_ref().clone()),
87        _ => Err(Error::InvalidSymbol(identifier.value.clone(), "AssetDef")),
88    }
89}
90
91#[derive(Debug, Default)]
92pub(crate) struct Context {
93    is_asset_expr: bool,
94    is_datum_expr: bool,
95    is_address_expr: bool,
96}
97
98impl Context {
99    pub fn enter_asset_expr(&self) -> Self {
100        Self {
101            is_asset_expr: true,
102            is_datum_expr: false,
103            is_address_expr: false,
104        }
105    }
106
107    pub fn enter_datum_expr(&self) -> Self {
108        Self {
109            is_asset_expr: false,
110            is_datum_expr: true,
111            is_address_expr: false,
112        }
113    }
114
115    pub fn enter_address_expr(&self) -> Self {
116        Self {
117            is_asset_expr: false,
118            is_datum_expr: false,
119            is_address_expr: true,
120        }
121    }
122
123    pub fn is_address_expr(&self) -> bool {
124        self.is_address_expr
125    }
126
127    pub fn is_asset_expr(&self) -> bool {
128        self.is_asset_expr
129    }
130
131    pub fn is_datum_expr(&self) -> bool {
132        self.is_datum_expr
133    }
134}
135
136pub(crate) trait IntoLower {
137    type Output;
138
139    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error>;
140}
141
142impl<T> IntoLower for Option<&T>
143where
144    T: IntoLower,
145{
146    type Output = Option<T::Output>;
147
148    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
149        self.map(|x| x.into_lower(ctx)).transpose()
150    }
151}
152
153impl<T> IntoLower for Box<T>
154where
155    T: IntoLower,
156{
157    type Output = T::Output;
158
159    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
160        self.as_ref().into_lower(ctx)
161    }
162}
163
164impl IntoLower for ast::Identifier {
165    type Output = ir::Expression;
166
167    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
168        let symbol = self
169            .symbol
170            .as_ref()
171            .ok_or(Error::MissingAnalyzePhase(self.value.clone()))?;
172
173        match symbol {
174            ast::Symbol::ParamVar(n, ty) => {
175                Ok(ir::Param::ExpectValue(n.to_lowercase().clone(), ty.into_lower(ctx)?).into())
176            }
177            ast::Symbol::LocalExpr(expr) => Ok(expr.into_lower(ctx)?),
178            ast::Symbol::PartyDef(x) => Ok(ir::Param::ExpectValue(
179                x.name.value.to_lowercase().clone(),
180                Type::Address,
181            )
182            .into()),
183            ast::Symbol::Input(def) => {
184                let inner = def.into_lower(ctx)?.utxos;
185
186                let out = if ctx.is_asset_expr() {
187                    ir::Coerce::IntoAssets(inner).into()
188                } else if ctx.is_datum_expr() {
189                    ir::Coerce::IntoDatum(inner).into()
190                } else {
191                    inner
192                };
193
194                Ok(out)
195            }
196            ast::Symbol::Reference(def) => def.into_lower(ctx),
197            ast::Symbol::Fees => Ok(ir::Param::ExpectFees.into()),
198            ast::Symbol::EnvVar(n, ty) => {
199                Ok(ir::Param::ExpectValue(n.to_lowercase().clone(), ty.into_lower(ctx)?).into())
200            }
201            ast::Symbol::PolicyDef(x) => {
202                let policy = x.into_lower(ctx)?;
203
204                if ctx.is_address_expr() {
205                    Ok(ir::CompilerOp::BuildScriptAddress(policy.hash).into())
206                } else {
207                    Ok(policy.hash)
208                }
209            }
210            ast::Symbol::Output(index) => Ok(ir::Expression::Number(*index as i128)),
211            _ => {
212                dbg!(&self);
213                todo!();
214            }
215        }
216    }
217}
218
219impl IntoLower for ast::UtxoRef {
220    type Output = ir::Expression;
221
222    fn into_lower(&self, _: &Context) -> Result<Self::Output, Error> {
223        let x = ir::Expression::UtxoRefs(vec![UtxoRef {
224            txid: self.txid.clone(),
225            index: self.index as u32,
226        }]);
227
228        Ok(x)
229    }
230}
231
232impl IntoLower for ast::StructConstructor {
233    type Output = ir::StructExpr;
234
235    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
236        let type_def = expect_type_def(&self.r#type)
237            .or_else(|_| {
238                expect_alias_def(&self.r#type).and_then(|alias_def| {
239                    alias_def.resolve_alias_chain().ok_or_else(|| {
240                        Error::InvalidAst("Alias does not resolve to a TypeDef".to_string())
241                    })
242                })
243            })
244            .map_err(|_| Error::InvalidSymbol(self.r#type.value.clone(), "TypeDef or AliasDef"))?;
245
246        let constructor = type_def
247            .find_case_index(&self.case.name.value)
248            .ok_or(Error::InvalidAst("case not found".to_string()))?;
249
250        let case_def = expect_case_def(&self.case.name)?;
251
252        let mut fields = vec![];
253
254        for (index, field_def) in case_def.fields.iter().enumerate() {
255            let value = self.case.find_field_value(&field_def.name.value);
256
257            if let Some(value) = value {
258                fields.push(value.into_lower(ctx)?);
259            } else {
260                let spread_target = self
261                    .case
262                    .spread
263                    .as_ref()
264                    .expect("spread must be set for missing explicit field")
265                    .into_lower(ctx)?;
266
267                fields.push(ir::Expression::EvalBuiltIn(Box::new(
268                    ir::BuiltInOp::Property(spread_target, ir::Expression::Number(index as i128)),
269                )));
270            }
271        }
272
273        Ok(ir::StructExpr {
274            constructor,
275            fields,
276        })
277    }
278}
279
280impl IntoLower for ast::PolicyField {
281    type Output = ir::Expression;
282
283    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
284        match self {
285            ast::PolicyField::Hash(x) => x.into_lower(ctx),
286            ast::PolicyField::Script(x) => x.into_lower(ctx),
287            ast::PolicyField::Ref(x) => x.into_lower(ctx),
288        }
289    }
290}
291
292impl IntoLower for ast::PolicyDef {
293    type Output = ir::PolicyExpr;
294
295    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
296        match &self.value {
297            ast::PolicyValue::Assign(x) => {
298                let out = ir::PolicyExpr {
299                    name: self.name.value.clone(),
300                    hash: ir::Expression::Hash(hex_decode(&x.value)?),
301                    script: ir::ScriptSource::expect_parameter(self.name.value.clone()),
302                };
303
304                Ok(out)
305            }
306            ast::PolicyValue::Constructor(x) => {
307                let hash = x
308                    .find_field("hash")
309                    .ok_or(Error::InvalidAst("Missing policy hash".to_string()))?
310                    .into_lower(ctx)?;
311
312                let rf = x.find_field("ref").map(|x| x.into_lower(ctx)).transpose()?;
313
314                let script = x
315                    .find_field("script")
316                    .map(|x| x.into_lower(ctx))
317                    .transpose()?;
318
319                let script = match (rf, script) {
320                    (Some(rf), Some(script)) => ir::ScriptSource::new_ref(rf, script),
321                    (Some(rf), None) => {
322                        ir::ScriptSource::expect_ref_input(self.name.value.clone(), rf)
323                    }
324                    (None, Some(script)) => ir::ScriptSource::new_embedded(script),
325                    (None, None) => ir::ScriptSource::expect_parameter(self.name.value.clone()),
326                };
327
328                Ok(ir::PolicyExpr {
329                    name: self.name.value.clone(),
330                    hash,
331                    script,
332                })
333            }
334        }
335    }
336}
337
338impl IntoLower for ast::Type {
339    type Output = Type;
340
341    fn into_lower(&self, _: &Context) -> Result<Self::Output, Error> {
342        match self {
343            ast::Type::Undefined => Ok(Type::Undefined),
344            ast::Type::Unit => Ok(Type::Unit),
345            ast::Type::Int => Ok(Type::Int),
346            ast::Type::Bool => Ok(Type::Bool),
347            ast::Type::Bytes => Ok(Type::Bytes),
348            ast::Type::Address => Ok(Type::Address),
349            ast::Type::Utxo => Ok(Type::Utxo),
350            ast::Type::UtxoRef => Ok(Type::UtxoRef),
351            ast::Type::AnyAsset => Ok(Type::AnyAsset),
352            ast::Type::List(_) => Ok(Type::List),
353            ast::Type::Map(_, _) => Ok(Type::Map),
354            ast::Type::Custom(x) => Ok(Type::Custom(x.value.clone())),
355        }
356    }
357}
358
359impl IntoLower for ast::AddOp {
360    type Output = ir::Expression;
361
362    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
363        let left = self.lhs.into_lower(ctx)?;
364        let right = self.rhs.into_lower(ctx)?;
365
366        Ok(ir::Expression::EvalBuiltIn(Box::new(ir::BuiltInOp::Add(
367            left, right,
368        ))))
369    }
370}
371
372impl IntoLower for ast::SubOp {
373    type Output = ir::Expression;
374
375    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
376        let left = self.lhs.into_lower(ctx)?;
377        let right = self.rhs.into_lower(ctx)?;
378
379        Ok(ir::Expression::EvalBuiltIn(Box::new(ir::BuiltInOp::Sub(
380            left, right,
381        ))))
382    }
383}
384
385impl IntoLower for ast::ConcatOp {
386    type Output = ir::Expression;
387
388    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
389        let left = self.lhs.into_lower(ctx)?;
390        let right = self.rhs.into_lower(ctx)?;
391
392        Ok(ir::Expression::EvalBuiltIn(Box::new(
393            ir::BuiltInOp::Concat(left, right),
394        )))
395    }
396}
397
398impl IntoLower for ast::NegateOp {
399    type Output = ir::Expression;
400
401    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
402        let operand = self.operand.into_lower(ctx)?;
403
404        Ok(ir::Expression::EvalBuiltIn(Box::new(
405            ir::BuiltInOp::Negate(operand),
406        )))
407    }
408}
409
410impl IntoLower for ast::FnCall {
411    type Output = ir::Expression;
412
413    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
414        // A callee that resolves to a function definition is either a built-in
415        // (lowered to a dedicated compiler op) or a user-defined function
416        // (inlined below).
417        if let Some(fn_def) = self.callee.symbol.as_ref().and_then(|s| s.as_fn_def()) {
418            if let Some(builtin) = fn_def.builtin {
419                return crate::builtins::resolve(builtin).lower_call(&self.args, ctx);
420            }
421
422            // Inline a user-defined function: lower its analyzed body, then
423            // substitute each parameter with the lowered call argument.
424            let body = fn_def.body.as_ref().ok_or_else(|| {
425                Error::InvalidAst(format!(
426                    "function '{}' has neither a body nor a built-in kind",
427                    fn_def.name.value
428                ))
429            })?;
430
431            let lowered_body = body.result.into_lower(ctx)?;
432
433            let mut subs = std::collections::HashMap::new();
434            for (param, arg) in fn_def.parameters.parameters.iter().zip(&self.args) {
435                subs.insert(param.name.value.to_lowercase(), arg.into_lower(ctx)?);
436            }
437
438            use tx3_tir::Node;
439            let mut visitor = ParamSubstituter { subs: &subs };
440            return lowered_body
441                .apply(&mut visitor)
442                .map_err(|e| Error::InvalidAst(e.to_string()));
443        }
444
445        // Otherwise the callee must name an asset; treat the call as an asset
446        // constructor.
447        match coerce_identifier_into_asset_def(&self.callee) {
448            Ok(asset_def) => {
449                let policy = asset_def.policy.into_lower(ctx)?;
450                let asset_name = asset_def.asset_name.into_lower(ctx)?;
451                let amount = self.args[0].into_lower(ctx)?;
452
453                Ok(ir::Expression::Assets(vec![ir::AssetExpr {
454                    policy,
455                    asset_name,
456                    amount,
457                }]))
458            }
459            Err(_) => Err(Error::InvalidAst(format!(
460                "unknown function: {}",
461                self.callee.value
462            ))),
463        }
464    }
465}
466
467/// TIR visitor used by function inlining to replace each `EvalParam` standing
468/// for a function parameter with the lowered call argument.
469struct ParamSubstituter<'a> {
470    subs: &'a std::collections::HashMap<String, ir::Expression>,
471}
472
473impl tx3_tir::Visitor for ParamSubstituter<'_> {
474    fn reduce(
475        &mut self,
476        expr: ir::Expression,
477    ) -> Result<ir::Expression, tx3_tir::reduce::Error> {
478        if let ir::Expression::EvalParam(ref param) = expr {
479            if let ir::Param::ExpectValue(name, _) = param.as_ref() {
480                if let Some(replacement) = self.subs.get(name) {
481                    return Ok(replacement.clone());
482                }
483            }
484        }
485        Ok(expr)
486    }
487}
488
489impl IntoLower for ast::PropertyOp {
490    type Output = ir::Expression;
491
492    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
493        let object = self.operand.into_lower(ctx)?;
494
495        let ty = self
496            .operand
497            .target_type()
498            .ok_or(Error::MissingAnalyzePhase(format!("{0:?}", self.operand)))?;
499
500        let prop_index =
501            ty.property_index(*self.property.clone())
502                .ok_or(Error::InvalidProperty(
503                    format!("{:?}", self.property),
504                    ty.to_string(),
505                ))?;
506
507        Ok(ir::Expression::EvalBuiltIn(Box::new(
508            ir::BuiltInOp::Property(object, prop_index.into_lower(ctx)?),
509        )))
510    }
511}
512
513impl IntoLower for ast::ListConstructor {
514    type Output = Vec<ir::Expression>;
515
516    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
517        let elements = self
518            .elements
519            .iter()
520            .map(|x| x.into_lower(ctx))
521            .collect::<Result<Vec<_>, _>>()?;
522
523        Ok(elements)
524    }
525}
526
527impl IntoLower for ast::MapConstructor {
528    type Output = ir::Expression;
529
530    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
531        let pairs = self
532            .fields
533            .iter()
534            .map(|field| {
535                let key = field.key.into_lower(ctx)?;
536                let value = field.value.into_lower(ctx)?;
537                Ok((key, value))
538            })
539            .collect::<Result<Vec<_>, _>>()?;
540
541        Ok(ir::Expression::Map(pairs))
542    }
543}
544
545impl IntoLower for ast::DataExpr {
546    type Output = ir::Expression;
547
548    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
549        let out = match self {
550            ast::DataExpr::None => ir::Expression::None,
551            ast::DataExpr::Number(x) => Self::Output::Number(*x as i128),
552            ast::DataExpr::Bool(x) => ir::Expression::Bool(*x),
553            ast::DataExpr::String(x) => ir::Expression::String(x.value.clone()),
554            ast::DataExpr::HexString(x) => ir::Expression::Bytes(hex_decode(&x.value)?),
555            ast::DataExpr::StructConstructor(x) => ir::Expression::Struct(x.into_lower(ctx)?),
556            ast::DataExpr::ListConstructor(x) => ir::Expression::List(x.into_lower(ctx)?),
557            ast::DataExpr::MapConstructor(x) => x.into_lower(ctx)?,
558            ast::DataExpr::AnyAssetConstructor(x) => x.into_lower(ctx)?,
559            ast::DataExpr::Unit => ir::Expression::Struct(ir::StructExpr::unit()),
560            ast::DataExpr::Identifier(x) => x.into_lower(ctx)?,
561            ast::DataExpr::AddOp(x) => x.into_lower(ctx)?,
562            ast::DataExpr::SubOp(x) => x.into_lower(ctx)?,
563            ast::DataExpr::ConcatOp(x) => x.into_lower(ctx)?,
564            ast::DataExpr::NegateOp(x) => x.into_lower(ctx)?,
565            ast::DataExpr::PropertyOp(x) => x.into_lower(ctx)?,
566            ast::DataExpr::UtxoRef(x) => x.into_lower(ctx)?,
567            ast::DataExpr::FnCall(x) => x.into_lower(ctx)?,
568        };
569
570        Ok(out)
571    }
572}
573
574impl IntoLower for ast::AnyAssetConstructor {
575    type Output = ir::Expression;
576
577    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
578        let ctx = &ctx.enter_datum_expr();
579        let policy = self.policy.into_lower(ctx)?;
580
581        let ctx = &ctx.enter_datum_expr();
582        let asset_name = self.asset_name.into_lower(ctx)?;
583
584        let ctx = &ctx.enter_datum_expr();
585        let amount = self.amount.into_lower(ctx)?;
586
587        Ok(ir::Expression::Assets(vec![ir::AssetExpr {
588            policy,
589            asset_name,
590            amount,
591        }]))
592    }
593}
594
595impl IntoLower for ast::InputBlockField {
596    type Output = ir::Expression;
597
598    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
599        match self {
600            ast::InputBlockField::From(x) => {
601                let ctx = ctx.enter_address_expr();
602                x.into_lower(&ctx)
603            }
604            ast::InputBlockField::DatumIs(_) => todo!(),
605            ast::InputBlockField::MinAmount(x) => {
606                let ctx = ctx.enter_asset_expr();
607                x.into_lower(&ctx)
608            }
609            ast::InputBlockField::Redeemer(x) => {
610                let ctx = ctx.enter_datum_expr();
611                x.into_lower(&ctx)
612            }
613            ast::InputBlockField::Ref(x) => x.into_lower(ctx),
614        }
615    }
616}
617
618impl IntoLower for ast::InputBlock {
619    type Output = ir::Input;
620
621    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
622        let from_field = self.find("from");
623
624        let address = from_field.map(|x| x.into_lower(ctx)).transpose()?;
625
626        let min_amount = self
627            .find("min_amount")
628            .map(|x| x.into_lower(ctx))
629            .transpose()?;
630
631        let r#ref = self.find("ref").map(|x| x.into_lower(ctx)).transpose()?;
632
633        let redeemer = self
634            .find("redeemer")
635            .map(|x| x.into_lower(ctx))
636            .transpose()?
637            .unwrap_or(ir::Expression::None);
638
639        let query = ir::InputQuery {
640            address: address.unwrap_or(ir::Expression::None),
641            min_amount: min_amount.unwrap_or(ir::Expression::None),
642            r#ref: r#ref.unwrap_or(ir::Expression::None),
643            many: self.many,
644            collateral: false,
645        };
646
647        let param = ir::Param::ExpectInput(self.name.to_lowercase().clone(), query);
648
649        let input = ir::Input {
650            name: self.name.to_lowercase().clone(),
651            utxos: param.into(),
652            redeemer,
653        };
654
655        Ok(input)
656    }
657}
658
659impl IntoLower for ast::OutputBlockField {
660    type Output = ir::Expression;
661
662    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
663        match self {
664            ast::OutputBlockField::To(x) => {
665                let ctx = ctx.enter_address_expr();
666                x.into_lower(&ctx)
667            }
668            ast::OutputBlockField::Amount(x) => {
669                let ctx = ctx.enter_asset_expr();
670                x.into_lower(&ctx)
671            }
672            ast::OutputBlockField::Datum(x) => {
673                let ctx = ctx.enter_datum_expr();
674                x.into_lower(&ctx)
675            }
676        }
677    }
678}
679
680impl IntoLower for ast::OutputBlock {
681    type Output = ir::Output;
682
683    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
684        let address = self.find("to").into_lower(ctx)?.unwrap_or_default();
685        let datum = self.find("datum").into_lower(ctx)?.unwrap_or_default();
686        let amount = self.find("amount").into_lower(ctx)?.unwrap_or_default();
687
688        Ok(ir::Output {
689            address,
690            datum,
691            amount,
692            optional: self.optional,
693        })
694    }
695}
696
697impl IntoLower for ast::ValidityBlockField {
698    type Output = ir::Expression;
699
700    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
701        match self {
702            ast::ValidityBlockField::SinceSlot(x) => x.into_lower(ctx),
703            ast::ValidityBlockField::UntilSlot(x) => x.into_lower(ctx),
704        }
705    }
706}
707
708impl IntoLower for ast::ValidityBlock {
709    type Output = ir::Validity;
710
711    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
712        let since = self.find("since_slot").into_lower(ctx)?.unwrap_or_default();
713        let until = self.find("until_slot").into_lower(ctx)?.unwrap_or_default();
714
715        Ok(ir::Validity { since, until })
716    }
717}
718
719impl IntoLower for ast::MintBlockField {
720    type Output = ir::Expression;
721
722    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
723        match self {
724            ast::MintBlockField::Amount(x) => x.into_lower(ctx),
725            ast::MintBlockField::Redeemer(x) => x.into_lower(ctx),
726        }
727    }
728}
729
730impl IntoLower for ast::MintBlock {
731    type Output = ir::Mint;
732
733    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
734        let amount = self.find("amount").into_lower(ctx)?.unwrap_or_default();
735        let redeemer = self.find("redeemer").into_lower(ctx)?.unwrap_or_default();
736
737        Ok(ir::Mint { amount, redeemer })
738    }
739}
740
741impl IntoLower for ast::MetadataBlockField {
742    type Output = ir::Metadata;
743    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
744        Ok(ir::Metadata {
745            key: self.key.into_lower(ctx)?,
746            value: self.value.into_lower(ctx)?,
747        })
748    }
749}
750
751impl IntoLower for ast::MetadataBlock {
752    type Output = Vec<ir::Metadata>;
753
754    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
755        let fields = self
756            .fields
757            .iter()
758            .map(|metadata_field| metadata_field.into_lower(ctx))
759            .collect::<Result<Vec<_>, _>>()?;
760
761        Ok(fields)
762    }
763}
764
765impl IntoLower for ast::ChainSpecificBlock {
766    type Output = ir::AdHocDirective;
767
768    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
769        match self {
770            ast::ChainSpecificBlock::Cardano(x) => x.into_lower(ctx),
771        }
772    }
773}
774
775impl IntoLower for ast::ReferenceBlock {
776    type Output = ir::Expression;
777
778    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
779        let r#ref = self.r#ref.into_lower(ctx)?;
780
781        let query = ir::InputQuery {
782            address: ir::Expression::None,
783            min_amount: ir::Expression::None,
784            r#ref,
785            many: false,
786            collateral: false,
787        };
788
789        let inner = ir::Param::ExpectInput(self.name.to_lowercase(), query).into();
790
791        let out = if ctx.is_asset_expr() {
792            ir::Coerce::IntoAssets(inner).into()
793        } else if ctx.is_datum_expr() {
794            ir::Coerce::IntoDatum(inner).into()
795        } else {
796            inner
797        };
798
799        Ok(out)
800    }
801}
802
803impl IntoLower for ast::CollateralBlockField {
804    type Output = ir::Expression;
805
806    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
807        match self {
808            ast::CollateralBlockField::From(x) => x.into_lower(ctx),
809            ast::CollateralBlockField::MinAmount(x) => x.into_lower(ctx),
810            ast::CollateralBlockField::Ref(x) => x.into_lower(ctx),
811        }
812    }
813}
814
815impl IntoLower for ast::CollateralBlock {
816    type Output = ir::Collateral;
817
818    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
819        let from = self.find("from").map(|x| x.into_lower(ctx)).transpose()?;
820
821        let min_amount = self
822            .find("min_amount")
823            .map(|x| x.into_lower(ctx))
824            .transpose()?;
825
826        let r#ref = self.find("ref").map(|x| x.into_lower(ctx)).transpose()?;
827
828        let query = ir::InputQuery {
829            address: from.unwrap_or(ir::Expression::None),
830            min_amount: min_amount.unwrap_or(ir::Expression::None),
831            r#ref: r#ref.unwrap_or(ir::Expression::None),
832            many: false,
833            collateral: true,
834        };
835
836        let param = ir::Param::ExpectInput("collateral".to_string(), query);
837
838        let collateral = ir::Collateral {
839            utxos: param.into(),
840        };
841
842        Ok(collateral)
843    }
844}
845
846impl IntoLower for ast::SignersBlock {
847    type Output = ir::Signers;
848
849    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
850        Ok(ir::Signers {
851            signers: self
852                .signers
853                .iter()
854                .map(|x| x.into_lower(ctx))
855                .collect::<Result<Vec<_>, _>>()?,
856        })
857    }
858}
859
860impl IntoLower for ast::TxDef {
861    type Output = ir::Tx;
862
863    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
864        let ir = ir::Tx {
865            references: self
866                .references
867                .iter()
868                .map(|x| x.r#ref.into_lower(ctx))
869                .collect::<Result<Vec<_>, _>>()?,
870            inputs: self
871                .inputs
872                .iter()
873                .map(|x| x.into_lower(ctx))
874                .collect::<Result<Vec<_>, _>>()?,
875            outputs: self
876                .outputs
877                .iter()
878                .map(|x| x.into_lower(ctx))
879                .collect::<Result<Vec<_>, _>>()?,
880            validity: self
881                .validity
882                .as_ref()
883                .map(|x| x.into_lower(ctx))
884                .transpose()?,
885            mints: self
886                .mints
887                .iter()
888                .map(|x| x.into_lower(ctx))
889                .collect::<Result<Vec<_>, _>>()?,
890            burns: self
891                .burns
892                .iter()
893                .map(|x| x.into_lower(ctx))
894                .collect::<Result<Vec<_>, _>>()?,
895            adhoc: self
896                .adhoc
897                .iter()
898                .map(|x| x.into_lower(ctx))
899                .collect::<Result<Vec<_>, _>>()?,
900            fees: ir::Param::ExpectFees.into(),
901            collateral: self
902                .collateral
903                .iter()
904                .map(|x| x.into_lower(ctx))
905                .collect::<Result<Vec<_>, _>>()?,
906            signers: self
907                .signers
908                .as_ref()
909                .map(|x| x.into_lower(ctx))
910                .transpose()?,
911            metadata: self
912                .metadata
913                .as_ref()
914                .map(|x| x.into_lower(ctx))
915                .transpose()?
916                .unwrap_or(vec![]),
917        };
918
919        Ok(ir)
920    }
921}
922
923pub fn lower_tx(ast: &ast::TxDef) -> Result<ir::Tx, Error> {
924    let ctx = &Context::default();
925
926    let tx = ast.into_lower(ctx)?;
927
928    Ok(tx)
929}
930
931/// Lowers the Tx3 language to the intermediate representation.
932///
933/// This function takes an AST and converts it into the intermediate
934/// representation (IR) of the Tx3 language.
935///
936/// # Arguments
937///
938/// * `ast` - The AST to lower
939///
940/// # Returns
941///
942/// * `Result<ir::Program, Error>` - The lowered intermediate representation
943pub fn lower(ast: &ast::Program, template: &str) -> Result<ir::Tx, Error> {
944    let tx = ast
945        .txs
946        .iter()
947        .find(|x| x.name.value == template)
948        .ok_or(Error::InvalidAst("tx not found".to_string()))?;
949
950    lower_tx(tx)
951}
952
953#[cfg(test)]
954mod tests {
955    use assert_json_diff::assert_json_eq;
956    use paste::paste;
957
958    use super::*;
959    use crate::parsing::{self};
960
961    fn make_snapshot_if_missing(example: &str, name: &str, tx: &ir::Tx) {
962        let manifest_dir = env!("CARGO_MANIFEST_DIR");
963
964        let path = format!("{}/../../examples/{}.{}.tir", manifest_dir, example, name);
965
966        if !std::fs::exists(&path).unwrap() {
967            let ir = serde_json::to_string_pretty(tx).unwrap();
968            std::fs::write(&path, ir).unwrap();
969        }
970    }
971
972    fn test_lowering_example(example: &str) {
973        let manifest_dir = env!("CARGO_MANIFEST_DIR");
974        let mut program = parsing::parse_well_known_example(example);
975
976        crate::analyzing::analyze(&mut program).ok().unwrap();
977
978        for tx in program.txs.iter() {
979            let tir = lower(&program, &tx.name.value).unwrap();
980
981            make_snapshot_if_missing(example, &tx.name.value, &tir);
982
983            let tir_file = format!(
984                "{}/../../examples/{}.{}.tir",
985                manifest_dir, example, tx.name.value
986            );
987
988            let expected = std::fs::read_to_string(tir_file).unwrap();
989            let expected: ir::Tx = serde_json::from_str(&expected).unwrap();
990
991            assert_json_eq!(tir, expected);
992        }
993    }
994
995    #[macro_export]
996    macro_rules! test_lowering {
997        ($name:ident) => {
998            paste! {
999                #[test]
1000                fn [<test_example_ $name>]() {
1001                    test_lowering_example(stringify!($name));
1002                }
1003            }
1004        };
1005    }
1006
1007    test_lowering!(lang_tour);
1008
1009    test_lowering!(transfer);
1010
1011    test_lowering!(swap);
1012
1013    test_lowering!(asteria);
1014
1015    test_lowering!(vesting);
1016
1017    test_lowering!(faucet);
1018
1019    test_lowering!(input_datum);
1020
1021    test_lowering!(env_vars);
1022
1023    test_lowering!(local_vars);
1024
1025    test_lowering!(cardano_witness);
1026
1027    test_lowering!(reference_script);
1028
1029    test_lowering!(withdrawal);
1030
1031    test_lowering!(map);
1032
1033    test_lowering!(burn);
1034
1035    test_lowering!(min_utxo);
1036
1037    test_lowering!(tip_slot);
1038
1039    test_lowering!(posix_time);
1040
1041    test_lowering!(donation);
1042
1043    test_lowering!(list_concat);
1044
1045    test_lowering!(buidler_fest_2026);
1046
1047    test_lowering!(functions);
1048
1049    test_lowering!(nested_functions);
1050
1051    test_lowering!(param_field_shadow);
1052
1053    test_lowering!(oracle_reference_datum);
1054}