1use std::collections::HashSet;
7use std::ops::Deref;
8
9use crate::ast;
10use crate::ir;
11use crate::UtxoRef;
12
13#[derive(Debug, thiserror::Error)]
14pub enum Error {
15 #[error("missing analyze phase")]
16 MissingAnalyzePhase,
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("failed to decode hex string: {0}")]
28 DecodeHexError(#[from] hex::FromHexError),
29}
30
31fn expect_type_def(ident: &ast::Identifier) -> Result<&ast::TypeDef, Error> {
32 let symbol = ident.symbol.as_ref().ok_or(Error::MissingAnalyzePhase)?;
33
34 symbol
35 .as_type_def()
36 .ok_or(Error::InvalidSymbol(ident.value.clone(), "TypeDef"))
37}
38
39fn expect_case_def(ident: &ast::Identifier) -> Result<&ast::VariantCase, Error> {
40 let symbol = ident.symbol.as_ref().ok_or(Error::MissingAnalyzePhase)?;
41
42 symbol
43 .as_variant_case()
44 .ok_or(Error::InvalidSymbol(ident.value.clone(), "VariantCase"))
45}
46
47#[allow(dead_code)]
48fn expect_field_def(ident: &ast::Identifier) -> Result<&ast::RecordField, Error> {
49 let symbol = ident.symbol.as_ref().ok_or(Error::MissingAnalyzePhase)?;
50
51 symbol
52 .as_field_def()
53 .ok_or(Error::InvalidSymbol(ident.value.clone(), "FieldDef"))
54}
55
56fn coerce_identifier_into_asset_def(identifier: &ast::Identifier) -> Result<ast::AssetDef, Error> {
57 match identifier.try_symbol()? {
58 ast::Symbol::AssetDef(x) => Ok(x.as_ref().clone()),
59 _ => Err(Error::InvalidSymbol(identifier.value.clone(), "AssetDef")),
60 }
61}
62
63fn coerce_identifier_into_asset_expr(
64 identifier: &ast::Identifier,
65) -> Result<ir::Expression, Error> {
66 match identifier.try_symbol()? {
67 ast::Symbol::Input(x, _) => Ok(ir::Expression::EvalInputAssets(x.clone())),
68 ast::Symbol::Fees => Ok(ir::Expression::FeeQuery),
69 ast::Symbol::ParamVar(name, ty) => match ty.deref() {
70 ast::Type::AnyAsset => Ok(ir::Expression::EvalParameter(
71 name.to_lowercase().clone(),
72 ir::Type::AnyAsset,
73 )),
74 _ => Err(Error::InvalidSymbolType(
75 identifier.value.clone(),
76 "AnyAsset",
77 )),
78 },
79 _ => Err(Error::InvalidSymbol(identifier.value.clone(), "AssetExpr")),
80 }
81}
82
83fn lower_into_address_expr(identifier: &ast::Identifier) -> Result<ir::Expression, Error> {
84 match identifier.try_symbol()? {
85 ast::Symbol::PolicyDef(x) => Ok(x.into_lower()?.hash),
86 ast::Symbol::PartyDef(x) => Ok(ir::Expression::EvalParameter(
87 x.name.to_lowercase().clone(),
88 ir::Type::Address,
89 )),
90 _ => Err(Error::InvalidSymbol(
91 identifier.value.clone(),
92 "AddressExpr",
93 )),
94 }
95}
96
97pub(crate) trait IntoLower {
98 type Output;
99
100 fn into_lower(&self) -> Result<Self::Output, Error>;
101}
102
103impl<T> IntoLower for Option<&T>
104where
105 T: IntoLower,
106{
107 type Output = Option<T::Output>;
108
109 fn into_lower(&self) -> Result<Self::Output, Error> {
110 self.map(|x| x.into_lower()).transpose()
111 }
112}
113
114impl<T> IntoLower for Box<T>
115where
116 T: IntoLower,
117{
118 type Output = T::Output;
119
120 fn into_lower(&self) -> Result<Self::Output, Error> {
121 self.as_ref().into_lower()
122 }
123}
124
125impl IntoLower for ast::Identifier {
126 type Output = ir::Expression;
127
128 fn into_lower(&self) -> Result<Self::Output, Error> {
129 let symbol = self.symbol.as_ref().expect("analyze phase must be run");
130
131 match symbol {
132 ast::Symbol::ParamVar(n, ty) => Ok(ir::Expression::EvalParameter(
133 n.to_lowercase().clone(),
134 ty.into_lower()?,
135 )),
136 ast::Symbol::PartyDef(x) => Ok(ir::Expression::EvalParameter(
137 x.name.to_lowercase().clone(),
138 ir::Type::Address,
139 )),
140 ast::Symbol::Input(n, _) => Ok(ir::Expression::EvalInputDatum(n.clone())),
141 _ => {
142 dbg!(&self);
143 todo!();
144 }
145 }
146 }
147}
148
149impl IntoLower for ast::UtxoRef {
150 type Output = ir::Expression;
151
152 fn into_lower(&self) -> Result<Self::Output, Error> {
153 let x = ir::Expression::UtxoRefs(vec![UtxoRef {
154 txid: self.txid.clone(),
155 index: self.index as u32,
156 }]);
157 Ok(x)
158 }
159}
160
161impl IntoLower for ast::StructConstructor {
162 type Output = ir::StructExpr;
163
164 fn into_lower(&self) -> Result<Self::Output, Error> {
165 let type_def = expect_type_def(&self.r#type)?;
166
167 let constructor = type_def
168 .find_case_index(&self.case.name.value)
169 .ok_or(Error::InvalidAst("case not found".to_string()))?;
170
171 let case_def = expect_case_def(&self.case.name)?;
172
173 let mut fields = vec![];
174
175 for field_def in case_def.fields.iter() {
176 let value = self.case.find_field_value(&field_def.name);
177
178 if let Some(value) = value {
179 fields.push(value.into_lower()?);
180 } else {
181 let spread_target = self
182 .case
183 .spread
184 .as_ref()
185 .expect("spread must be set for missing explicit field")
186 .into_lower()?;
187
188 fields.push(ir::Expression::EvalProperty(Box::new(ir::PropertyAccess {
189 object: Box::new(spread_target),
190 field: field_def.name.clone(),
191 })));
192 }
193 }
194
195 Ok(ir::StructExpr {
196 constructor,
197 fields,
198 })
199 }
200}
201
202impl IntoLower for ast::PolicyField {
203 type Output = ir::Expression;
204
205 fn into_lower(&self) -> Result<Self::Output, Error> {
206 match self {
207 ast::PolicyField::Hash(x) => x.into_lower(),
208 ast::PolicyField::Script(x) => x.into_lower(),
209 ast::PolicyField::Ref(x) => x.into_lower(),
210 }
211 }
212}
213
214impl IntoLower for ast::PolicyDef {
215 type Output = ir::PolicyExpr;
216
217 fn into_lower(&self) -> Result<Self::Output, Error> {
218 match &self.value {
219 ast::PolicyValue::Assign(x) => Ok(ir::PolicyExpr {
220 name: self.name.clone(),
221 hash: ir::Expression::Hash(hex::decode(&x.value)?),
222 script: None,
223 }),
224 ast::PolicyValue::Constructor(x) => {
225 let hash = x
226 .find_field("hash")
227 .ok_or(Error::InvalidAst("Missing policy hash".to_string()))?
228 .into_lower()?;
229
230 let ref_field = x.find_field("ref");
231 let script_field = x.find_field("script");
232
233 let script = match (ref_field, script_field) {
234 (Some(x), None) => Some(ir::ScriptSource::UtxoRef {
235 r#ref: x.into_lower()?,
236 source: None,
237 }),
238 (None, Some(x)) => Some(ir::ScriptSource::Embedded(x.into_lower()?)),
239 (Some(r#ref), Some(source)) => Some(ir::ScriptSource::UtxoRef {
240 r#ref: r#ref.into_lower()?,
241 source: Some(source.into_lower()?),
242 }),
243 (None, None) => None,
244 };
245
246 Ok(ir::PolicyExpr {
247 name: self.name.clone(),
248 hash,
249 script,
250 })
251 }
252 }
253 }
254}
255
256impl IntoLower for ast::Type {
257 type Output = ir::Type;
258
259 fn into_lower(&self) -> Result<Self::Output, Error> {
260 match self {
261 ast::Type::Undefined => Ok(ir::Type::Undefined),
262 ast::Type::Unit => Ok(ir::Type::Unit),
263 ast::Type::Int => Ok(ir::Type::Int),
264 ast::Type::Bool => Ok(ir::Type::Bool),
265 ast::Type::Bytes => Ok(ir::Type::Bytes),
266 ast::Type::Address => Ok(ir::Type::Address),
267 ast::Type::UtxoRef => Ok(ir::Type::UtxoRef),
268 ast::Type::AnyAsset => Ok(ir::Type::AnyAsset),
269 ast::Type::List(_) => Ok(ir::Type::List),
270 ast::Type::Custom(x) => Ok(ir::Type::Custom(x.value.clone())),
271 }
272 }
273}
274
275impl IntoLower for ast::DataBinaryOp {
276 type Output = ir::BinaryOp;
277
278 fn into_lower(&self) -> Result<Self::Output, Error> {
279 let left = self.left.into_lower()?;
280 let right = self.right.into_lower()?;
281
282 Ok(ir::BinaryOp {
283 left,
284 right,
285 op: match self.operator {
286 ast::BinaryOperator::Add => ir::BinaryOpKind::Add,
287 ast::BinaryOperator::Subtract => ir::BinaryOpKind::Sub,
288 },
289 })
290 }
291}
292
293impl IntoLower for ast::PropertyAccess {
294 type Output = ir::Expression;
295
296 fn into_lower(&self) -> Result<Self::Output, Error> {
297 let mut object = self.object.into_lower()?;
298
299 for field in self.path.iter() {
300 object = ir::Expression::EvalProperty(Box::new(ir::PropertyAccess {
301 object: Box::new(object),
302 field: field.value.clone(),
303 }));
304 }
305
306 Ok(object)
307 }
308}
309
310impl IntoLower for ast::ListConstructor {
311 type Output = Vec<ir::Expression>;
312
313 fn into_lower(&self) -> Result<Self::Output, Error> {
314 let elements = self
315 .elements
316 .iter()
317 .map(|x| x.into_lower())
318 .collect::<Result<Vec<_>, _>>()?;
319
320 Ok(elements)
321 }
322}
323
324impl IntoLower for ast::DataExpr {
325 type Output = ir::Expression;
326
327 fn into_lower(&self) -> Result<Self::Output, Error> {
328 let out = match self {
329 ast::DataExpr::None => ir::Expression::None,
330 ast::DataExpr::Number(x) => Self::Output::Number(*x as i128),
331 ast::DataExpr::Bool(x) => ir::Expression::Bool(*x),
332 ast::DataExpr::String(x) => ir::Expression::Bytes(x.value.as_bytes().to_vec()),
333 ast::DataExpr::HexString(x) => ir::Expression::Bytes(hex::decode(&x.value)?),
334 ast::DataExpr::StructConstructor(x) => ir::Expression::Struct(x.into_lower()?),
335 ast::DataExpr::ListConstructor(x) => ir::Expression::List(x.into_lower()?),
336 ast::DataExpr::Unit => ir::Expression::Struct(ir::StructExpr::unit()),
337 ast::DataExpr::Identifier(x) => x.into_lower()?,
338 ast::DataExpr::BinaryOp(x) => ir::Expression::EvalCustom(Box::new(x.into_lower()?)),
339 ast::DataExpr::PropertyAccess(x) => x.into_lower()?,
340 ast::DataExpr::UtxoRef(x) => x.into_lower()?,
341 };
342
343 Ok(out)
344 }
345}
346
347impl IntoLower for ast::StaticAssetConstructor {
348 type Output = ir::Expression;
349
350 fn into_lower(&self) -> Result<Self::Output, Error> {
351 let asset_def = coerce_identifier_into_asset_def(&self.r#type)?;
352
353 let policy = match asset_def.policy {
354 Some(x) => ir::Expression::Bytes(hex::decode(&x.value)?),
355 None => ir::Expression::None,
356 };
357
358 let asset_name = match asset_def.asset_name {
359 Some(x) => ir::Expression::Bytes(x.as_bytes().to_vec()),
360 None => ir::Expression::None,
361 };
362
363 let amount = self.amount.into_lower()?;
364
365 Ok(ir::Expression::Assets(vec![ir::AssetExpr {
366 policy,
367 asset_name,
368 amount,
369 }]))
370 }
371}
372
373impl IntoLower for ast::AnyAssetConstructor {
374 type Output = ir::Expression;
375
376 fn into_lower(&self) -> Result<Self::Output, Error> {
377 let policy = self.policy.into_lower()?;
378 let asset_name = self.asset_name.into_lower()?;
379 let amount = self.amount.into_lower()?;
380
381 Ok(ir::Expression::Assets(vec![ir::AssetExpr {
382 policy,
383 asset_name,
384 amount,
385 }]))
386 }
387}
388
389impl IntoLower for ast::AssetBinaryOp {
390 type Output = ir::BinaryOp;
391
392 fn into_lower(&self) -> Result<Self::Output, Error> {
393 let left = self.left.into_lower()?;
394 let right = self.right.into_lower()?;
395
396 Ok(ir::BinaryOp {
397 left,
398 right,
399 op: match self.operator {
400 ast::BinaryOperator::Add => ir::BinaryOpKind::Add,
401 ast::BinaryOperator::Subtract => ir::BinaryOpKind::Sub,
402 },
403 })
404 }
405}
406
407impl IntoLower for ast::AssetExpr {
408 type Output = ir::Expression;
409
410 fn into_lower(&self) -> Result<Self::Output, Error> {
411 match self {
412 ast::AssetExpr::StaticConstructor(x) => x.into_lower(),
413 ast::AssetExpr::AnyConstructor(x) => x.into_lower(),
414 ast::AssetExpr::BinaryOp(x) => {
415 Ok(ir::Expression::EvalCustom(Box::new(x.into_lower()?)))
416 }
417 ast::AssetExpr::Identifier(x) => coerce_identifier_into_asset_expr(x),
418 ast::AssetExpr::PropertyAccess(_x) => todo!(),
419 }
420 }
421}
422
423impl IntoLower for ast::AddressExpr {
424 type Output = ir::Expression;
425
426 fn into_lower(&self) -> Result<Self::Output, Error> {
427 match self {
428 ast::AddressExpr::String(x) => Ok(ir::Expression::String(x.value.clone())),
429 ast::AddressExpr::HexString(x) => Ok(ir::Expression::Bytes(hex::decode(&x.value)?)),
430 ast::AddressExpr::Identifier(x) => lower_into_address_expr(x),
431 }
432 }
433}
434
435impl IntoLower for ast::InputBlockField {
436 type Output = ir::Expression;
437
438 fn into_lower(&self) -> Result<Self::Output, Error> {
439 match self {
440 ast::InputBlockField::From(x) => x.into_lower(),
441 ast::InputBlockField::DatumIs(_) => todo!(),
442 ast::InputBlockField::MinAmount(x) => x.into_lower(),
443 ast::InputBlockField::Redeemer(x) => x.into_lower(),
444 ast::InputBlockField::Ref(x) => x.into_lower(),
445 }
446 }
447}
448
449impl IntoLower for ast::InputBlock {
450 type Output = ir::Input;
451
452 fn into_lower(&self) -> Result<Self::Output, Error> {
453 let from = self.find("from");
454 let min_amount = self.find("min_amount");
455 let r#ref = self.find("ref");
456 let redeemer = self.find("redeemer");
457
458 let policy = from
459 .and_then(ast::InputBlockField::as_address_expr)
460 .and_then(ast::AddressExpr::as_identifier)
461 .and_then(|x| x.symbol.as_ref())
462 .and_then(|x| x.as_policy_def())
463 .map(|x| x.into_lower())
464 .transpose()?;
465
466 let input = ir::Input {
467 name: self.name.to_lowercase().clone(),
468 query: ir::InputQuery {
469 address: from.into_lower()?,
470 min_amount: min_amount.into_lower()?,
471 r#ref: r#ref.into_lower()?,
472 }
473 .into(),
474 refs: HashSet::new(),
475 redeemer: redeemer.into_lower()?,
476 policy,
477 };
478
479 Ok(input)
480 }
481}
482
483impl IntoLower for ast::OutputBlockField {
484 type Output = ir::Expression;
485
486 fn into_lower(&self) -> Result<Self::Output, Error> {
487 match self {
488 ast::OutputBlockField::To(x) => x.into_lower(),
489 ast::OutputBlockField::Amount(x) => x.into_lower(),
490 ast::OutputBlockField::Datum(x) => x.into_lower(),
491 }
492 }
493}
494
495impl IntoLower for ast::OutputBlock {
496 type Output = ir::Output;
497
498 fn into_lower(&self) -> Result<Self::Output, Error> {
499 Ok(ir::Output {
500 address: self.find("to").into_lower()?,
501 datum: self.find("datum").into_lower()?,
502 amount: self.find("amount").into_lower()?,
503 })
504 }
505}
506
507impl IntoLower for ast::MintBlockField {
508 type Output = ir::Expression;
509
510 fn into_lower(&self) -> Result<Self::Output, Error> {
511 match self {
512 ast::MintBlockField::Amount(x) => x.into_lower(),
513 ast::MintBlockField::Redeemer(x) => x.into_lower(),
514 }
515 }
516}
517
518impl IntoLower for ast::MintBlock {
519 type Output = ir::Mint;
520
521 fn into_lower(&self) -> Result<Self::Output, Error> {
522 Ok(ir::Mint {
523 amount: self.find("amount").into_lower()?,
524 redeemer: self.find("redeemer").into_lower()?,
525 })
526 }
527}
528
529impl IntoLower for ast::ChainSpecificBlock {
530 type Output = ir::AdHocDirective;
531
532 fn into_lower(&self) -> Result<Self::Output, Error> {
533 match self {
534 ast::ChainSpecificBlock::Cardano(x) => x.into_lower(),
535 }
536 }
537}
538
539impl IntoLower for ast::ReferenceBlock {
540 type Output = ir::Expression;
541
542 fn into_lower(&self) -> Result<Self::Output, Error> {
543 self.r#ref.into_lower()
544 }
545}
546
547impl IntoLower for ast::CollateralBlockField {
548 type Output = ir::Expression;
549
550 fn into_lower(&self) -> Result<Self::Output, Error> {
551 match self {
552 ast::CollateralBlockField::From(x) => x.into_lower(),
553 ast::CollateralBlockField::MinAmount(x) => x.into_lower(),
554 ast::CollateralBlockField::Ref(x) => x.into_lower(),
555 }
556 }
557}
558
559impl IntoLower for ast::CollateralBlock {
560 type Output = ir::Collateral;
561
562 fn into_lower(&self) -> Result<Self::Output, Error> {
563 let from = self.find("from");
564 let min_amount = self.find("min_amount");
565 let r#ref = self.find("ref");
566
567 let collateral = ir::Collateral {
568 query: ir::InputQuery {
569 address: from.into_lower()?,
570 min_amount: min_amount.into_lower()?,
571 r#ref: r#ref.into_lower()?,
572 }
573 .into(),
574 };
575
576 Ok(collateral)
577 }
578}
579
580pub fn lower_tx(ast: &ast::TxDef) -> Result<ir::Tx, Error> {
581 let ir = ir::Tx {
582 references: ast
583 .references
584 .iter()
585 .map(|x| x.into_lower())
586 .collect::<Result<Vec<_>, _>>()?,
587 inputs: ast
588 .inputs
589 .iter()
590 .map(|x| x.into_lower())
591 .collect::<Result<Vec<_>, _>>()?,
592 outputs: ast
593 .outputs
594 .iter()
595 .map(|x| x.into_lower())
596 .collect::<Result<Vec<_>, _>>()?,
597 mint: ast.mint.as_ref().map(|x| x.into_lower()).transpose()?,
598 adhoc: ast
599 .adhoc
600 .iter()
601 .map(|x| x.into_lower())
602 .collect::<Result<Vec<_>, _>>()?,
603 fees: ir::Expression::FeeQuery,
604 collateral: ast
605 .collateral
606 .iter()
607 .map(|x| x.into_lower())
608 .collect::<Result<Vec<_>, _>>()?,
609 };
610
611 Ok(ir)
612}
613
614pub fn lower(ast: &ast::Program, template: &str) -> Result<ir::Tx, Error> {
627 let tx = ast
628 .txs
629 .iter()
630 .find(|x| x.name == template)
631 .ok_or(Error::InvalidAst("tx not found".to_string()))?;
632
633 lower_tx(tx)
634}
635
636#[cfg(test)]
637mod tests {
638 use assert_json_diff::assert_json_eq;
639 use paste::paste;
640
641 use super::*;
642 use crate::parsing::{self};
643
644 fn make_snapshot_if_missing(example: &str, name: &str, tx: &ir::Tx) {
645 let manifest_dir = env!("CARGO_MANIFEST_DIR");
646
647 let path = format!("{}/../../examples/{}.{}.tir", manifest_dir, example, name);
648
649 if !std::fs::exists(&path).unwrap() {
650 let ir = serde_json::to_string_pretty(tx).unwrap();
651 std::fs::write(&path, ir).unwrap();
652 }
653 }
654
655 fn test_lowering_example(example: &str) {
656 let manifest_dir = env!("CARGO_MANIFEST_DIR");
657 let mut program = parsing::parse_well_known_example(example);
658
659 crate::analyzing::analyze(&mut program).ok().unwrap();
660
661 for tx in program.txs.iter() {
662 let tir = lower(&program, &tx.name).unwrap();
663
664 make_snapshot_if_missing(example, &tx.name, &tir);
665
666 let tir_file = format!(
667 "{}/../../examples/{}.{}.tir",
668 manifest_dir, example, tx.name
669 );
670
671 let expected = std::fs::read_to_string(tir_file).unwrap();
672 let expected: ir::Tx = serde_json::from_str(&expected).unwrap();
673
674 assert_json_eq!(tir, expected);
675 }
676 }
677
678 #[macro_export]
679 macro_rules! test_lowering {
680 ($name:ident) => {
681 paste! {
682 #[test]
683 fn [<test_example_ $name>]() {
684 test_lowering_example(stringify!($name));
685 }
686 }
687 };
688 }
689
690 test_lowering!(lang_tour);
691
692 test_lowering!(transfer);
693
694 test_lowering!(swap);
695
696 test_lowering!(asteria);
697
698 test_lowering!(vesting);
699
700 test_lowering!(faucet);
701}