1use 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#[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 capture_policy_ref: bool,
117 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 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 pub fn record_script_ref(&self, r#ref: ir::Expression) {
179 self.script_refs.borrow_mut().record(r#ref);
180 }
181
182 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 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 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 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 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
555struct 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 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 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 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
1051pub 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 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 ($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 assert_eq!(txs["spend"].references.len(), 1);
1173 assert_eq!(txs["spend_two"].references.len(), 1);
1175 assert!(txs["spend_hash_only"].references.is_empty());
1177 assert_eq!(txs["mint_token"].references.len(), 1);
1179 assert_eq!(txs["burn_token"].references.len(), 1);
1181 assert!(txs["mint_hash_only"].references.is_empty());
1183 assert!(txs["send_to_policy"].references.is_empty());
1185 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}