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::DatumConstructor {
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::Custom(x) => Ok(ir::Type::Custom(x.value.clone())),
270 }
271 }
272}
273
274impl IntoLower for ast::DataBinaryOp {
275 type Output = ir::BinaryOp;
276
277 fn into_lower(&self) -> Result<Self::Output, Error> {
278 let left = self.left.into_lower()?;
279 let right = self.right.into_lower()?;
280
281 Ok(ir::BinaryOp {
282 left,
283 right,
284 op: match self.operator {
285 ast::BinaryOperator::Add => ir::BinaryOpKind::Add,
286 ast::BinaryOperator::Subtract => ir::BinaryOpKind::Sub,
287 },
288 })
289 }
290}
291
292impl IntoLower for ast::PropertyAccess {
293 type Output = ir::Expression;
294
295 fn into_lower(&self) -> Result<Self::Output, Error> {
296 let mut object = self.object.into_lower()?;
297
298 for field in self.path.iter() {
299 object = ir::Expression::EvalProperty(Box::new(ir::PropertyAccess {
300 object: Box::new(object),
301 field: field.value.clone(),
302 }));
303 }
304
305 Ok(object)
306 }
307}
308
309impl IntoLower for ast::DataExpr {
310 type Output = ir::Expression;
311
312 fn into_lower(&self) -> Result<Self::Output, Error> {
313 let out = match self {
314 ast::DataExpr::None => ir::Expression::None,
315 ast::DataExpr::Number(x) => Self::Output::Number(*x as i128),
316 ast::DataExpr::Bool(x) => ir::Expression::Bool(*x),
317 ast::DataExpr::String(x) => ir::Expression::Bytes(x.value.as_bytes().to_vec()),
318 ast::DataExpr::HexString(x) => ir::Expression::Bytes(hex::decode(&x.value)?),
319 ast::DataExpr::Constructor(x) => ir::Expression::Struct(x.into_lower()?),
320 ast::DataExpr::Unit => ir::Expression::Struct(ir::StructExpr::unit()),
321 ast::DataExpr::Identifier(x) => x.into_lower()?,
322 ast::DataExpr::BinaryOp(x) => ir::Expression::EvalCustom(Box::new(x.into_lower()?)),
323 ast::DataExpr::PropertyAccess(x) => x.into_lower()?,
324 ast::DataExpr::UtxoRef(x) => x.into_lower()?,
325 };
326
327 Ok(out)
328 }
329}
330
331impl IntoLower for ast::StaticAssetConstructor {
332 type Output = ir::Expression;
333
334 fn into_lower(&self) -> Result<Self::Output, Error> {
335 let asset_def = coerce_identifier_into_asset_def(&self.r#type)?;
336
337 let policy = match asset_def.policy {
338 Some(x) => ir::Expression::Bytes(hex::decode(&x.value)?),
339 None => ir::Expression::None,
340 };
341
342 let asset_name = match asset_def.asset_name {
343 Some(x) => ir::Expression::Bytes(x.as_bytes().to_vec()),
344 None => ir::Expression::None,
345 };
346
347 let amount = self.amount.into_lower()?;
348
349 Ok(ir::Expression::Assets(vec![ir::AssetExpr {
350 policy,
351 asset_name,
352 amount,
353 }]))
354 }
355}
356
357impl IntoLower for ast::AnyAssetConstructor {
358 type Output = ir::Expression;
359
360 fn into_lower(&self) -> Result<Self::Output, Error> {
361 let policy = self.policy.into_lower()?;
362 let asset_name = self.asset_name.into_lower()?;
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::AssetBinaryOp {
374 type Output = ir::BinaryOp;
375
376 fn into_lower(&self) -> Result<Self::Output, Error> {
377 let left = self.left.into_lower()?;
378 let right = self.right.into_lower()?;
379
380 Ok(ir::BinaryOp {
381 left,
382 right,
383 op: match self.operator {
384 ast::BinaryOperator::Add => ir::BinaryOpKind::Add,
385 ast::BinaryOperator::Subtract => ir::BinaryOpKind::Sub,
386 },
387 })
388 }
389}
390
391impl IntoLower for ast::AssetExpr {
392 type Output = ir::Expression;
393
394 fn into_lower(&self) -> Result<Self::Output, Error> {
395 match self {
396 ast::AssetExpr::StaticConstructor(x) => x.into_lower(),
397 ast::AssetExpr::AnyConstructor(x) => x.into_lower(),
398 ast::AssetExpr::BinaryOp(x) => {
399 Ok(ir::Expression::EvalCustom(Box::new(x.into_lower()?)))
400 }
401 ast::AssetExpr::Identifier(x) => coerce_identifier_into_asset_expr(x),
402 ast::AssetExpr::PropertyAccess(_x) => todo!(),
403 }
404 }
405}
406
407impl IntoLower for ast::AddressExpr {
408 type Output = ir::Expression;
409
410 fn into_lower(&self) -> Result<Self::Output, Error> {
411 match self {
412 ast::AddressExpr::String(x) => Ok(ir::Expression::String(x.value.clone())),
413 ast::AddressExpr::HexString(x) => Ok(ir::Expression::Bytes(hex::decode(&x.value)?)),
414 ast::AddressExpr::Identifier(x) => lower_into_address_expr(x),
415 }
416 }
417}
418
419impl IntoLower for ast::InputBlockField {
420 type Output = ir::Expression;
421
422 fn into_lower(&self) -> Result<Self::Output, Error> {
423 match self {
424 ast::InputBlockField::From(x) => x.into_lower(),
425 ast::InputBlockField::DatumIs(_) => todo!(),
426 ast::InputBlockField::MinAmount(x) => x.into_lower(),
427 ast::InputBlockField::Redeemer(x) => x.into_lower(),
428 ast::InputBlockField::Ref(x) => x.into_lower(),
429 }
430 }
431}
432
433impl IntoLower for ast::InputBlock {
434 type Output = ir::Input;
435
436 fn into_lower(&self) -> Result<Self::Output, Error> {
437 let from = self.find("from");
438 let min_amount = self.find("min_amount");
439 let r#ref = self.find("ref");
440 let redeemer = self.find("redeemer");
441
442 let policy = from
443 .and_then(ast::InputBlockField::as_address_expr)
444 .and_then(ast::AddressExpr::as_identifier)
445 .and_then(|x| x.symbol.as_ref())
446 .and_then(|x| x.as_policy_def())
447 .map(|x| x.into_lower())
448 .transpose()?;
449
450 let input = ir::Input {
451 name: self.name.to_lowercase().clone(),
452 query: ir::InputQuery {
453 address: from.into_lower()?,
454 min_amount: min_amount.into_lower()?,
455 r#ref: r#ref.into_lower()?,
456 }
457 .into(),
458 refs: HashSet::new(),
459 redeemer: redeemer.into_lower()?,
460 policy,
461 };
462
463 Ok(input)
464 }
465}
466
467impl IntoLower for ast::OutputBlockField {
468 type Output = ir::Expression;
469
470 fn into_lower(&self) -> Result<Self::Output, Error> {
471 match self {
472 ast::OutputBlockField::To(x) => x.into_lower(),
473 ast::OutputBlockField::Amount(x) => x.into_lower(),
474 ast::OutputBlockField::Datum(x) => x.into_lower(),
475 }
476 }
477}
478
479impl IntoLower for ast::OutputBlock {
480 type Output = ir::Output;
481
482 fn into_lower(&self) -> Result<Self::Output, Error> {
483 Ok(ir::Output {
484 address: self.find("to").into_lower()?,
485 datum: self.find("datum").into_lower()?,
486 amount: self.find("amount").into_lower()?,
487 })
488 }
489}
490
491impl IntoLower for ast::MintBlockField {
492 type Output = ir::Expression;
493
494 fn into_lower(&self) -> Result<Self::Output, Error> {
495 match self {
496 ast::MintBlockField::Amount(x) => x.into_lower(),
497 ast::MintBlockField::Redeemer(x) => x.into_lower(),
498 }
499 }
500}
501
502impl IntoLower for ast::MintBlock {
503 type Output = ir::Mint;
504
505 fn into_lower(&self) -> Result<Self::Output, Error> {
506 Ok(ir::Mint {
507 amount: self.find("amount").into_lower()?,
508 redeemer: self.find("redeemer").into_lower()?,
509 })
510 }
511}
512
513impl IntoLower for ast::ChainSpecificBlock {
514 type Output = ir::AdHocDirective;
515
516 fn into_lower(&self) -> Result<Self::Output, Error> {
517 match self {
518 ast::ChainSpecificBlock::Cardano(x) => x.into_lower(),
519 }
520 }
521}
522
523impl IntoLower for ast::ReferenceBlock {
524 type Output = ir::Expression;
525
526 fn into_lower(&self) -> Result<Self::Output, Error> {
527 self.r#ref.into_lower()
528 }
529}
530
531impl IntoLower for ast::CollateralBlockField {
532 type Output = ir::Expression;
533
534 fn into_lower(&self) -> Result<Self::Output, Error> {
535 match self {
536 ast::CollateralBlockField::From(x) => x.into_lower(),
537 ast::CollateralBlockField::MinAmount(x) => x.into_lower(),
538 ast::CollateralBlockField::Ref(x) => x.into_lower(),
539 }
540 }
541}
542
543impl IntoLower for ast::CollateralBlock {
544 type Output = ir::Collateral;
545
546 fn into_lower(&self) -> Result<Self::Output, Error> {
547 let from = self.find("from");
548 let min_amount = self.find("min_amount");
549 let r#ref = self.find("ref");
550
551 let collateral = ir::Collateral {
552 query: ir::InputQuery {
553 address: from.into_lower()?,
554 min_amount: min_amount.into_lower()?,
555 r#ref: r#ref.into_lower()?,
556 }
557 .into(),
558 };
559
560 Ok(collateral)
561 }
562}
563
564pub fn lower_tx(ast: &ast::TxDef) -> Result<ir::Tx, Error> {
565 let ir = ir::Tx {
566 references: ast
567 .references
568 .iter()
569 .map(|x| x.into_lower())
570 .collect::<Result<Vec<_>, _>>()?,
571 inputs: ast
572 .inputs
573 .iter()
574 .map(|x| x.into_lower())
575 .collect::<Result<Vec<_>, _>>()?,
576 outputs: ast
577 .outputs
578 .iter()
579 .map(|x| x.into_lower())
580 .collect::<Result<Vec<_>, _>>()?,
581 mint: ast.mint.as_ref().map(|x| x.into_lower()).transpose()?,
582 adhoc: ast
583 .adhoc
584 .iter()
585 .map(|x| x.into_lower())
586 .collect::<Result<Vec<_>, _>>()?,
587 fees: ir::Expression::FeeQuery,
588 collateral: ast
589 .collateral
590 .iter()
591 .map(|x| x.into_lower())
592 .collect::<Result<Vec<_>, _>>()?,
593 };
594
595 Ok(ir)
596}
597
598pub fn lower(ast: &ast::Program, template: &str) -> Result<ir::Tx, Error> {
611 let tx = ast
612 .txs
613 .iter()
614 .find(|x| x.name == template)
615 .ok_or(Error::InvalidAst("tx not found".to_string()))?;
616
617 lower_tx(tx)
618}
619
620#[cfg(test)]
621mod tests {
622 use assert_json_diff::assert_json_eq;
623 use paste::paste;
624
625 use super::*;
626 use crate::parsing::{self};
627
628 fn make_snapshot_if_missing(example: &str, name: &str, tx: &ir::Tx) {
629 let manifest_dir = env!("CARGO_MANIFEST_DIR");
630
631 let path = format!("{}/../../examples/{}.{}.tir", manifest_dir, example, name);
632
633 if !std::fs::exists(&path).unwrap() {
634 let ir = serde_json::to_string_pretty(tx).unwrap();
635 std::fs::write(&path, ir).unwrap();
636 }
637 }
638
639 fn test_lowering_example(example: &str) {
640 let manifest_dir = env!("CARGO_MANIFEST_DIR");
641 let mut program = parsing::parse_well_known_example(example);
642
643 crate::analyzing::analyze(&mut program).ok().unwrap();
644
645 for tx in program.txs.iter() {
646 let tir = lower(&program, &tx.name).unwrap();
647
648 make_snapshot_if_missing(example, &tx.name, &tir);
649
650 let tir_file = format!(
651 "{}/../../examples/{}.{}.tir",
652 manifest_dir, example, tx.name
653 );
654
655 let expected = std::fs::read_to_string(tir_file).unwrap();
656 let expected: ir::Tx = serde_json::from_str(&expected).unwrap();
657
658 assert_json_eq!(tir, expected);
659 }
660 }
661
662 #[macro_export]
663 macro_rules! test_lowering {
664 ($name:ident) => {
665 paste! {
666 #[test]
667 fn [<test_example_ $name>]() {
668 test_lowering_example(stringify!($name));
669 }
670 }
671 };
672 }
673
674 test_lowering!(lang_tour);
675
676 test_lowering!(transfer);
677
678 test_lowering!(swap);
679
680 test_lowering!(asteria);
681
682 test_lowering!(vesting);
683
684 test_lowering!(faucet);
685}