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