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