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 let function_name = &self.callee.value;
415
416 match function_name.as_str() {
417 "min_utxo" => {
418 if self.args.len() != 1 {
419 return Err(Error::InvalidAst(format!(
420 "min_utxo expects 1 argument, got {}",
421 self.args.len()
422 )));
423 }
424 let arg = self.args[0].into_lower(ctx)?;
425 Ok(ir::Expression::EvalCompiler(Box::new(
426 ir::CompilerOp::ComputeMinUtxo(arg),
427 )))
428 }
429 "tip_slot" => {
430 if !self.args.is_empty() {
431 return Err(Error::InvalidAst(format!(
432 "tip_slot expects 0 arguments, got {}",
433 self.args.len()
434 )));
435 }
436 Ok(ir::Expression::EvalCompiler(Box::new(
437 ir::CompilerOp::ComputeTipSlot,
438 )))
439 }
440 "slot_to_time" => {
441 if self.args.len() != 1 {
442 return Err(Error::InvalidAst(format!(
443 "slot_to_time expects 1 argument, got {}",
444 self.args.len()
445 )));
446 }
447 let arg = self.args[0].into_lower(ctx)?;
448 Ok(ir::Expression::EvalCompiler(Box::new(
449 ir::CompilerOp::ComputeSlotToTime(arg),
450 )))
451 }
452 "time_to_slot" => {
453 if self.args.len() != 1 {
454 return Err(Error::InvalidAst(format!(
455 "time_to_slot expects 1 argument, got {}",
456 self.args.len()
457 )));
458 }
459
460 let arg = self.args[0].into_lower(ctx)?;
461
462 Ok(ir::Expression::EvalCompiler(Box::new(
463 ir::CompilerOp::ComputeTimeToSlot(arg),
464 )))
465 }
466 _ => {
467 match coerce_identifier_into_asset_def(&self.callee) {
470 Ok(asset_def) => {
471 let policy = asset_def.policy.into_lower(ctx)?;
472 let asset_name = asset_def.asset_name.into_lower(ctx)?;
473 let amount = self.args[0].into_lower(ctx)?;
474
475 Ok(ir::Expression::Assets(vec![ir::AssetExpr {
476 policy,
477 asset_name,
478 amount,
479 }]))
480 }
481 Err(_) => Err(Error::InvalidAst(format!(
482 "unknown function: {}",
483 function_name
484 ))),
485 }
486 }
487 }
488 }
489}
490
491impl IntoLower for ast::PropertyOp {
492 type Output = ir::Expression;
493
494 fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
495 let object = self.operand.into_lower(ctx)?;
496
497 let ty = self
498 .operand
499 .target_type()
500 .ok_or(Error::MissingAnalyzePhase(format!("{0:?}", self.operand)))?;
501
502 let prop_index =
503 ty.property_index(*self.property.clone())
504 .ok_or(Error::InvalidProperty(
505 format!("{:?}", self.property),
506 ty.to_string(),
507 ))?;
508
509 Ok(ir::Expression::EvalBuiltIn(Box::new(
510 ir::BuiltInOp::Property(object, prop_index.into_lower(ctx)?),
511 )))
512 }
513}
514
515impl IntoLower for ast::ListConstructor {
516 type Output = Vec<ir::Expression>;
517
518 fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
519 let elements = self
520 .elements
521 .iter()
522 .map(|x| x.into_lower(ctx))
523 .collect::<Result<Vec<_>, _>>()?;
524
525 Ok(elements)
526 }
527}
528
529impl IntoLower for ast::MapConstructor {
530 type Output = ir::Expression;
531
532 fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
533 let pairs = self
534 .fields
535 .iter()
536 .map(|field| {
537 let key = field.key.into_lower(ctx)?;
538 let value = field.value.into_lower(ctx)?;
539 Ok((key, value))
540 })
541 .collect::<Result<Vec<_>, _>>()?;
542
543 Ok(ir::Expression::Map(pairs))
544 }
545}
546
547impl IntoLower for ast::DataExpr {
548 type Output = ir::Expression;
549
550 fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
551 let out = match self {
552 ast::DataExpr::None => ir::Expression::None,
553 ast::DataExpr::Number(x) => Self::Output::Number(*x as i128),
554 ast::DataExpr::Bool(x) => ir::Expression::Bool(*x),
555 ast::DataExpr::String(x) => ir::Expression::String(x.value.clone()),
556 ast::DataExpr::HexString(x) => ir::Expression::Bytes(hex_decode(&x.value)?),
557 ast::DataExpr::StructConstructor(x) => ir::Expression::Struct(x.into_lower(ctx)?),
558 ast::DataExpr::ListConstructor(x) => ir::Expression::List(x.into_lower(ctx)?),
559 ast::DataExpr::MapConstructor(x) => x.into_lower(ctx)?,
560 ast::DataExpr::AnyAssetConstructor(x) => x.into_lower(ctx)?,
561 ast::DataExpr::Unit => ir::Expression::Struct(ir::StructExpr::unit()),
562 ast::DataExpr::Identifier(x) => x.into_lower(ctx)?,
563 ast::DataExpr::AddOp(x) => x.into_lower(ctx)?,
564 ast::DataExpr::SubOp(x) => x.into_lower(ctx)?,
565 ast::DataExpr::ConcatOp(x) => x.into_lower(ctx)?,
566 ast::DataExpr::NegateOp(x) => x.into_lower(ctx)?,
567 ast::DataExpr::PropertyOp(x) => x.into_lower(ctx)?,
568 ast::DataExpr::UtxoRef(x) => x.into_lower(ctx)?,
569 ast::DataExpr::FnCall(x) => x.into_lower(ctx)?,
570 ast::DataExpr::MinUtxo(x) => ir::Expression::EvalCompiler(Box::new(
571 ir::CompilerOp::ComputeMinUtxo(x.into_lower(ctx)?),
572 )),
573 ast::DataExpr::ComputeTipSlot => {
574 ir::Expression::EvalCompiler(Box::new(ir::CompilerOp::ComputeTipSlot))
575 }
576 ast::DataExpr::SlotToTime(x) => ir::Expression::EvalCompiler(Box::new(
577 ir::CompilerOp::ComputeSlotToTime(x.into_lower(ctx)?),
578 )),
579 ast::DataExpr::TimeToSlot(x) => ir::Expression::EvalCompiler(Box::new(
580 ir::CompilerOp::ComputeTimeToSlot(x.into_lower(ctx)?),
581 )),
582 };
583
584 Ok(out)
585 }
586}
587
588impl IntoLower for ast::AnyAssetConstructor {
589 type Output = ir::Expression;
590
591 fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
592 let ctx = &ctx.enter_datum_expr();
593 let policy = self.policy.into_lower(ctx)?;
594
595 let ctx = &ctx.enter_datum_expr();
596 let asset_name = self.asset_name.into_lower(ctx)?;
597
598 let ctx = &ctx.enter_datum_expr();
599 let amount = self.amount.into_lower(ctx)?;
600
601 Ok(ir::Expression::Assets(vec![ir::AssetExpr {
602 policy,
603 asset_name,
604 amount,
605 }]))
606 }
607}
608
609impl IntoLower for ast::InputBlockField {
610 type Output = ir::Expression;
611
612 fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
613 match self {
614 ast::InputBlockField::From(x) => {
615 let ctx = ctx.enter_address_expr();
616 x.into_lower(&ctx)
617 }
618 ast::InputBlockField::DatumIs(_) => todo!(),
619 ast::InputBlockField::MinAmount(x) => {
620 let ctx = ctx.enter_asset_expr();
621 x.into_lower(&ctx)
622 }
623 ast::InputBlockField::Redeemer(x) => {
624 let ctx = ctx.enter_datum_expr();
625 x.into_lower(&ctx)
626 }
627 ast::InputBlockField::Ref(x) => x.into_lower(ctx),
628 }
629 }
630}
631
632impl IntoLower for ast::InputBlock {
633 type Output = ir::Input;
634
635 fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
636 let from_field = self.find("from");
637
638 let address = from_field.map(|x| x.into_lower(ctx)).transpose()?;
639
640 let min_amount = self
641 .find("min_amount")
642 .map(|x| x.into_lower(ctx))
643 .transpose()?;
644
645 let r#ref = self.find("ref").map(|x| x.into_lower(ctx)).transpose()?;
646
647 let redeemer = self
648 .find("redeemer")
649 .map(|x| x.into_lower(ctx))
650 .transpose()?
651 .unwrap_or(ir::Expression::None);
652
653 let query = ir::InputQuery {
654 address: address.unwrap_or(ir::Expression::None),
655 min_amount: min_amount.unwrap_or(ir::Expression::None),
656 r#ref: r#ref.unwrap_or(ir::Expression::None),
657 many: self.many,
658 collateral: false,
659 };
660
661 let param = ir::Param::ExpectInput(self.name.to_lowercase().clone(), query);
662
663 let input = ir::Input {
664 name: self.name.to_lowercase().clone(),
665 utxos: param.into(),
666 redeemer,
667 };
668
669 Ok(input)
670 }
671}
672
673impl IntoLower for ast::OutputBlockField {
674 type Output = ir::Expression;
675
676 fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
677 match self {
678 ast::OutputBlockField::To(x) => {
679 let ctx = ctx.enter_address_expr();
680 x.into_lower(&ctx)
681 }
682 ast::OutputBlockField::Amount(x) => {
683 let ctx = ctx.enter_asset_expr();
684 x.into_lower(&ctx)
685 }
686 ast::OutputBlockField::Datum(x) => {
687 let ctx = ctx.enter_datum_expr();
688 x.into_lower(&ctx)
689 }
690 }
691 }
692}
693
694impl IntoLower for ast::OutputBlock {
695 type Output = ir::Output;
696
697 fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
698 let address = self.find("to").into_lower(ctx)?.unwrap_or_default();
699 let datum = self.find("datum").into_lower(ctx)?.unwrap_or_default();
700 let amount = self.find("amount").into_lower(ctx)?.unwrap_or_default();
701
702 Ok(ir::Output {
703 address,
704 datum,
705 amount,
706 optional: self.optional,
707 })
708 }
709}
710
711impl IntoLower for ast::ValidityBlockField {
712 type Output = ir::Expression;
713
714 fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
715 match self {
716 ast::ValidityBlockField::SinceSlot(x) => x.into_lower(ctx),
717 ast::ValidityBlockField::UntilSlot(x) => x.into_lower(ctx),
718 }
719 }
720}
721
722impl IntoLower for ast::ValidityBlock {
723 type Output = ir::Validity;
724
725 fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
726 let since = self.find("since_slot").into_lower(ctx)?.unwrap_or_default();
727 let until = self.find("until_slot").into_lower(ctx)?.unwrap_or_default();
728
729 Ok(ir::Validity { since, until })
730 }
731}
732
733impl IntoLower for ast::MintBlockField {
734 type Output = ir::Expression;
735
736 fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
737 match self {
738 ast::MintBlockField::Amount(x) => x.into_lower(ctx),
739 ast::MintBlockField::Redeemer(x) => x.into_lower(ctx),
740 }
741 }
742}
743
744impl IntoLower for ast::MintBlock {
745 type Output = ir::Mint;
746
747 fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
748 let amount = self.find("amount").into_lower(ctx)?.unwrap_or_default();
749 let redeemer = self.find("redeemer").into_lower(ctx)?.unwrap_or_default();
750
751 Ok(ir::Mint { amount, redeemer })
752 }
753}
754
755impl IntoLower for ast::MetadataBlockField {
756 type Output = ir::Metadata;
757 fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
758 Ok(ir::Metadata {
759 key: self.key.into_lower(ctx)?,
760 value: self.value.into_lower(ctx)?,
761 })
762 }
763}
764
765impl IntoLower for ast::MetadataBlock {
766 type Output = Vec<ir::Metadata>;
767
768 fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
769 let fields = self
770 .fields
771 .iter()
772 .map(|metadata_field| metadata_field.into_lower(ctx))
773 .collect::<Result<Vec<_>, _>>()?;
774
775 Ok(fields)
776 }
777}
778
779impl IntoLower for ast::ChainSpecificBlock {
780 type Output = ir::AdHocDirective;
781
782 fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
783 match self {
784 ast::ChainSpecificBlock::Cardano(x) => x.into_lower(ctx),
785 }
786 }
787}
788
789impl IntoLower for ast::ReferenceBlock {
790 type Output = ir::Expression;
791
792 fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
793 let r#ref = self.r#ref.into_lower(ctx)?;
794
795 let query = ir::InputQuery {
796 address: ir::Expression::None,
797 min_amount: ir::Expression::None,
798 r#ref,
799 many: false,
800 collateral: false,
801 };
802
803 let inner = ir::Param::ExpectInput(self.name.to_lowercase(), query).into();
804
805 let out = if ctx.is_asset_expr() {
806 ir::Coerce::IntoAssets(inner).into()
807 } else if ctx.is_datum_expr() {
808 ir::Coerce::IntoDatum(inner).into()
809 } else {
810 inner
811 };
812
813 Ok(out)
814 }
815}
816
817impl IntoLower for ast::CollateralBlockField {
818 type Output = ir::Expression;
819
820 fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
821 match self {
822 ast::CollateralBlockField::From(x) => x.into_lower(ctx),
823 ast::CollateralBlockField::MinAmount(x) => x.into_lower(ctx),
824 ast::CollateralBlockField::Ref(x) => x.into_lower(ctx),
825 }
826 }
827}
828
829impl IntoLower for ast::CollateralBlock {
830 type Output = ir::Collateral;
831
832 fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
833 let from = self.find("from").map(|x| x.into_lower(ctx)).transpose()?;
834
835 let min_amount = self
836 .find("min_amount")
837 .map(|x| x.into_lower(ctx))
838 .transpose()?;
839
840 let r#ref = self.find("ref").map(|x| x.into_lower(ctx)).transpose()?;
841
842 let query = ir::InputQuery {
843 address: from.unwrap_or(ir::Expression::None),
844 min_amount: min_amount.unwrap_or(ir::Expression::None),
845 r#ref: r#ref.unwrap_or(ir::Expression::None),
846 many: false,
847 collateral: true,
848 };
849
850 let param = ir::Param::ExpectInput("collateral".to_string(), query);
851
852 let collateral = ir::Collateral {
853 utxos: param.into(),
854 };
855
856 Ok(collateral)
857 }
858}
859
860impl IntoLower for ast::SignersBlock {
861 type Output = ir::Signers;
862
863 fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
864 Ok(ir::Signers {
865 signers: self
866 .signers
867 .iter()
868 .map(|x| x.into_lower(ctx))
869 .collect::<Result<Vec<_>, _>>()?,
870 })
871 }
872}
873
874impl IntoLower for ast::TxDef {
875 type Output = ir::Tx;
876
877 fn into_lower(&self, ctx: &Context) -> Result<Self::Output, Error> {
878 let ir = ir::Tx {
879 references: self
880 .references
881 .iter()
882 .map(|x| x.r#ref.into_lower(ctx))
883 .collect::<Result<Vec<_>, _>>()?,
884 inputs: self
885 .inputs
886 .iter()
887 .map(|x| x.into_lower(ctx))
888 .collect::<Result<Vec<_>, _>>()?,
889 outputs: self
890 .outputs
891 .iter()
892 .map(|x| x.into_lower(ctx))
893 .collect::<Result<Vec<_>, _>>()?,
894 validity: self
895 .validity
896 .as_ref()
897 .map(|x| x.into_lower(ctx))
898 .transpose()?,
899 mints: self
900 .mints
901 .iter()
902 .map(|x| x.into_lower(ctx))
903 .collect::<Result<Vec<_>, _>>()?,
904 burns: self
905 .burns
906 .iter()
907 .map(|x| x.into_lower(ctx))
908 .collect::<Result<Vec<_>, _>>()?,
909 adhoc: self
910 .adhoc
911 .iter()
912 .map(|x| x.into_lower(ctx))
913 .collect::<Result<Vec<_>, _>>()?,
914 fees: ir::Param::ExpectFees.into(),
915 collateral: self
916 .collateral
917 .iter()
918 .map(|x| x.into_lower(ctx))
919 .collect::<Result<Vec<_>, _>>()?,
920 signers: self
921 .signers
922 .as_ref()
923 .map(|x| x.into_lower(ctx))
924 .transpose()?,
925 metadata: self
926 .metadata
927 .as_ref()
928 .map(|x| x.into_lower(ctx))
929 .transpose()?
930 .unwrap_or(vec![]),
931 };
932
933 Ok(ir)
934 }
935}
936
937pub fn lower_tx(ast: &ast::TxDef) -> Result<ir::Tx, Error> {
938 let ctx = &Context::default();
939
940 let tx = ast.into_lower(ctx)?;
941
942 Ok(tx)
943}
944
945pub fn lower(ast: &ast::Program, template: &str) -> Result<ir::Tx, Error> {
958 let tx = ast
959 .txs
960 .iter()
961 .find(|x| x.name.value == template)
962 .ok_or(Error::InvalidAst("tx not found".to_string()))?;
963
964 lower_tx(tx)
965}
966
967#[cfg(test)]
968mod tests {
969 use assert_json_diff::assert_json_eq;
970 use paste::paste;
971
972 use super::*;
973 use crate::parsing::{self};
974
975 fn make_snapshot_if_missing(example: &str, name: &str, tx: &ir::Tx) {
976 let manifest_dir = env!("CARGO_MANIFEST_DIR");
977
978 let path = format!("{}/../../examples/{}.{}.tir", manifest_dir, example, name);
979
980 if !std::fs::exists(&path).unwrap() {
981 let ir = serde_json::to_string_pretty(tx).unwrap();
982 std::fs::write(&path, ir).unwrap();
983 }
984 }
985
986 fn test_lowering_example(example: &str) {
987 let manifest_dir = env!("CARGO_MANIFEST_DIR");
988 let mut program = parsing::parse_well_known_example(example);
989
990 crate::analyzing::analyze(&mut program).ok().unwrap();
991
992 for tx in program.txs.iter() {
993 let tir = lower(&program, &tx.name.value).unwrap();
994
995 make_snapshot_if_missing(example, &tx.name.value, &tir);
996
997 let tir_file = format!(
998 "{}/../../examples/{}.{}.tir",
999 manifest_dir, example, tx.name.value
1000 );
1001
1002 let expected = std::fs::read_to_string(tir_file).unwrap();
1003 let expected: ir::Tx = serde_json::from_str(&expected).unwrap();
1004
1005 assert_json_eq!(tir, expected);
1006 }
1007 }
1008
1009 #[macro_export]
1010 macro_rules! test_lowering {
1011 ($name:ident) => {
1012 paste! {
1013 #[test]
1014 fn [<test_example_ $name>]() {
1015 test_lowering_example(stringify!($name));
1016 }
1017 }
1018 };
1019 }
1020
1021 test_lowering!(lang_tour);
1022
1023 test_lowering!(transfer);
1024
1025 test_lowering!(swap);
1026
1027 test_lowering!(asteria);
1028
1029 test_lowering!(vesting);
1030
1031 test_lowering!(faucet);
1032
1033 test_lowering!(input_datum);
1034
1035 test_lowering!(env_vars);
1036
1037 test_lowering!(local_vars);
1038
1039 test_lowering!(cardano_witness);
1040
1041 test_lowering!(reference_script);
1042
1043 test_lowering!(withdrawal);
1044
1045 test_lowering!(map);
1046
1047 test_lowering!(burn);
1048
1049 test_lowering!(min_utxo);
1050
1051 test_lowering!(donation);
1052
1053 test_lowering!(list_concat);
1054
1055 test_lowering!(buidler_fest_2026);
1056
1057 test_lowering!(param_field_shadow);
1058
1059 test_lowering!(oracle_reference_datum);
1060}