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