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 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 lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
201        self.map(|x| x.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 lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
212        self.as_ref().lower(ctx)
213    }
214}
215
216impl IntoLower for ast::Identifier {
217    type Output = ir::Expression;
218
219    fn 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.lower(ctx)?).into())
228            }
229            ast::Symbol::LocalExpr(expr) => Ok(expr.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.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.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.lower(ctx)?).into())
252            }
253            ast::Symbol::PolicyDef(x) => {
254                let policy = x.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 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 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.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                    .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 lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
345        match self {
346            ast::PolicyField::Hash(x) => x.lower(ctx),
347            ast::PolicyField::Script(x) => x.lower(ctx),
348            ast::PolicyField::Ref(x) => x.lower(ctx),
349        }
350    }
351}
352
353impl IntoLower for ast::PolicyDef {
354    type Output = ir::PolicyExpr;
355
356    fn 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                    .lower(ctx)?;
372
373                let rf = x.find_field("ref").map(|x| x.lower(ctx)).transpose()?;
374
375                let script = x.find_field("script").map(|x| x.lower(ctx)).transpose()?;
376
377                let script = match (rf, script) {
378                    (Some(rf), Some(script)) => ir::ScriptSource::new_ref(rf, script),
379                    (Some(rf), None) => {
380                        ir::ScriptSource::expect_ref_input(self.name.value.clone(), rf)
381                    }
382                    (None, Some(script)) => ir::ScriptSource::new_embedded(script),
383                    (None, None) => ir::ScriptSource::expect_parameter(self.name.value.clone()),
384                };
385
386                Ok(ir::PolicyExpr {
387                    name: self.name.value.clone(),
388                    hash,
389                    script,
390                })
391            }
392        }
393    }
394}
395
396impl IntoLower for ast::Type {
397    type Output = Type;
398
399    fn lower(&self, _: &Context) -> Result<Self::Output, Error> {
400        match self {
401            ast::Type::Undefined => Ok(Type::Undefined),
402            ast::Type::Unit => Ok(Type::Unit),
403            ast::Type::Int => Ok(Type::Int),
404            ast::Type::Bool => Ok(Type::Bool),
405            ast::Type::Bytes => Ok(Type::Bytes),
406            ast::Type::Address => Ok(Type::Address),
407            ast::Type::Utxo => Ok(Type::Utxo),
408            ast::Type::UtxoRef => Ok(Type::UtxoRef),
409            ast::Type::AnyAsset => Ok(Type::AnyAsset),
410            ast::Type::List(_) => Ok(Type::List),
411            ast::Type::Map(_, _) => Ok(Type::Map),
412            ast::Type::Tuple(_) => Ok(Type::Tuple),
413            ast::Type::Custom(x) => Ok(Type::Custom(x.value.clone())),
414        }
415    }
416}
417
418impl IntoLower for ast::AddOp {
419    type Output = ir::Expression;
420
421    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
422        let left = self.lhs.lower(ctx)?;
423        let right = self.rhs.lower(ctx)?;
424
425        Ok(ir::Expression::EvalBuiltIn(Box::new(ir::BuiltInOp::Add(
426            left, right,
427        ))))
428    }
429}
430
431impl IntoLower for ast::SubOp {
432    type Output = ir::Expression;
433
434    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
435        let left = self.lhs.lower(ctx)?;
436        let right = self.rhs.lower(ctx)?;
437
438        Ok(ir::Expression::EvalBuiltIn(Box::new(ir::BuiltInOp::Sub(
439            left, right,
440        ))))
441    }
442}
443
444impl IntoLower for ast::MulOp {
445    type Output = ir::Expression;
446
447    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
448        let left = self.lhs.lower(ctx)?;
449        let right = self.rhs.lower(ctx)?;
450
451        Ok(ir::Expression::EvalBuiltIn(Box::new(ir::BuiltInOp::Mul(
452            left, right,
453        ))))
454    }
455}
456
457impl IntoLower for ast::DivOp {
458    type Output = ir::Expression;
459
460    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
461        let left = self.lhs.lower(ctx)?;
462        let right = self.rhs.lower(ctx)?;
463
464        Ok(ir::Expression::EvalBuiltIn(Box::new(ir::BuiltInOp::Div(
465            left, right,
466        ))))
467    }
468}
469
470impl IntoLower for ast::ConcatOp {
471    type Output = ir::Expression;
472
473    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
474        let left = self.lhs.lower(ctx)?;
475        let right = self.rhs.lower(ctx)?;
476
477        Ok(ir::Expression::EvalBuiltIn(Box::new(
478            ir::BuiltInOp::Concat(left, right),
479        )))
480    }
481}
482
483impl IntoLower for ast::NegateOp {
484    type Output = ir::Expression;
485
486    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
487        let operand = self.operand.lower(ctx)?;
488
489        Ok(ir::Expression::EvalBuiltIn(Box::new(
490            ir::BuiltInOp::Negate(operand),
491        )))
492    }
493}
494
495impl IntoLower for ast::FnCall {
496    type Output = ir::Expression;
497
498    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
499        // A callee that resolves to a function definition is either a built-in
500        // (lowered to a dedicated compiler op) or a user-defined function
501        // (inlined below).
502        if let Some(fn_def) = self.callee.symbol.as_ref().and_then(|s| s.as_fn_def()) {
503            if let Some(builtin) = fn_def.builtin {
504                return crate::builtins::resolve(builtin).lower_call(&self.args, ctx);
505            }
506
507            // Inline a user-defined function: lower its analyzed body, then
508            // substitute each parameter with the lowered call argument.
509            let body = fn_def.body.as_ref().ok_or_else(|| {
510                Error::InvalidAst(format!(
511                    "function '{}' has neither a body nor a built-in kind",
512                    fn_def.name.value
513                ))
514            })?;
515
516            let lowered_body = body.result.lower(ctx)?;
517
518            let mut subs = std::collections::HashMap::new();
519            for (param, arg) in fn_def.parameters.parameters.iter().zip(&self.args) {
520                subs.insert(param.name.value.to_lowercase(), arg.lower(ctx)?);
521            }
522
523            use tx3_tir::Node;
524            let mut visitor = ParamSubstituter { subs: &subs };
525            return lowered_body
526                .apply(&mut visitor)
527                .map_err(|e| Error::InvalidAst(e.to_string()));
528        }
529
530        // Otherwise the callee must name an asset; treat the call as an asset
531        // constructor.
532        match coerce_identifier_into_asset_def(&self.callee) {
533            Ok(asset_def) => {
534                let policy = asset_def.policy.lower(ctx)?;
535                let asset_name = asset_def.asset_name.lower(ctx)?;
536                let amount = self.args[0].lower(ctx)?;
537
538                Ok(ir::Expression::Assets(vec![ir::AssetExpr {
539                    policy,
540                    asset_name,
541                    amount,
542                }]))
543            }
544            Err(_) => Err(Error::InvalidAst(format!(
545                "unknown function: {}",
546                self.callee.value
547            ))),
548        }
549    }
550}
551
552/// TIR visitor used by function inlining to replace each `EvalParam` standing
553/// for a function parameter with the lowered call argument.
554struct ParamSubstituter<'a> {
555    subs: &'a std::collections::HashMap<String, ir::Expression>,
556}
557
558impl tx3_tir::Visitor for ParamSubstituter<'_> {
559    fn reduce(&mut self, expr: ir::Expression) -> Result<ir::Expression, tx3_tir::reduce::Error> {
560        if let ir::Expression::EvalParam(ref param) = expr {
561            if let ir::Param::ExpectValue(name, _) = param.as_ref() {
562                if let Some(replacement) = self.subs.get(name) {
563                    return Ok(replacement.clone());
564                }
565            }
566        }
567        Ok(expr)
568    }
569}
570
571impl IntoLower for ast::PropertyOp {
572    type Output = ir::Expression;
573
574    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
575        // `ok_or_else`, not `ok_or`: the error payload Debug-formats the whole
576        // operand subtree, and an analyzed operand embeds its resolved symbols
577        // (a `Symbol::Input` carries an entire `InputBlock` by value). Building
578        // it eagerly on the success path is what made a feature-dense tx take
579        // seconds to lower.
580        let ty = self
581            .operand
582            .target_type()
583            .ok_or_else(|| Error::MissingAnalyzePhase(format!("{0:?}", self.operand)))?;
584
585        // Property access is a structured-data read, so the operand must
586        // lower in datum context regardless of the surrounding expression:
587        // `datum_is`-annotated operands then coerce to their datum (which is
588        // `Indexable`) instead of their assets (which are not), even inside
589        // amount/min_amount/change expressions.
590        let object = self.operand.lower(&ctx.enter_datum_expr())?;
591
592        let prop_index = ty.property_index(*self.property.clone()).ok_or_else(|| {
593            Error::InvalidProperty(format!("{:?}", self.property), ty.to_string())
594        })?;
595
596        Ok(ir::Expression::EvalBuiltIn(Box::new(
597            ir::BuiltInOp::Property(object, prop_index.lower(ctx)?),
598        )))
599    }
600}
601
602impl IntoLower for ast::ListConstructor {
603    type Output = Vec<ir::Expression>;
604
605    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
606        let elements = self
607            .elements
608            .iter()
609            .map(|x| x.lower(ctx))
610            .collect::<Result<Vec<_>, _>>()?;
611
612        Ok(elements)
613    }
614}
615
616impl IntoLower for ast::TupleConstructor {
617    type Output = ir::Expression;
618
619    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
620        let elements = self
621            .elements
622            .iter()
623            .map(|x| x.lower(ctx))
624            .collect::<Result<Vec<_>, _>>()?;
625
626        Ok(ir::Expression::Tuple(elements))
627    }
628}
629
630impl IntoLower for ast::MapConstructor {
631    type Output = ir::Expression;
632
633    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
634        let pairs = self
635            .fields
636            .iter()
637            .map(|field| {
638                let key = field.key.lower(ctx)?;
639                let value = field.value.lower(ctx)?;
640                Ok((key, value))
641            })
642            .collect::<Result<Vec<_>, _>>()?;
643
644        Ok(ir::Expression::Map(pairs))
645    }
646}
647
648impl IntoLower for ast::DataExpr {
649    type Output = ir::Expression;
650
651    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
652        let out = match self {
653            ast::DataExpr::None => ir::Expression::None,
654            ast::DataExpr::Number(x) => Self::Output::Number(*x as i128),
655            ast::DataExpr::Bool(x) => ir::Expression::Bool(*x),
656            ast::DataExpr::String(x) => ir::Expression::String(x.value.clone()),
657            ast::DataExpr::HexString(x) => ir::Expression::Bytes(hex_decode(&x.value)?),
658            ast::DataExpr::StructConstructor(x) => ir::Expression::Struct(x.lower(ctx)?),
659            ast::DataExpr::ListConstructor(x) => ir::Expression::List(x.lower(ctx)?),
660            ast::DataExpr::MapConstructor(x) => x.lower(ctx)?,
661            ast::DataExpr::TupleConstructor(x) => x.lower(ctx)?,
662            ast::DataExpr::AnyAssetConstructor(x) => x.lower(ctx)?,
663            ast::DataExpr::Unit => ir::Expression::Struct(ir::StructExpr::unit()),
664            ast::DataExpr::Identifier(x) => x.lower(ctx)?,
665            ast::DataExpr::AddOp(x) => x.lower(ctx)?,
666            ast::DataExpr::SubOp(x) => x.lower(ctx)?,
667            ast::DataExpr::MulOp(x) => x.lower(ctx)?,
668            ast::DataExpr::DivOp(x) => x.lower(ctx)?,
669            ast::DataExpr::ConcatOp(x) => x.lower(ctx)?,
670            ast::DataExpr::NegateOp(x) => x.lower(ctx)?,
671            ast::DataExpr::PropertyOp(x) => x.lower(ctx)?,
672            ast::DataExpr::UtxoRef(x) => x.lower(ctx)?,
673            ast::DataExpr::FnCall(x) => x.lower(ctx)?,
674        };
675
676        Ok(out)
677    }
678}
679
680impl IntoLower for ast::AnyAssetConstructor {
681    type Output = ir::Expression;
682
683    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
684        let ctx = &ctx.enter_datum_expr();
685        let policy = self.policy.lower(ctx)?;
686
687        let ctx = &ctx.enter_datum_expr();
688        let asset_name = self.asset_name.lower(ctx)?;
689
690        let ctx = &ctx.enter_datum_expr();
691        let amount = self.amount.lower(ctx)?;
692
693        Ok(ir::Expression::Assets(vec![ir::AssetExpr {
694            policy,
695            asset_name,
696            amount,
697        }]))
698    }
699}
700
701impl IntoLower for ast::InputBlockField {
702    type Output = ir::Expression;
703
704    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
705        match self {
706            ast::InputBlockField::From(x) => {
707                // Spending from a script address runs its script.
708                let ctx = ctx.enter_address_expr().capturing_policy_refs();
709                x.lower(&ctx)
710            }
711            ast::InputBlockField::DatumIs(_) => Ok(ir::Expression::None),
712            ast::InputBlockField::MinAmount(x) => {
713                let ctx = ctx.enter_asset_expr();
714                x.lower(&ctx)
715            }
716            ast::InputBlockField::Redeemer(x) => {
717                let ctx = ctx.enter_datum_expr();
718                x.lower(&ctx)
719            }
720            ast::InputBlockField::Ref(x) => x.lower(ctx),
721        }
722    }
723}
724
725impl IntoLower for ast::InputBlock {
726    type Output = ir::Input;
727
728    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
729        let from_field = self.find("from");
730
731        let address = from_field.map(|x| x.lower(ctx)).transpose()?;
732
733        let min_amount = self.find("min_amount").map(|x| x.lower(ctx)).transpose()?;
734
735        let r#ref = self.find("ref").map(|x| x.lower(ctx)).transpose()?;
736
737        let redeemer = self
738            .find("redeemer")
739            .map(|x| x.lower(ctx))
740            .transpose()?
741            .unwrap_or(ir::Expression::None);
742
743        let query = ir::InputQuery {
744            address: address.unwrap_or(ir::Expression::None),
745            min_amount: min_amount.unwrap_or(ir::Expression::None),
746            r#ref: r#ref.unwrap_or(ir::Expression::None),
747            many: self.many,
748            collateral: false,
749        };
750
751        let param = ir::Param::ExpectInput(self.name.to_lowercase().clone(), query);
752
753        let input = ir::Input {
754            name: self.name.to_lowercase().clone(),
755            utxos: param.into(),
756            redeemer,
757        };
758
759        Ok(input)
760    }
761}
762
763impl IntoLower for ast::OutputBlockField {
764    type Output = ir::Expression;
765
766    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
767        match self {
768            ast::OutputBlockField::To(x) => {
769                let ctx = ctx.enter_address_expr();
770                x.lower(&ctx)
771            }
772            ast::OutputBlockField::Amount(x) => {
773                let ctx = ctx.enter_asset_expr();
774                x.lower(&ctx)
775            }
776            ast::OutputBlockField::Datum(x) => {
777                let ctx = ctx.enter_datum_expr();
778                x.lower(&ctx)
779            }
780        }
781    }
782}
783
784impl IntoLower for ast::OutputBlock {
785    type Output = ir::Output;
786
787    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
788        let address = self.find("to").lower(ctx)?.unwrap_or_default();
789        let datum = self.find("datum").lower(ctx)?.unwrap_or_default();
790        let amount = self.find("amount").lower(ctx)?.unwrap_or_default();
791
792        Ok(ir::Output {
793            address,
794            datum,
795            amount,
796            optional: self.optional,
797        })
798    }
799}
800
801impl IntoLower for ast::ValidityBlockField {
802    type Output = ir::Expression;
803
804    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
805        match self {
806            ast::ValidityBlockField::SinceSlot(x) => x.lower(ctx),
807            ast::ValidityBlockField::UntilSlot(x) => x.lower(ctx),
808        }
809    }
810}
811
812impl IntoLower for ast::ValidityBlock {
813    type Output = ir::Validity;
814
815    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
816        let since = self.find("since_slot").lower(ctx)?.unwrap_or_default();
817        let until = self.find("until_slot").lower(ctx)?.unwrap_or_default();
818
819        Ok(ir::Validity { since, until })
820    }
821}
822
823impl IntoLower for ast::MintBlockField {
824    type Output = ir::Expression;
825
826    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
827        match self {
828            // Minting/burning runs the asset's policy script.
829            ast::MintBlockField::Amount(x) => x.lower(&ctx.capturing_policy_refs()),
830            ast::MintBlockField::Redeemer(x) => x.lower(ctx),
831        }
832    }
833}
834
835impl IntoLower for ast::MintBlock {
836    type Output = ir::Mint;
837
838    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
839        let amount = self.find("amount").lower(ctx)?.unwrap_or_default();
840        let redeemer = self.find("redeemer").lower(ctx)?.unwrap_or_default();
841
842        Ok(ir::Mint { amount, redeemer })
843    }
844}
845
846impl IntoLower for ast::MetadataBlockField {
847    type Output = ir::Metadata;
848    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
849        Ok(ir::Metadata {
850            key: self.key.lower(ctx)?,
851            value: self.value.lower(ctx)?,
852        })
853    }
854}
855
856impl IntoLower for ast::MetadataBlock {
857    type Output = Vec<ir::Metadata>;
858
859    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
860        let fields = self
861            .fields
862            .iter()
863            .map(|metadata_field| metadata_field.lower(ctx))
864            .collect::<Result<Vec<_>, _>>()?;
865
866        Ok(fields)
867    }
868}
869
870impl IntoLower for ast::ChainSpecificBlock {
871    type Output = ir::AdHocDirective;
872
873    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
874        match self {
875            ast::ChainSpecificBlock::Cardano(x) => x.lower(ctx),
876        }
877    }
878}
879
880impl IntoLower for ast::ReferenceBlock {
881    type Output = ir::Expression;
882
883    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
884        let r#ref = self.r#ref.lower(ctx)?;
885
886        let query = ir::InputQuery {
887            address: ir::Expression::None,
888            min_amount: ir::Expression::None,
889            r#ref,
890            many: false,
891            collateral: false,
892        };
893
894        let inner = ir::Param::ExpectInput(self.name.to_lowercase(), query).into();
895
896        let out = if ctx.is_asset_expr() {
897            ir::Coerce::IntoAssets(inner).into()
898        } else if ctx.is_datum_expr() {
899            ir::Coerce::IntoDatum(inner).into()
900        } else {
901            inner
902        };
903
904        Ok(out)
905    }
906}
907
908impl IntoLower for ast::CollateralBlockField {
909    type Output = ir::Expression;
910
911    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
912        match self {
913            ast::CollateralBlockField::From(x) => x.lower(ctx),
914            ast::CollateralBlockField::MinAmount(x) => x.lower(ctx),
915            ast::CollateralBlockField::Ref(x) => x.lower(ctx),
916        }
917    }
918}
919
920impl IntoLower for ast::CollateralBlock {
921    type Output = ir::Collateral;
922
923    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
924        let from = self.find("from").map(|x| x.lower(ctx)).transpose()?;
925
926        let min_amount = self.find("min_amount").map(|x| x.lower(ctx)).transpose()?;
927
928        let r#ref = self.find("ref").map(|x| x.lower(ctx)).transpose()?;
929
930        let query = ir::InputQuery {
931            address: from.unwrap_or(ir::Expression::None),
932            min_amount: min_amount.unwrap_or(ir::Expression::None),
933            r#ref: r#ref.unwrap_or(ir::Expression::None),
934            many: false,
935            collateral: true,
936        };
937
938        let param = ir::Param::ExpectInput("collateral".to_string(), query);
939
940        let collateral = ir::Collateral {
941            utxos: param.into(),
942        };
943
944        Ok(collateral)
945    }
946}
947
948impl IntoLower for ast::SignersBlock {
949    type Output = ir::Signers;
950
951    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
952        Ok(ir::Signers {
953            signers: self
954                .signers
955                .iter()
956                .map(|x| x.lower(ctx))
957                .collect::<Result<Vec<_>, _>>()?,
958        })
959    }
960}
961
962impl IntoLower for ast::TxDef {
963    type Output = ir::Tx;
964
965    fn lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
966        // Seed with explicit `reference` blocks first so they dedup against,
967        // and precede, refs that the body derives from ref-backed policies.
968        for reference in self.references.iter() {
969            let r#ref = reference.r#ref.lower(ctx)?;
970            ctx.record_script_ref(r#ref);
971        }
972
973        let inputs = self
974            .inputs
975            .iter()
976            .map(|x| x.lower(ctx))
977            .collect::<Result<Vec<_>, _>>()?;
978        let outputs = self
979            .outputs
980            .iter()
981            .map(|x| x.lower(ctx))
982            .collect::<Result<Vec<_>, _>>()?;
983        let validity = self.validity.as_ref().map(|x| x.lower(ctx)).transpose()?;
984        let mints = self
985            .mints
986            .iter()
987            .map(|x| x.lower(ctx))
988            .collect::<Result<Vec<_>, _>>()?;
989        let burns = self
990            .burns
991            .iter()
992            .map(|x| x.lower(ctx))
993            .collect::<Result<Vec<_>, _>>()?;
994        let adhoc = self
995            .adhoc
996            .iter()
997            .map(|x| x.lower(ctx))
998            .collect::<Result<Vec<_>, _>>()?;
999        let collateral = self
1000            .collateral
1001            .iter()
1002            .map(|x| x.lower(ctx))
1003            .collect::<Result<Vec<_>, _>>()?;
1004        let signers = self.signers.as_ref().map(|x| x.lower(ctx)).transpose()?;
1005        let metadata = self
1006            .metadata
1007            .as_ref()
1008            .map(|x| x.lower(ctx))
1009            .transpose()?
1010            .unwrap_or(vec![]);
1011
1012        let ir = ir::Tx {
1013            references: ctx.drain_script_refs(),
1014            inputs,
1015            outputs,
1016            validity,
1017            mints,
1018            burns,
1019            adhoc,
1020            fees: ir::Param::ExpectFees.into(),
1021            collateral,
1022            signers,
1023            metadata,
1024        };
1025
1026        Ok(ir)
1027    }
1028}
1029
1030pub fn lower_tx(ast: &ast::TxDef) -> Result<ir::Tx, Error> {
1031    let ctx = &Context::default();
1032
1033    let tx = ast.lower(ctx)?;
1034
1035    Ok(tx)
1036}
1037
1038/// Lowers the Tx3 language to the intermediate representation.
1039///
1040/// This function takes an AST and converts it into the intermediate
1041/// representation (IR) of the Tx3 language.
1042///
1043/// # Arguments
1044///
1045/// * `ast` - The AST to lower
1046///
1047/// # Returns
1048///
1049/// * `Result<ir::Program, Error>` - The lowered intermediate representation
1050pub fn lower(ast: &ast::Program, template: &str) -> Result<ir::Tx, Error> {
1051    let tx = ast
1052        .txs
1053        .iter()
1054        .find(|x| x.name.value == template)
1055        .ok_or(Error::InvalidAst("tx not found".to_string()))?;
1056
1057    lower_tx(tx)
1058}
1059
1060#[cfg(test)]
1061mod tests {
1062    use assert_json_diff::assert_json_eq;
1063    use paste::paste;
1064
1065    use super::*;
1066    use crate::parsing::{self};
1067
1068    fn make_snapshot_if_missing(example: &str, name: &str, tx: &ir::Tx) {
1069        let manifest_dir = env!("CARGO_MANIFEST_DIR");
1070
1071        let path = format!("{}/../../examples/{}.{}.tir", manifest_dir, example, name);
1072
1073        if !std::fs::exists(&path).unwrap() {
1074            let ir = serde_json::to_string_pretty(tx).unwrap();
1075            std::fs::write(&path, ir).unwrap();
1076        }
1077    }
1078
1079    /// Lowers every tx in an example, snapshot-checks each, and returns the
1080    /// lowered TIRs keyed by tx name so callers can assert extra invariants.
1081    fn test_lowering_example(example: &str) -> std::collections::BTreeMap<String, ir::Tx> {
1082        let manifest_dir = env!("CARGO_MANIFEST_DIR");
1083        let mut program = parsing::parse_well_known_example(example);
1084
1085        crate::analyzing::analyze(&mut program).ok().unwrap();
1086
1087        let mut lowered = std::collections::BTreeMap::new();
1088
1089        for tx in program.txs.iter() {
1090            let tir = lower(&program, &tx.name.value).unwrap();
1091
1092            make_snapshot_if_missing(example, &tx.name.value, &tir);
1093
1094            let tir_file = format!(
1095                "{}/../../examples/{}.{}.tir",
1096                manifest_dir, example, tx.name.value
1097            );
1098
1099            let expected = std::fs::read_to_string(tir_file).unwrap();
1100            let expected: ir::Tx = serde_json::from_str(&expected).unwrap();
1101
1102            assert_json_eq!(tir, expected);
1103
1104            lowered.insert(tx.name.value.clone(), tir);
1105        }
1106
1107        lowered
1108    }
1109
1110    #[macro_export]
1111    macro_rules! test_lowering {
1112        ($name:ident) => {
1113            paste! {
1114                #[test]
1115                fn [<test_example_ $name>]() {
1116                    test_lowering_example(stringify!($name));
1117                }
1118            }
1119        };
1120        // Variant with extra assertions: the block receives the lowered TIRs
1121        // keyed by tx name (a `BTreeMap<String, ir::Tx>`) bound to `$txs`.
1122        ($name:ident, |$txs:ident| $checks:block) => {
1123            paste! {
1124                #[test]
1125                fn [<test_example_ $name>]() {
1126                    let $txs = test_lowering_example(stringify!($name));
1127                    $checks
1128                }
1129            }
1130        };
1131    }
1132
1133    test_lowering!(lang_tour);
1134
1135    test_lowering!(tuples);
1136
1137    test_lowering!(transfer);
1138
1139    test_lowering!(swap);
1140
1141    test_lowering!(asteria);
1142
1143    test_lowering!(vesting);
1144
1145    test_lowering!(faucet);
1146
1147    test_lowering!(input_datum);
1148
1149    test_lowering!(env_vars);
1150
1151    test_lowering!(local_vars);
1152
1153    test_lowering!(cardano_witness);
1154
1155    test_lowering!(reference_script);
1156
1157    test_lowering!(policy_reference_script, |txs| {
1158        // ref-backed policy as `from`
1159        assert_eq!(txs["spend"].references.len(), 1);
1160        // same ref across two inputs is deduped
1161        assert_eq!(txs["spend_two"].references.len(), 1);
1162        // hash-only policy: nothing to reference
1163        assert!(txs["spend_hash_only"].references.is_empty());
1164        // ref-backed mint
1165        assert_eq!(txs["mint_token"].references.len(), 1);
1166        // ref-backed burn
1167        assert_eq!(txs["burn_token"].references.len(), 1);
1168        // hash-only mint: nothing to reference
1169        assert!(txs["mint_hash_only"].references.is_empty());
1170        // output recipient only: script does not run, no reference input
1171        assert!(txs["send_to_policy"].references.is_empty());
1172        // ref-backed withdrawal stake credential
1173        assert_eq!(txs["withdraw"].references.len(), 1);
1174    });
1175
1176    test_lowering!(withdrawal);
1177
1178    test_lowering!(map);
1179
1180    test_lowering!(burn);
1181
1182    test_lowering!(min_utxo);
1183
1184    test_lowering!(tip_slot);
1185
1186    test_lowering!(posix_time);
1187
1188    test_lowering!(donation);
1189
1190    test_lowering!(list_concat);
1191
1192    test_lowering!(buidler_fest_2026);
1193
1194    test_lowering!(functions);
1195
1196    test_lowering!(nested_functions);
1197
1198    test_lowering!(param_field_shadow);
1199
1200    test_lowering!(oracle_reference_datum);
1201
1202    test_lowering!(reference_datum);
1203}