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        let function_name = &self.callee.value;
415
416        match function_name.as_str() {
417            "min_utxo" => {
418                if self.args.len() != 1 {
419                    return Err(Error::InvalidAst(format!(
420                        "min_utxo expects 1 argument, got {}",
421                        self.args.len()
422                    )));
423                }
424                let arg = self.args[0].into_lower(ctx)?;
425                Ok(ir::Expression::EvalCompiler(Box::new(
426                    ir::CompilerOp::ComputeMinUtxo(arg),
427                )))
428            }
429            "tip_slot" => {
430                if !self.args.is_empty() {
431                    return Err(Error::InvalidAst(format!(
432                        "tip_slot expects 0 arguments, got {}",
433                        self.args.len()
434                    )));
435                }
436                Ok(ir::Expression::EvalCompiler(Box::new(
437                    ir::CompilerOp::ComputeTipSlot,
438                )))
439            }
440            "slot_to_time" => {
441                if self.args.len() != 1 {
442                    return Err(Error::InvalidAst(format!(
443                        "slot_to_time expects 1 argument, got {}",
444                        self.args.len()
445                    )));
446                }
447                let arg = self.args[0].into_lower(ctx)?;
448                Ok(ir::Expression::EvalCompiler(Box::new(
449                    ir::CompilerOp::ComputeSlotToTime(arg),
450                )))
451            }
452            "time_to_slot" => {
453                if self.args.len() != 1 {
454                    return Err(Error::InvalidAst(format!(
455                        "time_to_slot expects 1 argument, got {}",
456                        self.args.len()
457                    )));
458                }
459
460                let arg = self.args[0].into_lower(ctx)?;
461
462                Ok(ir::Expression::EvalCompiler(Box::new(
463                    ir::CompilerOp::ComputeTimeToSlot(arg),
464                )))
465            }
466            _ => {
467                // Try to coerce as asset - if it fails, we'll get an error
468
469                match coerce_identifier_into_asset_def(&self.callee) {
470                    Ok(asset_def) => {
471                        let policy = asset_def.policy.into_lower(ctx)?;
472                        let asset_name = asset_def.asset_name.into_lower(ctx)?;
473                        let amount = self.args[0].into_lower(ctx)?;
474
475                        Ok(ir::Expression::Assets(vec![ir::AssetExpr {
476                            policy,
477                            asset_name,
478                            amount,
479                        }]))
480                    }
481                    Err(_) => Err(Error::InvalidAst(format!(
482                        "unknown function: {}",
483                        function_name
484                    ))),
485                }
486            }
487        }
488    }
489}
490
491impl IntoLower for ast::PropertyOp {
492    type Output = ir::Expression;
493
494    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
495        let object = self.operand.into_lower(ctx)?;
496
497        let ty = self
498            .operand
499            .target_type()
500            .ok_or(Error::MissingAnalyzePhase(format!("{0:?}", self.operand)))?;
501
502        let prop_index =
503            ty.property_index(*self.property.clone())
504                .ok_or(Error::InvalidProperty(
505                    format!("{:?}", self.property),
506                    ty.to_string(),
507                ))?;
508
509        Ok(ir::Expression::EvalBuiltIn(Box::new(
510            ir::BuiltInOp::Property(object, prop_index.into_lower(ctx)?),
511        )))
512    }
513}
514
515impl IntoLower for ast::ListConstructor {
516    type Output = Vec<ir::Expression>;
517
518    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
519        let elements = self
520            .elements
521            .iter()
522            .map(|x| x.into_lower(ctx))
523            .collect::<Result<Vec<_>, _>>()?;
524
525        Ok(elements)
526    }
527}
528
529impl IntoLower for ast::MapConstructor {
530    type Output = ir::Expression;
531
532    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
533        let pairs = self
534            .fields
535            .iter()
536            .map(|field| {
537                let key = field.key.into_lower(ctx)?;
538                let value = field.value.into_lower(ctx)?;
539                Ok((key, value))
540            })
541            .collect::<Result<Vec<_>, _>>()?;
542
543        Ok(ir::Expression::Map(pairs))
544    }
545}
546
547impl IntoLower for ast::DataExpr {
548    type Output = ir::Expression;
549
550    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
551        let out = match self {
552            ast::DataExpr::None => ir::Expression::None,
553            ast::DataExpr::Number(x) => Self::Output::Number(*x as i128),
554            ast::DataExpr::Bool(x) => ir::Expression::Bool(*x),
555            ast::DataExpr::String(x) => ir::Expression::String(x.value.clone()),
556            ast::DataExpr::HexString(x) => ir::Expression::Bytes(hex_decode(&x.value)?),
557            ast::DataExpr::StructConstructor(x) => ir::Expression::Struct(x.into_lower(ctx)?),
558            ast::DataExpr::ListConstructor(x) => ir::Expression::List(x.into_lower(ctx)?),
559            ast::DataExpr::MapConstructor(x) => x.into_lower(ctx)?,
560            ast::DataExpr::AnyAssetConstructor(x) => x.into_lower(ctx)?,
561            ast::DataExpr::Unit => ir::Expression::Struct(ir::StructExpr::unit()),
562            ast::DataExpr::Identifier(x) => x.into_lower(ctx)?,
563            ast::DataExpr::AddOp(x) => x.into_lower(ctx)?,
564            ast::DataExpr::SubOp(x) => x.into_lower(ctx)?,
565            ast::DataExpr::ConcatOp(x) => x.into_lower(ctx)?,
566            ast::DataExpr::NegateOp(x) => x.into_lower(ctx)?,
567            ast::DataExpr::PropertyOp(x) => x.into_lower(ctx)?,
568            ast::DataExpr::UtxoRef(x) => x.into_lower(ctx)?,
569            ast::DataExpr::FnCall(x) => x.into_lower(ctx)?,
570            ast::DataExpr::MinUtxo(x) => ir::Expression::EvalCompiler(Box::new(
571                ir::CompilerOp::ComputeMinUtxo(x.into_lower(ctx)?),
572            )),
573            ast::DataExpr::ComputeTipSlot => {
574                ir::Expression::EvalCompiler(Box::new(ir::CompilerOp::ComputeTipSlot))
575            }
576            ast::DataExpr::SlotToTime(x) => ir::Expression::EvalCompiler(Box::new(
577                ir::CompilerOp::ComputeSlotToTime(x.into_lower(ctx)?),
578            )),
579            ast::DataExpr::TimeToSlot(x) => ir::Expression::EvalCompiler(Box::new(
580                ir::CompilerOp::ComputeTimeToSlot(x.into_lower(ctx)?),
581            )),
582        };
583
584        Ok(out)
585    }
586}
587
588impl IntoLower for ast::AnyAssetConstructor {
589    type Output = ir::Expression;
590
591    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
592        let ctx = &ctx.enter_datum_expr();
593        let policy = self.policy.into_lower(ctx)?;
594
595        let ctx = &ctx.enter_datum_expr();
596        let asset_name = self.asset_name.into_lower(ctx)?;
597
598        let ctx = &ctx.enter_datum_expr();
599        let amount = self.amount.into_lower(ctx)?;
600
601        Ok(ir::Expression::Assets(vec![ir::AssetExpr {
602            policy,
603            asset_name,
604            amount,
605        }]))
606    }
607}
608
609impl IntoLower for ast::InputBlockField {
610    type Output = ir::Expression;
611
612    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
613        match self {
614            ast::InputBlockField::From(x) => {
615                let ctx = ctx.enter_address_expr();
616                x.into_lower(&ctx)
617            }
618            ast::InputBlockField::DatumIs(_) => todo!(),
619            ast::InputBlockField::MinAmount(x) => {
620                let ctx = ctx.enter_asset_expr();
621                x.into_lower(&ctx)
622            }
623            ast::InputBlockField::Redeemer(x) => {
624                let ctx = ctx.enter_datum_expr();
625                x.into_lower(&ctx)
626            }
627            ast::InputBlockField::Ref(x) => x.into_lower(ctx),
628        }
629    }
630}
631
632impl IntoLower for ast::InputBlock {
633    type Output = ir::Input;
634
635    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
636        let from_field = self.find("from");
637
638        let address = from_field.map(|x| x.into_lower(ctx)).transpose()?;
639
640        let min_amount = self
641            .find("min_amount")
642            .map(|x| x.into_lower(ctx))
643            .transpose()?;
644
645        let r#ref = self.find("ref").map(|x| x.into_lower(ctx)).transpose()?;
646
647        let redeemer = self
648            .find("redeemer")
649            .map(|x| x.into_lower(ctx))
650            .transpose()?
651            .unwrap_or(ir::Expression::None);
652
653        let query = ir::InputQuery {
654            address: address.unwrap_or(ir::Expression::None),
655            min_amount: min_amount.unwrap_or(ir::Expression::None),
656            r#ref: r#ref.unwrap_or(ir::Expression::None),
657            many: self.many,
658            collateral: false,
659        };
660
661        let param = ir::Param::ExpectInput(self.name.to_lowercase().clone(), query);
662
663        let input = ir::Input {
664            name: self.name.to_lowercase().clone(),
665            utxos: param.into(),
666            redeemer,
667        };
668
669        Ok(input)
670    }
671}
672
673impl IntoLower for ast::OutputBlockField {
674    type Output = ir::Expression;
675
676    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
677        match self {
678            ast::OutputBlockField::To(x) => {
679                let ctx = ctx.enter_address_expr();
680                x.into_lower(&ctx)
681            }
682            ast::OutputBlockField::Amount(x) => {
683                let ctx = ctx.enter_asset_expr();
684                x.into_lower(&ctx)
685            }
686            ast::OutputBlockField::Datum(x) => {
687                let ctx = ctx.enter_datum_expr();
688                x.into_lower(&ctx)
689            }
690        }
691    }
692}
693
694impl IntoLower for ast::OutputBlock {
695    type Output = ir::Output;
696
697    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
698        let address = self.find("to").into_lower(ctx)?.unwrap_or_default();
699        let datum = self.find("datum").into_lower(ctx)?.unwrap_or_default();
700        let amount = self.find("amount").into_lower(ctx)?.unwrap_or_default();
701
702        Ok(ir::Output {
703            address,
704            datum,
705            amount,
706            optional: self.optional,
707        })
708    }
709}
710
711impl IntoLower for ast::ValidityBlockField {
712    type Output = ir::Expression;
713
714    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
715        match self {
716            ast::ValidityBlockField::SinceSlot(x) => x.into_lower(ctx),
717            ast::ValidityBlockField::UntilSlot(x) => x.into_lower(ctx),
718        }
719    }
720}
721
722impl IntoLower for ast::ValidityBlock {
723    type Output = ir::Validity;
724
725    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
726        let since = self.find("since_slot").into_lower(ctx)?.unwrap_or_default();
727        let until = self.find("until_slot").into_lower(ctx)?.unwrap_or_default();
728
729        Ok(ir::Validity { since, until })
730    }
731}
732
733impl IntoLower for ast::MintBlockField {
734    type Output = ir::Expression;
735
736    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
737        match self {
738            ast::MintBlockField::Amount(x) => x.into_lower(ctx),
739            ast::MintBlockField::Redeemer(x) => x.into_lower(ctx),
740        }
741    }
742}
743
744impl IntoLower for ast::MintBlock {
745    type Output = ir::Mint;
746
747    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
748        let amount = self.find("amount").into_lower(ctx)?.unwrap_or_default();
749        let redeemer = self.find("redeemer").into_lower(ctx)?.unwrap_or_default();
750
751        Ok(ir::Mint { amount, redeemer })
752    }
753}
754
755impl IntoLower for ast::MetadataBlockField {
756    type Output = ir::Metadata;
757    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
758        Ok(ir::Metadata {
759            key: self.key.into_lower(ctx)?,
760            value: self.value.into_lower(ctx)?,
761        })
762    }
763}
764
765impl IntoLower for ast::MetadataBlock {
766    type Output = Vec<ir::Metadata>;
767
768    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
769        let fields = self
770            .fields
771            .iter()
772            .map(|metadata_field| metadata_field.into_lower(ctx))
773            .collect::<Result<Vec<_>, _>>()?;
774
775        Ok(fields)
776    }
777}
778
779impl IntoLower for ast::ChainSpecificBlock {
780    type Output = ir::AdHocDirective;
781
782    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
783        match self {
784            ast::ChainSpecificBlock::Cardano(x) => x.into_lower(ctx),
785        }
786    }
787}
788
789impl IntoLower for ast::ReferenceBlock {
790    type Output = ir::Expression;
791
792    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
793        let r#ref = self.r#ref.into_lower(ctx)?;
794
795        let query = ir::InputQuery {
796            address: ir::Expression::None,
797            min_amount: ir::Expression::None,
798            r#ref,
799            many: false,
800            collateral: false,
801        };
802
803        let inner = ir::Param::ExpectInput(self.name.to_lowercase(), query).into();
804
805        let out = if ctx.is_asset_expr() {
806            ir::Coerce::IntoAssets(inner).into()
807        } else if ctx.is_datum_expr() {
808            ir::Coerce::IntoDatum(inner).into()
809        } else {
810            inner
811        };
812
813        Ok(out)
814    }
815}
816
817impl IntoLower for ast::CollateralBlockField {
818    type Output = ir::Expression;
819
820    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
821        match self {
822            ast::CollateralBlockField::From(x) => x.into_lower(ctx),
823            ast::CollateralBlockField::MinAmount(x) => x.into_lower(ctx),
824            ast::CollateralBlockField::Ref(x) => x.into_lower(ctx),
825        }
826    }
827}
828
829impl IntoLower for ast::CollateralBlock {
830    type Output = ir::Collateral;
831
832    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
833        let from = self.find("from").map(|x| x.into_lower(ctx)).transpose()?;
834
835        let min_amount = self
836            .find("min_amount")
837            .map(|x| x.into_lower(ctx))
838            .transpose()?;
839
840        let r#ref = self.find("ref").map(|x| x.into_lower(ctx)).transpose()?;
841
842        let query = ir::InputQuery {
843            address: from.unwrap_or(ir::Expression::None),
844            min_amount: min_amount.unwrap_or(ir::Expression::None),
845            r#ref: r#ref.unwrap_or(ir::Expression::None),
846            many: false,
847            collateral: true,
848        };
849
850        let param = ir::Param::ExpectInput("collateral".to_string(), query);
851
852        let collateral = ir::Collateral {
853            utxos: param.into(),
854        };
855
856        Ok(collateral)
857    }
858}
859
860impl IntoLower for ast::SignersBlock {
861    type Output = ir::Signers;
862
863    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
864        Ok(ir::Signers {
865            signers: self
866                .signers
867                .iter()
868                .map(|x| x.into_lower(ctx))
869                .collect::<Result<Vec<_>, _>>()?,
870        })
871    }
872}
873
874impl IntoLower for ast::TxDef {
875    type Output = ir::Tx;
876
877    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
878        let ir = ir::Tx {
879            references: self
880                .references
881                .iter()
882                .map(|x| x.r#ref.into_lower(ctx))
883                .collect::<Result<Vec<_>, _>>()?,
884            inputs: self
885                .inputs
886                .iter()
887                .map(|x| x.into_lower(ctx))
888                .collect::<Result<Vec<_>, _>>()?,
889            outputs: self
890                .outputs
891                .iter()
892                .map(|x| x.into_lower(ctx))
893                .collect::<Result<Vec<_>, _>>()?,
894            validity: self
895                .validity
896                .as_ref()
897                .map(|x| x.into_lower(ctx))
898                .transpose()?,
899            mints: self
900                .mints
901                .iter()
902                .map(|x| x.into_lower(ctx))
903                .collect::<Result<Vec<_>, _>>()?,
904            burns: self
905                .burns
906                .iter()
907                .map(|x| x.into_lower(ctx))
908                .collect::<Result<Vec<_>, _>>()?,
909            adhoc: self
910                .adhoc
911                .iter()
912                .map(|x| x.into_lower(ctx))
913                .collect::<Result<Vec<_>, _>>()?,
914            fees: ir::Param::ExpectFees.into(),
915            collateral: self
916                .collateral
917                .iter()
918                .map(|x| x.into_lower(ctx))
919                .collect::<Result<Vec<_>, _>>()?,
920            signers: self
921                .signers
922                .as_ref()
923                .map(|x| x.into_lower(ctx))
924                .transpose()?,
925            metadata: self
926                .metadata
927                .as_ref()
928                .map(|x| x.into_lower(ctx))
929                .transpose()?
930                .unwrap_or(vec![]),
931        };
932
933        Ok(ir)
934    }
935}
936
937pub fn lower_tx(ast: &ast::TxDef) -> Result<ir::Tx, Error> {
938    let ctx = &Context::default();
939
940    let tx = ast.into_lower(ctx)?;
941
942    Ok(tx)
943}
944
945/// Lowers the Tx3 language to the intermediate representation.
946///
947/// This function takes an AST and converts it into the intermediate
948/// representation (IR) of the Tx3 language.
949///
950/// # Arguments
951///
952/// * `ast` - The AST to lower
953///
954/// # Returns
955///
956/// * `Result<ir::Program, Error>` - The lowered intermediate representation
957pub fn lower(ast: &ast::Program, template: &str) -> Result<ir::Tx, Error> {
958    let tx = ast
959        .txs
960        .iter()
961        .find(|x| x.name.value == template)
962        .ok_or(Error::InvalidAst("tx not found".to_string()))?;
963
964    lower_tx(tx)
965}
966
967#[cfg(test)]
968mod tests {
969    use assert_json_diff::assert_json_eq;
970    use paste::paste;
971
972    use super::*;
973    use crate::parsing::{self};
974
975    fn make_snapshot_if_missing(example: &str, name: &str, tx: &ir::Tx) {
976        let manifest_dir = env!("CARGO_MANIFEST_DIR");
977
978        let path = format!("{}/../../examples/{}.{}.tir", manifest_dir, example, name);
979
980        if !std::fs::exists(&path).unwrap() {
981            let ir = serde_json::to_string_pretty(tx).unwrap();
982            std::fs::write(&path, ir).unwrap();
983        }
984    }
985
986    fn test_lowering_example(example: &str) {
987        let manifest_dir = env!("CARGO_MANIFEST_DIR");
988        let mut program = parsing::parse_well_known_example(example);
989
990        crate::analyzing::analyze(&mut program).ok().unwrap();
991
992        for tx in program.txs.iter() {
993            let tir = lower(&program, &tx.name.value).unwrap();
994
995            make_snapshot_if_missing(example, &tx.name.value, &tir);
996
997            let tir_file = format!(
998                "{}/../../examples/{}.{}.tir",
999                manifest_dir, example, tx.name.value
1000            );
1001
1002            let expected = std::fs::read_to_string(tir_file).unwrap();
1003            let expected: ir::Tx = serde_json::from_str(&expected).unwrap();
1004
1005            assert_json_eq!(tir, expected);
1006        }
1007    }
1008
1009    #[macro_export]
1010    macro_rules! test_lowering {
1011        ($name:ident) => {
1012            paste! {
1013                #[test]
1014                fn [<test_example_ $name>]() {
1015                    test_lowering_example(stringify!($name));
1016                }
1017            }
1018        };
1019    }
1020
1021    test_lowering!(lang_tour);
1022
1023    test_lowering!(transfer);
1024
1025    test_lowering!(swap);
1026
1027    test_lowering!(asteria);
1028
1029    test_lowering!(vesting);
1030
1031    test_lowering!(faucet);
1032
1033    test_lowering!(input_datum);
1034
1035    test_lowering!(env_vars);
1036
1037    test_lowering!(local_vars);
1038
1039    test_lowering!(cardano_witness);
1040
1041    test_lowering!(reference_script);
1042
1043    test_lowering!(withdrawal);
1044
1045    test_lowering!(map);
1046
1047    test_lowering!(burn);
1048
1049    test_lowering!(min_utxo);
1050
1051    test_lowering!(donation);
1052
1053    test_lowering!(list_concat);
1054
1055    test_lowering!(buidler_fest_2026);
1056
1057    test_lowering!(param_field_shadow);
1058
1059    test_lowering!(oracle_reference_datum);
1060}