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::Fees => Ok(ir::Param::ExpectFees.into()),
197            ast::Symbol::EnvVar(n, ty) => {
198                Ok(ir::Param::ExpectValue(n.to_lowercase().clone(), ty.into_lower(ctx)?).into())
199            }
200            ast::Symbol::PolicyDef(x) => {
201                let policy = x.into_lower(ctx)?;
202
203                if ctx.is_address_expr() {
204                    Ok(ir::CompilerOp::BuildScriptAddress(policy.hash).into())
205                } else {
206                    Ok(policy.hash)
207                }
208            }
209            ast::Symbol::Output(index) => Ok(ir::Expression::Number(*index as i128)),
210            _ => {
211                dbg!(&self);
212                todo!();
213            }
214        }
215    }
216}
217
218impl IntoLower for ast::UtxoRef {
219    type Output = ir::Expression;
220
221    fn into_lower(&self, _: &Context) -> Result<Self::Output, Error> {
222        let x = ir::Expression::UtxoRefs(vec![UtxoRef {
223            txid: self.txid.clone(),
224            index: self.index as u32,
225        }]);
226
227        Ok(x)
228    }
229}
230
231impl IntoLower for ast::StructConstructor {
232    type Output = ir::StructExpr;
233
234    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
235        let type_def = expect_type_def(&self.r#type)
236            .or_else(|_| {
237                expect_alias_def(&self.r#type).and_then(|alias_def| {
238                    alias_def.resolve_alias_chain().ok_or_else(|| {
239                        Error::InvalidAst("Alias does not resolve to a TypeDef".to_string())
240                    })
241                })
242            })
243            .map_err(|_| Error::InvalidSymbol(self.r#type.value.clone(), "TypeDef or AliasDef"))?;
244
245        let constructor = type_def
246            .find_case_index(&self.case.name.value)
247            .ok_or(Error::InvalidAst("case not found".to_string()))?;
248
249        let case_def = expect_case_def(&self.case.name)?;
250
251        let mut fields = vec![];
252
253        for (index, field_def) in case_def.fields.iter().enumerate() {
254            let value = self.case.find_field_value(&field_def.name.value);
255
256            if let Some(value) = value {
257                fields.push(value.into_lower(ctx)?);
258            } else {
259                let spread_target = self
260                    .case
261                    .spread
262                    .as_ref()
263                    .expect("spread must be set for missing explicit field")
264                    .into_lower(ctx)?;
265
266                fields.push(ir::Expression::EvalBuiltIn(Box::new(
267                    ir::BuiltInOp::Property(spread_target, ir::Expression::Number(index as i128)),
268                )));
269            }
270        }
271
272        Ok(ir::StructExpr {
273            constructor,
274            fields,
275        })
276    }
277}
278
279impl IntoLower for ast::PolicyField {
280    type Output = ir::Expression;
281
282    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
283        match self {
284            ast::PolicyField::Hash(x) => x.into_lower(ctx),
285            ast::PolicyField::Script(x) => x.into_lower(ctx),
286            ast::PolicyField::Ref(x) => x.into_lower(ctx),
287        }
288    }
289}
290
291impl IntoLower for ast::PolicyDef {
292    type Output = ir::PolicyExpr;
293
294    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
295        match &self.value {
296            ast::PolicyValue::Assign(x) => {
297                let out = ir::PolicyExpr {
298                    name: self.name.value.clone(),
299                    hash: ir::Expression::Hash(hex_decode(&x.value)?),
300                    script: ir::ScriptSource::expect_parameter(self.name.value.clone()),
301                };
302
303                Ok(out)
304            }
305            ast::PolicyValue::Constructor(x) => {
306                let hash = x
307                    .find_field("hash")
308                    .ok_or(Error::InvalidAst("Missing policy hash".to_string()))?
309                    .into_lower(ctx)?;
310
311                let rf = x.find_field("ref").map(|x| x.into_lower(ctx)).transpose()?;
312
313                let script = x
314                    .find_field("script")
315                    .map(|x| x.into_lower(ctx))
316                    .transpose()?;
317
318                let script = match (rf, script) {
319                    (Some(rf), Some(script)) => ir::ScriptSource::new_ref(rf, script),
320                    (Some(rf), None) => {
321                        ir::ScriptSource::expect_ref_input(self.name.value.clone(), rf)
322                    }
323                    (None, Some(script)) => ir::ScriptSource::new_embedded(script),
324                    (None, None) => ir::ScriptSource::expect_parameter(self.name.value.clone()),
325                };
326
327                Ok(ir::PolicyExpr {
328                    name: self.name.value.clone(),
329                    hash,
330                    script,
331                })
332            }
333        }
334    }
335}
336
337impl IntoLower for ast::Type {
338    type Output = Type;
339
340    fn into_lower(&self, _: &Context) -> Result<Self::Output, Error> {
341        match self {
342            ast::Type::Undefined => Ok(Type::Undefined),
343            ast::Type::Unit => Ok(Type::Unit),
344            ast::Type::Int => Ok(Type::Int),
345            ast::Type::Bool => Ok(Type::Bool),
346            ast::Type::Bytes => Ok(Type::Bytes),
347            ast::Type::Address => Ok(Type::Address),
348            ast::Type::Utxo => Ok(Type::Utxo),
349            ast::Type::UtxoRef => Ok(Type::UtxoRef),
350            ast::Type::AnyAsset => Ok(Type::AnyAsset),
351            ast::Type::List(_) => Ok(Type::List),
352            ast::Type::Map(_, _) => Ok(Type::Map),
353            ast::Type::Custom(x) => Ok(Type::Custom(x.value.clone())),
354        }
355    }
356}
357
358impl IntoLower for ast::AddOp {
359    type Output = ir::Expression;
360
361    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
362        let left = self.lhs.into_lower(ctx)?;
363        let right = self.rhs.into_lower(ctx)?;
364
365        Ok(ir::Expression::EvalBuiltIn(Box::new(ir::BuiltInOp::Add(
366            left, right,
367        ))))
368    }
369}
370
371impl IntoLower for ast::SubOp {
372    type Output = ir::Expression;
373
374    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
375        let left = self.lhs.into_lower(ctx)?;
376        let right = self.rhs.into_lower(ctx)?;
377
378        Ok(ir::Expression::EvalBuiltIn(Box::new(ir::BuiltInOp::Sub(
379            left, right,
380        ))))
381    }
382}
383
384impl IntoLower for ast::ConcatOp {
385    type Output = ir::Expression;
386
387    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
388        let left = self.lhs.into_lower(ctx)?;
389        let right = self.rhs.into_lower(ctx)?;
390
391        Ok(ir::Expression::EvalBuiltIn(Box::new(
392            ir::BuiltInOp::Concat(left, right),
393        )))
394    }
395}
396
397impl IntoLower for ast::NegateOp {
398    type Output = ir::Expression;
399
400    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
401        let operand = self.operand.into_lower(ctx)?;
402
403        Ok(ir::Expression::EvalBuiltIn(Box::new(
404            ir::BuiltInOp::Negate(operand),
405        )))
406    }
407}
408
409impl IntoLower for ast::FnCall {
410    type Output = ir::Expression;
411
412    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
413        let function_name = &self.callee.value;
414
415        match function_name.as_str() {
416            "min_utxo" => {
417                if self.args.len() != 1 {
418                    return Err(Error::InvalidAst(format!(
419                        "min_utxo expects 1 argument, got {}",
420                        self.args.len()
421                    )));
422                }
423                let arg = self.args[0].into_lower(ctx)?;
424                Ok(ir::Expression::EvalCompiler(Box::new(
425                    ir::CompilerOp::ComputeMinUtxo(arg),
426                )))
427            }
428            "tip_slot" => {
429                if !self.args.is_empty() {
430                    return Err(Error::InvalidAst(format!(
431                        "tip_slot expects 0 arguments, got {}",
432                        self.args.len()
433                    )));
434                }
435                Ok(ir::Expression::EvalCompiler(Box::new(
436                    ir::CompilerOp::ComputeTipSlot,
437                )))
438            }
439            "slot_to_time" => {
440                if self.args.len() != 1 {
441                    return Err(Error::InvalidAst(format!(
442                        "slot_to_time expects 1 argument, got {}",
443                        self.args.len()
444                    )));
445                }
446                let arg = self.args[0].into_lower(ctx)?;
447                Ok(ir::Expression::EvalCompiler(Box::new(
448                    ir::CompilerOp::ComputeSlotToTime(arg),
449                )))
450            }
451            "time_to_slot" => {
452                if self.args.len() != 1 {
453                    return Err(Error::InvalidAst(format!(
454                        "time_to_slot expects 1 argument, got {}",
455                        self.args.len()
456                    )));
457                }
458
459                let arg = self.args[0].into_lower(ctx)?;
460
461                Ok(ir::Expression::EvalCompiler(Box::new(
462                    ir::CompilerOp::ComputeTimeToSlot(arg),
463                )))
464            }
465            _ => {
466                // Try to coerce as asset - if it fails, we'll get an error
467
468                match coerce_identifier_into_asset_def(&self.callee) {
469                    Ok(asset_def) => {
470                        let policy = asset_def.policy.into_lower(ctx)?;
471                        let asset_name = asset_def.asset_name.into_lower(ctx)?;
472                        let amount = self.args[0].into_lower(ctx)?;
473
474                        Ok(ir::Expression::Assets(vec![ir::AssetExpr {
475                            policy,
476                            asset_name,
477                            amount,
478                        }]))
479                    }
480                    Err(_) => Err(Error::InvalidAst(format!(
481                        "unknown function: {}",
482                        function_name
483                    ))),
484                }
485            }
486        }
487    }
488}
489
490impl IntoLower for ast::PropertyOp {
491    type Output = ir::Expression;
492
493    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
494        let object = self.operand.into_lower(ctx)?;
495
496        let ty = self
497            .operand
498            .target_type()
499            .ok_or(Error::MissingAnalyzePhase(format!("{0:?}", self.operand)))?;
500
501        let prop_index =
502            ty.property_index(*self.property.clone())
503                .ok_or(Error::InvalidProperty(
504                    format!("{:?}", self.property),
505                    ty.to_string(),
506                ))?;
507
508        Ok(ir::Expression::EvalBuiltIn(Box::new(
509            ir::BuiltInOp::Property(object, prop_index.into_lower(ctx)?),
510        )))
511    }
512}
513
514impl IntoLower for ast::ListConstructor {
515    type Output = Vec<ir::Expression>;
516
517    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
518        let elements = self
519            .elements
520            .iter()
521            .map(|x| x.into_lower(ctx))
522            .collect::<Result<Vec<_>, _>>()?;
523
524        Ok(elements)
525    }
526}
527
528impl IntoLower for ast::MapConstructor {
529    type Output = ir::Expression;
530
531    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
532        let pairs = self
533            .fields
534            .iter()
535            .map(|field| {
536                let key = field.key.into_lower(ctx)?;
537                let value = field.value.into_lower(ctx)?;
538                Ok((key, value))
539            })
540            .collect::<Result<Vec<_>, _>>()?;
541
542        Ok(ir::Expression::Map(pairs))
543    }
544}
545
546impl IntoLower for ast::DataExpr {
547    type Output = ir::Expression;
548
549    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
550        let out = match self {
551            ast::DataExpr::None => ir::Expression::None,
552            ast::DataExpr::Number(x) => Self::Output::Number(*x as i128),
553            ast::DataExpr::Bool(x) => ir::Expression::Bool(*x),
554            ast::DataExpr::String(x) => ir::Expression::String(x.value.clone()),
555            ast::DataExpr::HexString(x) => ir::Expression::Bytes(hex_decode(&x.value)?),
556            ast::DataExpr::StructConstructor(x) => ir::Expression::Struct(x.into_lower(ctx)?),
557            ast::DataExpr::ListConstructor(x) => ir::Expression::List(x.into_lower(ctx)?),
558            ast::DataExpr::MapConstructor(x) => x.into_lower(ctx)?,
559            ast::DataExpr::AnyAssetConstructor(x) => x.into_lower(ctx)?,
560            ast::DataExpr::Unit => ir::Expression::Struct(ir::StructExpr::unit()),
561            ast::DataExpr::Identifier(x) => x.into_lower(ctx)?,
562            ast::DataExpr::AddOp(x) => x.into_lower(ctx)?,
563            ast::DataExpr::SubOp(x) => x.into_lower(ctx)?,
564            ast::DataExpr::ConcatOp(x) => x.into_lower(ctx)?,
565            ast::DataExpr::NegateOp(x) => x.into_lower(ctx)?,
566            ast::DataExpr::PropertyOp(x) => x.into_lower(ctx)?,
567            ast::DataExpr::UtxoRef(x) => x.into_lower(ctx)?,
568            ast::DataExpr::FnCall(x) => x.into_lower(ctx)?,
569            ast::DataExpr::MinUtxo(x) => ir::Expression::EvalCompiler(Box::new(
570                ir::CompilerOp::ComputeMinUtxo(x.into_lower(ctx)?),
571            )),
572            ast::DataExpr::ComputeTipSlot => {
573                ir::Expression::EvalCompiler(Box::new(ir::CompilerOp::ComputeTipSlot))
574            }
575            ast::DataExpr::SlotToTime(x) => ir::Expression::EvalCompiler(Box::new(
576                ir::CompilerOp::ComputeSlotToTime(x.into_lower(ctx)?),
577            )),
578            ast::DataExpr::TimeToSlot(x) => ir::Expression::EvalCompiler(Box::new(
579                ir::CompilerOp::ComputeTimeToSlot(x.into_lower(ctx)?),
580            )),
581        };
582
583        Ok(out)
584    }
585}
586
587impl IntoLower for ast::AnyAssetConstructor {
588    type Output = ir::Expression;
589
590    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
591        let ctx = &ctx.enter_datum_expr();
592        let policy = self.policy.into_lower(ctx)?;
593
594        let ctx = &ctx.enter_datum_expr();
595        let asset_name = self.asset_name.into_lower(ctx)?;
596
597        let ctx = &ctx.enter_datum_expr();
598        let amount = self.amount.into_lower(ctx)?;
599
600        Ok(ir::Expression::Assets(vec![ir::AssetExpr {
601            policy,
602            asset_name,
603            amount,
604        }]))
605    }
606}
607
608impl IntoLower for ast::InputBlockField {
609    type Output = ir::Expression;
610
611    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
612        match self {
613            ast::InputBlockField::From(x) => {
614                let ctx = ctx.enter_address_expr();
615                x.into_lower(&ctx)
616            }
617            ast::InputBlockField::DatumIs(_) => todo!(),
618            ast::InputBlockField::MinAmount(x) => {
619                let ctx = ctx.enter_asset_expr();
620                x.into_lower(&ctx)
621            }
622            ast::InputBlockField::Redeemer(x) => {
623                let ctx = ctx.enter_datum_expr();
624                x.into_lower(&ctx)
625            }
626            ast::InputBlockField::Ref(x) => x.into_lower(ctx),
627        }
628    }
629}
630
631impl IntoLower for ast::InputBlock {
632    type Output = ir::Input;
633
634    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
635        let from_field = self.find("from");
636
637        let address = from_field.map(|x| x.into_lower(ctx)).transpose()?;
638
639        let min_amount = self
640            .find("min_amount")
641            .map(|x| x.into_lower(ctx))
642            .transpose()?;
643
644        let r#ref = self.find("ref").map(|x| x.into_lower(ctx)).transpose()?;
645
646        let redeemer = self
647            .find("redeemer")
648            .map(|x| x.into_lower(ctx))
649            .transpose()?
650            .unwrap_or(ir::Expression::None);
651
652        let query = ir::InputQuery {
653            address: address.unwrap_or(ir::Expression::None),
654            min_amount: min_amount.unwrap_or(ir::Expression::None),
655            r#ref: r#ref.unwrap_or(ir::Expression::None),
656            many: self.many,
657            collateral: false,
658        };
659
660        let param = ir::Param::ExpectInput(self.name.to_lowercase().clone(), query);
661
662        let input = ir::Input {
663            name: self.name.to_lowercase().clone(),
664            utxos: param.into(),
665            redeemer,
666        };
667
668        Ok(input)
669    }
670}
671
672impl IntoLower for ast::OutputBlockField {
673    type Output = ir::Expression;
674
675    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
676        match self {
677            ast::OutputBlockField::To(x) => {
678                let ctx = ctx.enter_address_expr();
679                x.into_lower(&ctx)
680            }
681            ast::OutputBlockField::Amount(x) => {
682                let ctx = ctx.enter_asset_expr();
683                x.into_lower(&ctx)
684            }
685            ast::OutputBlockField::Datum(x) => {
686                let ctx = ctx.enter_datum_expr();
687                x.into_lower(&ctx)
688            }
689        }
690    }
691}
692
693impl IntoLower for ast::OutputBlock {
694    type Output = ir::Output;
695
696    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
697        let address = self.find("to").into_lower(ctx)?.unwrap_or_default();
698        let datum = self.find("datum").into_lower(ctx)?.unwrap_or_default();
699        let amount = self.find("amount").into_lower(ctx)?.unwrap_or_default();
700
701        Ok(ir::Output {
702            address,
703            datum,
704            amount,
705            optional: self.optional,
706        })
707    }
708}
709
710impl IntoLower for ast::ValidityBlockField {
711    type Output = ir::Expression;
712
713    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
714        match self {
715            ast::ValidityBlockField::SinceSlot(x) => x.into_lower(ctx),
716            ast::ValidityBlockField::UntilSlot(x) => x.into_lower(ctx),
717        }
718    }
719}
720
721impl IntoLower for ast::ValidityBlock {
722    type Output = ir::Validity;
723
724    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
725        let since = self.find("since_slot").into_lower(ctx)?.unwrap_or_default();
726        let until = self.find("until_slot").into_lower(ctx)?.unwrap_or_default();
727
728        Ok(ir::Validity { since, until })
729    }
730}
731
732impl IntoLower for ast::MintBlockField {
733    type Output = ir::Expression;
734
735    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
736        match self {
737            ast::MintBlockField::Amount(x) => x.into_lower(ctx),
738            ast::MintBlockField::Redeemer(x) => x.into_lower(ctx),
739        }
740    }
741}
742
743impl IntoLower for ast::MintBlock {
744    type Output = ir::Mint;
745
746    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
747        let amount = self.find("amount").into_lower(ctx)?.unwrap_or_default();
748        let redeemer = self.find("redeemer").into_lower(ctx)?.unwrap_or_default();
749
750        Ok(ir::Mint { amount, redeemer })
751    }
752}
753
754impl IntoLower for ast::MetadataBlockField {
755    type Output = ir::Metadata;
756    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
757        Ok(ir::Metadata {
758            key: self.key.into_lower(ctx)?,
759            value: self.value.into_lower(ctx)?,
760        })
761    }
762}
763
764impl IntoLower for ast::MetadataBlock {
765    type Output = Vec<ir::Metadata>;
766
767    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
768        let fields = self
769            .fields
770            .iter()
771            .map(|metadata_field| metadata_field.into_lower(ctx))
772            .collect::<Result<Vec<_>, _>>()?;
773
774        Ok(fields)
775    }
776}
777
778impl IntoLower for ast::ChainSpecificBlock {
779    type Output = ir::AdHocDirective;
780
781    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
782        match self {
783            ast::ChainSpecificBlock::Cardano(x) => x.into_lower(ctx),
784        }
785    }
786}
787
788impl IntoLower for ast::ReferenceBlock {
789    type Output = ir::Expression;
790
791    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
792        self.r#ref.into_lower(ctx)
793    }
794}
795
796impl IntoLower for ast::CollateralBlockField {
797    type Output = ir::Expression;
798
799    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
800        match self {
801            ast::CollateralBlockField::From(x) => x.into_lower(ctx),
802            ast::CollateralBlockField::MinAmount(x) => x.into_lower(ctx),
803            ast::CollateralBlockField::Ref(x) => x.into_lower(ctx),
804        }
805    }
806}
807
808impl IntoLower for ast::CollateralBlock {
809    type Output = ir::Collateral;
810
811    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
812        let from = self.find("from").map(|x| x.into_lower(ctx)).transpose()?;
813
814        let min_amount = self
815            .find("min_amount")
816            .map(|x| x.into_lower(ctx))
817            .transpose()?;
818
819        let r#ref = self.find("ref").map(|x| x.into_lower(ctx)).transpose()?;
820
821        let query = ir::InputQuery {
822            address: from.unwrap_or(ir::Expression::None),
823            min_amount: min_amount.unwrap_or(ir::Expression::None),
824            r#ref: r#ref.unwrap_or(ir::Expression::None),
825            many: false,
826            collateral: true,
827        };
828
829        let param = ir::Param::ExpectInput("collateral".to_string(), query);
830
831        let collateral = ir::Collateral {
832            utxos: param.into(),
833        };
834
835        Ok(collateral)
836    }
837}
838
839impl IntoLower for ast::SignersBlock {
840    type Output = ir::Signers;
841
842    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
843        Ok(ir::Signers {
844            signers: self
845                .signers
846                .iter()
847                .map(|x| x.into_lower(ctx))
848                .collect::<Result<Vec<_>, _>>()?,
849        })
850    }
851}
852
853impl IntoLower for ast::TxDef {
854    type Output = ir::Tx;
855
856    fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
857        let ir = ir::Tx {
858            references: self
859                .references
860                .iter()
861                .map(|x| x.into_lower(ctx))
862                .collect::<Result<Vec<_>, _>>()?,
863            inputs: self
864                .inputs
865                .iter()
866                .map(|x| x.into_lower(ctx))
867                .collect::<Result<Vec<_>, _>>()?,
868            outputs: self
869                .outputs
870                .iter()
871                .map(|x| x.into_lower(ctx))
872                .collect::<Result<Vec<_>, _>>()?,
873            validity: self
874                .validity
875                .as_ref()
876                .map(|x| x.into_lower(ctx))
877                .transpose()?,
878            mints: self
879                .mints
880                .iter()
881                .map(|x| x.into_lower(ctx))
882                .collect::<Result<Vec<_>, _>>()?,
883            burns: self
884                .burns
885                .iter()
886                .map(|x| x.into_lower(ctx))
887                .collect::<Result<Vec<_>, _>>()?,
888            adhoc: self
889                .adhoc
890                .iter()
891                .map(|x| x.into_lower(ctx))
892                .collect::<Result<Vec<_>, _>>()?,
893            fees: ir::Param::ExpectFees.into(),
894            collateral: self
895                .collateral
896                .iter()
897                .map(|x| x.into_lower(ctx))
898                .collect::<Result<Vec<_>, _>>()?,
899            signers: self
900                .signers
901                .as_ref()
902                .map(|x| x.into_lower(ctx))
903                .transpose()?,
904            metadata: self
905                .metadata
906                .as_ref()
907                .map(|x| x.into_lower(ctx))
908                .transpose()?
909                .unwrap_or(vec![]),
910        };
911
912        Ok(ir)
913    }
914}
915
916pub fn lower_tx(ast: &ast::TxDef) -> Result<ir::Tx, Error> {
917    let ctx = &Context::default();
918
919    let tx = ast.into_lower(ctx)?;
920
921    Ok(tx)
922}
923
924/// Lowers the Tx3 language to the intermediate representation.
925///
926/// This function takes an AST and converts it into the intermediate
927/// representation (IR) of the Tx3 language.
928///
929/// # Arguments
930///
931/// * `ast` - The AST to lower
932///
933/// # Returns
934///
935/// * `Result<ir::Program, Error>` - The lowered intermediate representation
936pub fn lower(ast: &ast::Program, template: &str) -> Result<ir::Tx, Error> {
937    let tx = ast
938        .txs
939        .iter()
940        .find(|x| x.name.value == template)
941        .ok_or(Error::InvalidAst("tx not found".to_string()))?;
942
943    lower_tx(tx)
944}
945
946#[cfg(test)]
947mod tests {
948    use assert_json_diff::assert_json_eq;
949    use paste::paste;
950
951    use super::*;
952    use crate::parsing::{self};
953
954    fn make_snapshot_if_missing(example: &str, name: &str, tx: &ir::Tx) {
955        let manifest_dir = env!("CARGO_MANIFEST_DIR");
956
957        let path = format!("{}/../../examples/{}.{}.tir", manifest_dir, example, name);
958
959        if !std::fs::exists(&path).unwrap() {
960            let ir = serde_json::to_string_pretty(tx).unwrap();
961            std::fs::write(&path, ir).unwrap();
962        }
963    }
964
965    fn test_lowering_example(example: &str) {
966        let manifest_dir = env!("CARGO_MANIFEST_DIR");
967        let mut program = parsing::parse_well_known_example(example);
968
969        crate::analyzing::analyze(&mut program).ok().unwrap();
970
971        for tx in program.txs.iter() {
972            let tir = lower(&program, &tx.name.value).unwrap();
973
974            make_snapshot_if_missing(example, &tx.name.value, &tir);
975
976            let tir_file = format!(
977                "{}/../../examples/{}.{}.tir",
978                manifest_dir, example, tx.name.value
979            );
980
981            let expected = std::fs::read_to_string(tir_file).unwrap();
982            let expected: ir::Tx = serde_json::from_str(&expected).unwrap();
983
984            assert_json_eq!(tir, expected);
985        }
986    }
987
988    #[macro_export]
989    macro_rules! test_lowering {
990        ($name:ident) => {
991            paste! {
992                #[test]
993                fn [<test_example_ $name>]() {
994                    test_lowering_example(stringify!($name));
995                }
996            }
997        };
998    }
999
1000    test_lowering!(lang_tour);
1001
1002    test_lowering!(transfer);
1003
1004    test_lowering!(swap);
1005
1006    test_lowering!(asteria);
1007
1008    test_lowering!(vesting);
1009
1010    test_lowering!(faucet);
1011
1012    test_lowering!(input_datum);
1013
1014    test_lowering!(env_vars);
1015
1016    test_lowering!(local_vars);
1017
1018    test_lowering!(cardano_witness);
1019
1020    test_lowering!(reference_script);
1021
1022    test_lowering!(withdrawal);
1023    test_lowering!(map);
1024    test_lowering!(burn);
1025
1026    test_lowering!(min_utxo);
1027
1028    test_lowering!(donation);
1029
1030    test_lowering!(list_concat);
1031
1032    test_lowering!(buidler_fest_2026);
1033}