tx3_lang/
applying.rs

1use std::collections::{BTreeMap, HashMap, HashSet};
2
3use crate::{
4    ir::{self, BinaryOp},
5    ArgValue, Utxo,
6};
7
8#[derive(Debug, thiserror::Error)]
9pub enum Error {
10    #[error("invalid binary operation {0:?}")]
11    InvalidBinaryOp(BinaryOp),
12
13    #[error("invalid argument {0:?} for {1}")]
14    InvalidArgument(ArgValue, String),
15
16    #[error("property {0} not found in {1}")]
17    PropertyNotFound(String, String),
18}
19
20fn arg_value_into_expr(arg: ArgValue) -> ir::Expression {
21    match arg {
22        ArgValue::Address(x) => ir::Expression::Address(x),
23        ArgValue::Int(x) => ir::Expression::Number(x),
24        ArgValue::Bool(x) => ir::Expression::Bool(x),
25        ArgValue::String(x) => ir::Expression::String(x),
26        ArgValue::Bytes(x) => ir::Expression::Bytes(x),
27        ArgValue::UtxoSet(x) => ir::Expression::UtxoSet(x),
28        ArgValue::UtxoRef(x) => ir::Expression::UtxoRefs(vec![x]),
29    }
30}
31
32pub trait Apply: Sized + std::fmt::Debug {
33    fn apply_args(self, args: &BTreeMap<String, ArgValue>) -> Result<Self, Error>;
34    fn apply_inputs(self, args: &BTreeMap<String, HashSet<Utxo>>) -> Result<Self, Error>;
35    fn apply_fees(self, fees: u64) -> Result<Self, Error>;
36    fn is_constant(&self) -> bool;
37    fn params(&self) -> BTreeMap<String, ir::Type>;
38    fn queries(&self) -> BTreeMap<String, ir::InputQuery>;
39
40    fn reduce_self(self) -> Result<Self, Error>;
41    fn reduce_nested(self) -> Result<Self, Error>;
42
43    fn reduce(self) -> Result<Self, Error> {
44        let reduced = self.reduce_nested()?;
45
46        if reduced.is_constant() {
47            reduced.reduce_self()
48        } else {
49            Ok(reduced)
50        }
51    }
52}
53
54impl<T> Apply for Option<T>
55where
56    T: Apply,
57{
58    fn apply_args(self, args: &BTreeMap<String, ArgValue>) -> Result<Self, Error> {
59        self.map(|x| x.apply_args(args)).transpose()
60    }
61
62    fn apply_inputs(self, args: &BTreeMap<String, HashSet<Utxo>>) -> Result<Self, Error> {
63        self.map(|x| x.apply_inputs(args)).transpose()
64    }
65
66    fn apply_fees(self, fees: u64) -> Result<Self, Error> {
67        self.map(|x| x.apply_fees(fees)).transpose()
68    }
69
70    fn is_constant(&self) -> bool {
71        match self {
72            Some(x) => x.is_constant(),
73            None => true,
74        }
75    }
76
77    fn params(&self) -> BTreeMap<String, ir::Type> {
78        match self {
79            Some(x) => x.params(),
80            None => BTreeMap::new(),
81        }
82    }
83
84    fn queries(&self) -> BTreeMap<String, ir::InputQuery> {
85        match self {
86            Some(x) => x.queries(),
87            None => BTreeMap::new(),
88        }
89    }
90
91    fn reduce_self(self) -> Result<Self, Error> {
92        Ok(self)
93    }
94
95    fn reduce_nested(self) -> Result<Self, Error> {
96        self.map(|x| x.reduce()).transpose()
97    }
98}
99
100impl<T> Apply for Box<T>
101where
102    T: Apply,
103{
104    fn apply_args(self, args: &BTreeMap<String, ArgValue>) -> Result<Self, Error> {
105        let x = *self;
106        Ok(Box::new(x.apply_args(args)?))
107    }
108
109    fn apply_inputs(self, args: &BTreeMap<String, HashSet<Utxo>>) -> Result<Self, Error> {
110        let x = *self;
111        Ok(Box::new(x.apply_inputs(args)?))
112    }
113
114    fn apply_fees(self, fees: u64) -> Result<Self, Error> {
115        let x = *self;
116        Ok(Box::new(x.apply_fees(fees)?))
117    }
118
119    fn is_constant(&self) -> bool {
120        self.as_ref().is_constant()
121    }
122
123    fn params(&self) -> BTreeMap<String, ir::Type> {
124        self.as_ref().params()
125    }
126
127    fn queries(&self) -> BTreeMap<String, ir::InputQuery> {
128        self.as_ref().queries()
129    }
130
131    fn reduce_self(self) -> Result<Self, Error> {
132        Ok(self)
133    }
134
135    fn reduce_nested(self) -> Result<Self, Error> {
136        let x = (*self).reduce()?;
137        Ok(Box::new(x))
138    }
139}
140
141impl<T> Apply for Vec<T>
142where
143    T: Apply,
144{
145    fn apply_args(self, args: &BTreeMap<String, ArgValue>) -> Result<Self, Error> {
146        self.into_iter().map(|x| x.apply_args(args)).collect()
147    }
148
149    fn apply_inputs(self, args: &BTreeMap<String, HashSet<Utxo>>) -> Result<Self, Error> {
150        self.into_iter().map(|x| x.apply_inputs(args)).collect()
151    }
152
153    fn apply_fees(self, fees: u64) -> Result<Self, Error> {
154        self.into_iter().map(|x| x.apply_fees(fees)).collect()
155    }
156
157    fn is_constant(&self) -> bool {
158        self.iter().all(|x| x.is_constant())
159    }
160
161    fn params(&self) -> BTreeMap<String, ir::Type> {
162        // TODO: what happens if there's a conflict on types?
163
164        self.iter()
165            .map(|x| x.params())
166            .fold(BTreeMap::new(), |mut acc, map| {
167                acc.extend(map);
168                acc
169            })
170    }
171
172    fn queries(&self) -> BTreeMap<String, ir::InputQuery> {
173        self.iter()
174            .map(|x| x.queries())
175            .fold(BTreeMap::new(), |mut acc, map| {
176                acc.extend(map);
177                acc
178            })
179    }
180
181    fn reduce_self(self) -> Result<Self, Error> {
182        Ok(self)
183    }
184
185    fn reduce_nested(self) -> Result<Self, Error> {
186        let reduced = self
187            .into_iter()
188            .map(|x| x.reduce())
189            .collect::<Result<Vec<_>, _>>()?;
190
191        Ok(reduced)
192    }
193}
194
195impl<T> Apply for HashMap<String, T>
196where
197    T: Apply,
198{
199    fn apply_args(self, args: &BTreeMap<String, ArgValue>) -> Result<Self, Error> {
200        let items = self
201            .into_iter()
202            .map(|(k, v)| v.apply_args(args).map(|v| (k, v)))
203            .collect::<Result<Vec<_>, _>>()?
204            .into_iter()
205            .collect();
206
207        Ok(items)
208    }
209
210    fn apply_inputs(self, args: &BTreeMap<String, HashSet<Utxo>>) -> Result<Self, Error> {
211        let items = self
212            .into_iter()
213            .map(|(k, v)| v.apply_inputs(args).map(|v| (k, v)))
214            .collect::<Result<Vec<_>, _>>()?
215            .into_iter()
216            .collect();
217
218        Ok(items)
219    }
220
221    fn apply_fees(self, fees: u64) -> Result<Self, Error> {
222        let items = self
223            .into_iter()
224            .map(|(k, v)| v.apply_fees(fees).map(|v| (k, v)))
225            .collect::<Result<Vec<_>, _>>()?
226            .into_iter()
227            .collect();
228
229        Ok(items)
230    }
231
232    fn is_constant(&self) -> bool {
233        self.iter().all(|(_, v)| v.is_constant())
234    }
235
236    fn params(&self) -> BTreeMap<String, ir::Type> {
237        self.iter()
238            .map(|(_, v)| v.params())
239            .fold(BTreeMap::new(), |mut acc, map| {
240                acc.extend(map);
241                acc
242            })
243    }
244
245    fn queries(&self) -> BTreeMap<String, ir::InputQuery> {
246        self.iter()
247            .map(|(_, v)| v.queries())
248            .fold(BTreeMap::new(), |mut acc, map| {
249                acc.extend(map);
250                acc
251            })
252    }
253
254    fn reduce_self(self) -> Result<Self, Error> {
255        Ok(self)
256    }
257
258    fn reduce_nested(self) -> Result<Self, Error> {
259        let reduced = self
260            .into_iter()
261            .map(|(k, v)| v.reduce().map(|v| (k, v)))
262            .collect::<Result<Vec<_>, _>>()?
263            .into_iter()
264            .collect();
265
266        Ok(reduced)
267    }
268}
269
270impl Apply for ir::ScriptSource {
271    fn apply_args(self, args: &BTreeMap<String, ArgValue>) -> Result<Self, Error> {
272        match self {
273            ir::ScriptSource::Embedded(x) => Ok(ir::ScriptSource::Embedded(x.apply_args(args)?)),
274            ir::ScriptSource::UtxoRef { r#ref, source } => Ok(ir::ScriptSource::UtxoRef {
275                r#ref: r#ref.apply_args(args)?,
276                source: source.apply_args(args)?,
277            }),
278        }
279    }
280
281    fn apply_inputs(self, args: &BTreeMap<String, HashSet<Utxo>>) -> Result<Self, Error> {
282        match self {
283            ir::ScriptSource::Embedded(x) => Ok(ir::ScriptSource::Embedded(x.apply_inputs(args)?)),
284            ir::ScriptSource::UtxoRef { r#ref, source } => Ok(ir::ScriptSource::UtxoRef {
285                r#ref: r#ref.apply_inputs(args)?,
286                source: source.apply_inputs(args)?,
287            }),
288        }
289    }
290
291    fn apply_fees(self, fees: u64) -> Result<Self, Error> {
292        match self {
293            ir::ScriptSource::Embedded(x) => Ok(ir::ScriptSource::Embedded(x.apply_fees(fees)?)),
294            ir::ScriptSource::UtxoRef { r#ref, source } => Ok(ir::ScriptSource::UtxoRef {
295                r#ref: r#ref.apply_fees(fees)?,
296                source: source.apply_fees(fees)?,
297            }),
298        }
299    }
300
301    fn is_constant(&self) -> bool {
302        match self {
303            ir::ScriptSource::Embedded(x) => x.is_constant(),
304            ir::ScriptSource::UtxoRef { r#ref, source } => {
305                r#ref.is_constant() && source.is_constant()
306            }
307        }
308    }
309
310    fn params(&self) -> BTreeMap<String, ir::Type> {
311        let mut params = BTreeMap::new();
312
313        match self {
314            ir::ScriptSource::Embedded(x) => params.extend(x.params()),
315            ir::ScriptSource::UtxoRef { r#ref, source } => {
316                params.extend(r#ref.params());
317                params.extend(source.params());
318            }
319        }
320
321        params
322    }
323
324    fn queries(&self) -> BTreeMap<String, ir::InputQuery> {
325        let mut queries = BTreeMap::new();
326
327        match self {
328            ir::ScriptSource::Embedded(x) => queries.extend(x.queries()),
329            ir::ScriptSource::UtxoRef { r#ref, source } => {
330                queries.extend(r#ref.queries());
331                queries.extend(source.queries());
332            }
333        }
334
335        queries
336    }
337
338    fn reduce_self(self) -> Result<Self, Error> {
339        Ok(self)
340    }
341
342    fn reduce_nested(self) -> Result<Self, Error> {
343        match self {
344            ir::ScriptSource::Embedded(x) => Ok(ir::ScriptSource::Embedded(x.reduce_nested()?)),
345            ir::ScriptSource::UtxoRef { r#ref, source } => Ok(ir::ScriptSource::UtxoRef {
346                r#ref: r#ref.reduce_nested()?,
347                source: source.reduce_nested()?,
348            }),
349        }
350    }
351}
352
353impl TryFrom<&ArgValue> for ir::ScriptSource {
354    type Error = Error;
355
356    fn try_from(value: &ArgValue) -> Result<Self, Self::Error> {
357        match value {
358            ArgValue::Bytes(x) => Ok(ir::ScriptSource::Embedded(ir::Expression::Bytes(x.clone()))),
359            ArgValue::UtxoRef(x) => Ok(ir::ScriptSource::UtxoRef {
360                r#ref: ir::Expression::UtxoRefs(vec![x.clone()]),
361                source: None,
362            }),
363            _ => Err(Error::InvalidArgument(value.clone(), "script".to_string())),
364        }
365    }
366}
367
368impl Apply for ir::PolicyExpr {
369    fn apply_args(self, args: &BTreeMap<String, ArgValue>) -> Result<Self, Error> {
370        let name = self.name;
371        let hash = self.hash.apply_args(args)?;
372
373        let script = if self.script.is_some() {
374            self.script.apply_args(args)?
375        } else {
376            let defined = args.get(&format!("{}_script", name.to_lowercase()));
377            defined.map(TryFrom::try_from).transpose()?
378        };
379
380        Ok(Self { name, hash, script })
381    }
382
383    fn apply_inputs(self, args: &BTreeMap<String, HashSet<Utxo>>) -> Result<Self, Error> {
384        let name = self.name;
385        let hash = self.hash.apply_inputs(args)?;
386
387        let script = self
388            .script
389            .map(|x| match x {
390                ir::ScriptSource::UtxoRef { r#ref, .. } => {
391                    let source = args
392                        .get(&name.to_lowercase())
393                        .and_then(|x| x.iter().next())
394                        .and_then(|x| x.script.clone());
395
396                    Ok(ir::ScriptSource::UtxoRef { r#ref, source })
397                }
398                x => Ok(x),
399            })
400            .transpose()?;
401
402        Ok(Self { name, hash, script })
403    }
404
405    fn apply_fees(self, fees: u64) -> Result<Self, Error> {
406        Ok(Self {
407            name: self.name,
408            hash: self.hash.apply_fees(fees)?,
409            script: self.script.apply_fees(fees)?,
410        })
411    }
412
413    fn is_constant(&self) -> bool {
414        self.hash.is_constant() && self.script.is_constant()
415    }
416
417    fn params(&self) -> BTreeMap<String, ir::Type> {
418        let mut params = BTreeMap::new();
419        params.extend(self.hash.params());
420        params.extend(self.script.params());
421
422        if self.script.is_none() {
423            params.insert(
424                format!("{}_script", self.name.to_lowercase()),
425                ir::Type::UtxoRef,
426            );
427        }
428
429        params
430    }
431
432    fn queries(&self) -> BTreeMap<String, ir::InputQuery> {
433        let mut queries = BTreeMap::new();
434
435        if let Some(ir::ScriptSource::UtxoRef { r#ref, source }) = &self.script {
436            if source.is_none() {
437                queries.insert(
438                    self.name.to_lowercase(),
439                    ir::InputQuery {
440                        r#ref: Some(r#ref.clone()),
441                        ..Default::default()
442                    },
443                );
444            }
445        }
446
447        queries
448    }
449
450    fn reduce_self(self) -> Result<Self, Error> {
451        Ok(self)
452    }
453
454    fn reduce_nested(self) -> Result<Self, Error> {
455        Ok(Self {
456            name: self.name,
457            hash: self.hash.reduce_nested()?,
458            script: self.script.reduce_nested()?,
459        })
460    }
461}
462
463impl Apply for ir::StructExpr {
464    fn apply_args(self, args: &BTreeMap<String, ArgValue>) -> Result<Self, Error> {
465        Ok(Self {
466            constructor: self.constructor,
467            fields: self
468                .fields
469                .into_iter()
470                .map(|x| x.apply_args(args))
471                .collect::<Result<Vec<_>, _>>()?,
472        })
473    }
474
475    fn apply_inputs(self, args: &BTreeMap<String, HashSet<Utxo>>) -> Result<Self, Error> {
476        Ok(Self {
477            constructor: self.constructor,
478            fields: self
479                .fields
480                .into_iter()
481                .map(|x| x.apply_inputs(args))
482                .collect::<Result<Vec<_>, _>>()?,
483        })
484    }
485
486    fn apply_fees(self, fees: u64) -> Result<Self, Error> {
487        Ok(Self {
488            constructor: self.constructor,
489            fields: self
490                .fields
491                .into_iter()
492                .map(|x| x.apply_fees(fees))
493                .collect::<Result<Vec<_>, _>>()?,
494        })
495    }
496
497    fn is_constant(&self) -> bool {
498        self.fields.iter().all(|x| x.is_constant())
499    }
500
501    fn params(&self) -> BTreeMap<String, ir::Type> {
502        self.fields
503            .iter()
504            .map(|x| x.params())
505            .fold(BTreeMap::new(), |mut acc, map| {
506                acc.extend(map);
507                acc
508            })
509    }
510
511    fn queries(&self) -> BTreeMap<String, ir::InputQuery> {
512        // structs don't have queries
513        BTreeMap::new()
514    }
515
516    fn reduce_self(self) -> Result<Self, Error> {
517        Ok(self)
518    }
519
520    fn reduce_nested(self) -> Result<Self, Error> {
521        Ok(Self {
522            constructor: self.constructor,
523            fields: self
524                .fields
525                .into_iter()
526                .map(|x| x.reduce())
527                .collect::<Result<Vec<_>, _>>()?,
528        })
529    }
530}
531
532impl Apply for ir::PropertyAccess {
533    fn apply_args(self, args: &BTreeMap<String, ArgValue>) -> Result<Self, Error> {
534        Ok(Self {
535            object: self.object.apply_args(args)?,
536            field: self.field,
537        })
538    }
539
540    fn apply_inputs(self, args: &BTreeMap<String, HashSet<Utxo>>) -> Result<Self, Error> {
541        Ok(Self {
542            object: self.object.apply_inputs(args)?,
543            field: self.field,
544        })
545    }
546
547    fn apply_fees(self, fees: u64) -> Result<Self, Error> {
548        Ok(Self {
549            object: self.object.apply_fees(fees)?,
550            field: self.field,
551        })
552    }
553
554    fn is_constant(&self) -> bool {
555        self.object.is_constant()
556    }
557
558    fn params(&self) -> BTreeMap<String, ir::Type> {
559        self.object.params()
560    }
561
562    fn queries(&self) -> BTreeMap<String, ir::InputQuery> {
563        self.object.queries()
564    }
565
566    fn reduce_self(self) -> Result<Self, Error> {
567        Ok(self)
568    }
569
570    fn reduce_nested(self) -> Result<Self, Error> {
571        Ok(Self {
572            object: self.object.reduce()?,
573            field: self.field,
574        })
575    }
576}
577
578impl Apply for ir::AssetExpr {
579    fn apply_args(self, args: &BTreeMap<String, ArgValue>) -> Result<Self, Error> {
580        Ok(Self {
581            policy: self.policy.apply_args(args)?,
582            asset_name: self.asset_name.apply_args(args)?,
583            amount: self.amount.apply_args(args)?,
584        })
585    }
586
587    fn apply_inputs(self, args: &BTreeMap<String, HashSet<Utxo>>) -> Result<Self, Error> {
588        Ok(Self {
589            policy: self.policy.apply_inputs(args)?,
590            asset_name: self.asset_name.apply_inputs(args)?,
591            amount: self.amount.apply_inputs(args)?,
592        })
593    }
594
595    fn apply_fees(self, fees: u64) -> Result<Self, Error> {
596        Ok(Self {
597            policy: self.policy.apply_fees(fees)?,
598            asset_name: self.asset_name.apply_fees(fees)?,
599            amount: self.amount.apply_fees(fees)?,
600        })
601    }
602
603    fn is_constant(&self) -> bool {
604        self.policy.is_constant() && self.asset_name.is_constant() && self.amount.is_constant()
605    }
606
607    fn params(&self) -> BTreeMap<String, ir::Type> {
608        let mut params = BTreeMap::new();
609        params.extend(self.policy.params());
610        params.extend(self.asset_name.params());
611        params.extend(self.amount.params());
612        params
613    }
614
615    fn queries(&self) -> BTreeMap<String, ir::InputQuery> {
616        // assets don't have queries
617        BTreeMap::new()
618    }
619
620    fn reduce_self(self) -> Result<Self, Error> {
621        Ok(self)
622    }
623
624    fn reduce_nested(self) -> Result<Self, Error> {
625        Ok(Self {
626            policy: self.policy.reduce()?,
627            asset_name: self.asset_name.reduce()?,
628            amount: self.amount.reduce()?,
629        })
630    }
631}
632
633impl Apply for ir::BinaryOp {
634    fn apply_args(self, args: &BTreeMap<String, ArgValue>) -> Result<Self, Error> {
635        let left = self.left.apply_args(args)?;
636        let right = self.right.apply_args(args)?;
637
638        let op = Self {
639            left,
640            right,
641            op: self.op,
642        };
643
644        // TODO: reduce if both sides are constants
645
646        Ok(op)
647    }
648
649    fn apply_inputs(self, args: &BTreeMap<String, HashSet<Utxo>>) -> Result<Self, Error> {
650        let left = self.left.apply_inputs(args)?;
651        let right = self.right.apply_inputs(args)?;
652
653        Ok(Self {
654            left,
655            right,
656            op: self.op,
657        })
658    }
659
660    fn apply_fees(self, fees: u64) -> Result<Self, Error> {
661        Ok(Self {
662            left: self.left.apply_fees(fees)?,
663            right: self.right.apply_fees(fees)?,
664            op: self.op,
665        })
666    }
667
668    fn is_constant(&self) -> bool {
669        self.left.is_constant() && self.right.is_constant()
670    }
671
672    fn params(&self) -> BTreeMap<String, ir::Type> {
673        let mut params = BTreeMap::new();
674        params.extend(self.left.params());
675        params.extend(self.right.params());
676        params
677    }
678
679    fn queries(&self) -> BTreeMap<String, ir::InputQuery> {
680        // binary ops don't have queries
681        BTreeMap::new()
682    }
683
684    fn reduce_self(self) -> Result<Self, Error> {
685        Ok(self)
686    }
687
688    fn reduce_nested(self) -> Result<Self, Error> {
689        Ok(Self {
690            left: self.left.reduce()?,
691            right: self.right.reduce()?,
692            op: self.op,
693        })
694    }
695}
696
697fn build_assets(aggregated: HashMap<AssetClass, i128>) -> Vec<ir::AssetExpr> {
698    // Convert back to Vec<AssetExpr>
699    aggregated
700        .into_iter()
701        .map(|((policy, asset_name), amount)| ir::AssetExpr {
702            policy: match policy {
703                Some(policy) => ir::Expression::Bytes(policy.to_vec()),
704                None => ir::Expression::None,
705            },
706            asset_name: match asset_name {
707                Some(asset_name) => ir::Expression::Bytes(asset_name.to_vec()),
708                None => ir::Expression::None,
709            },
710            amount: ir::Expression::Number(amount),
711        })
712        .collect()
713}
714
715type AssetClass<'a> = (Option<&'a [u8]>, Option<&'a [u8]>);
716
717impl ir::AssetExpr {
718    fn expect_constant_policy(&self) -> Option<&[u8]> {
719        match &self.policy {
720            ir::Expression::None => None,
721            ir::Expression::Bytes(x) => Some(x.as_slice()),
722            _ => None,
723        }
724    }
725
726    fn expect_constant_name(&self) -> Option<&[u8]> {
727        match &self.asset_name {
728            ir::Expression::None => None,
729            ir::Expression::Bytes(x) => Some(x.as_slice()),
730            _ => None,
731        }
732    }
733
734    fn expect_constant_amount(&self) -> i128 {
735        match &self.amount {
736            ir::Expression::Number(x) => *x,
737            _ => unreachable!("amount expected to be Number"),
738        }
739    }
740
741    fn aggregate(items: &[Self]) -> HashMap<AssetClass, i128> {
742        let mut aggregated: HashMap<AssetClass, i128> = HashMap::new();
743
744        // Group assets by policy and asset_name, summing their amounts
745        for asset in items.iter() {
746            let policy = asset.expect_constant_policy();
747            let asset_name = asset.expect_constant_name();
748            let amount = asset.expect_constant_amount();
749
750            let key = (policy, asset_name);
751            *aggregated.entry(key).or_default() += amount;
752        }
753
754        aggregated
755    }
756
757    fn sum<'a>(
758        a: HashMap<AssetClass<'a>, i128>,
759        b: HashMap<AssetClass<'a>, i128>,
760    ) -> HashMap<AssetClass<'a>, i128> {
761        let mut aggregated = a;
762
763        for (key, value) in b {
764            *aggregated.entry(key).or_default() += value;
765        }
766
767        aggregated
768    }
769
770    fn sub<'a>(
771        a: HashMap<AssetClass<'a>, i128>,
772        b: HashMap<AssetClass<'a>, i128>,
773    ) -> HashMap<AssetClass<'a>, i128> {
774        let mut aggregated = a;
775
776        for (key, value) in b {
777            *aggregated.entry(key).or_default() -= value;
778        }
779
780        aggregated
781    }
782}
783
784impl Apply for ir::InputQuery {
785    fn apply_args(self, args: &BTreeMap<String, ArgValue>) -> Result<Self, Error> {
786        Ok(Self {
787            address: self.address.apply_args(args)?,
788            min_amount: self.min_amount.apply_args(args)?,
789            r#ref: self.r#ref.apply_args(args)?,
790        })
791    }
792
793    fn apply_inputs(self, args: &BTreeMap<String, HashSet<Utxo>>) -> Result<Self, Error> {
794        Ok(Self {
795            address: self.address.apply_inputs(args)?,
796            min_amount: self.min_amount.apply_inputs(args)?,
797            r#ref: self.r#ref.apply_inputs(args)?,
798        })
799    }
800
801    fn apply_fees(self, fees: u64) -> Result<Self, Error> {
802        Ok(Self {
803            address: self.address.apply_fees(fees)?,
804            min_amount: self.min_amount.apply_fees(fees)?,
805            r#ref: self.r#ref.apply_fees(fees)?,
806        })
807    }
808
809    fn is_constant(&self) -> bool {
810        self.address.is_constant() && self.min_amount.is_constant() && self.r#ref.is_constant()
811    }
812
813    fn params(&self) -> BTreeMap<String, ir::Type> {
814        let mut params = BTreeMap::new();
815        params.extend(self.address.params());
816        params.extend(self.min_amount.params());
817        params.extend(self.r#ref.params());
818        params
819    }
820
821    fn queries(&self) -> BTreeMap<String, ir::InputQuery> {
822        // input queries itself don't have inner queries. This is assuming that an
823        // expression higher up the tree will return this as a required query.
824        BTreeMap::new()
825    }
826
827    fn reduce_self(self) -> Result<Self, Error> {
828        Ok(self)
829    }
830
831    fn reduce_nested(self) -> Result<Self, Error> {
832        Ok(Self {
833            address: self.address.reduce()?,
834            min_amount: self.min_amount.reduce()?,
835            r#ref: self.r#ref.reduce()?,
836        })
837    }
838}
839
840impl Apply for ir::Input {
841    fn apply_args(self, args: &BTreeMap<String, ArgValue>) -> Result<Self, Error> {
842        Ok(Self {
843            query: self.query.apply_args(args)?,
844            redeemer: self.redeemer.apply_args(args)?,
845            policy: self.policy.apply_args(args)?,
846            ..self
847        })
848    }
849
850    fn apply_inputs(self, args: &BTreeMap<String, HashSet<Utxo>>) -> Result<Self, Error> {
851        let defined = args.get(&self.name).cloned();
852
853        if let Some(refs) = defined {
854            return Ok(Self {
855                query: None,
856                refs: refs.into_iter().map(|x| x.r#ref).collect(),
857                ..self
858            });
859        }
860
861        Ok(self)
862    }
863
864    fn apply_fees(self, fees: u64) -> Result<Self, Error> {
865        Ok(Self {
866            query: self.query.apply_fees(fees)?,
867            redeemer: self.redeemer.apply_fees(fees)?,
868            policy: self.policy.apply_fees(fees)?,
869            ..self
870        })
871    }
872
873    fn is_constant(&self) -> bool {
874        self.query.is_constant() && self.redeemer.is_constant() && self.policy.is_constant()
875    }
876
877    fn params(&self) -> BTreeMap<String, ir::Type> {
878        let mut params = BTreeMap::new();
879        params.extend(self.query.params());
880        params.extend(self.redeemer.params());
881        params.extend(self.policy.params());
882        params
883    }
884
885    fn queries(&self) -> BTreeMap<String, ir::InputQuery> {
886        let input = self.query.iter().map(|x| (self.name.clone(), x.clone()));
887        let policy = self.policy.queries();
888
889        BTreeMap::from_iter(input.chain(policy))
890    }
891
892    fn reduce_self(self) -> Result<Self, Error> {
893        Ok(self)
894    }
895
896    fn reduce_nested(self) -> Result<Self, Error> {
897        Ok(Self {
898            query: self.query.reduce()?,
899            redeemer: self.redeemer.reduce()?,
900            policy: self.policy.reduce()?,
901            ..self
902        })
903    }
904}
905
906impl Apply for ir::Expression {
907    fn apply_args(self, args: &BTreeMap<String, ArgValue>) -> Result<Self, Error> {
908        match self {
909            ir::Expression::Struct(x) => Ok(ir::Expression::Struct(x.apply_args(args)?)),
910            ir::Expression::List(x) => Ok(ir::Expression::List(x.apply_args(args)?)),
911            ir::Expression::Assets(x) => Ok(ir::Expression::Assets(x.apply_args(args)?)),
912            ir::Expression::EvalCustom(x) => Ok(ir::Expression::EvalCustom(x.apply_args(args)?)),
913            ir::Expression::EvalParameter(name, ty) => {
914                let defined = args.get(&name).cloned();
915
916                match defined {
917                    Some(x) => Ok(arg_value_into_expr(x)),
918                    None => Ok(ir::Expression::EvalParameter(name, ty)),
919                }
920            }
921
922            // the remaining cases are constants, so we can just return them
923            x => Ok(x),
924        }
925    }
926
927    fn apply_inputs(self, args: &BTreeMap<String, HashSet<Utxo>>) -> Result<Self, Error> {
928        match self {
929            ir::Expression::EvalInputDatum(x) => {
930                let defined = args.get(&x).cloned();
931
932                match defined {
933                    Some(x) => {
934                        let datum = x
935                            .into_iter()
936                            .flat_map(|x| x.datum)
937                            .next()
938                            .unwrap_or(ir::Expression::None);
939
940                        Ok(datum)
941                    }
942                    None => Ok(ir::Expression::EvalInputDatum(x)),
943                }
944            }
945            ir::Expression::EvalInputAssets(x) => {
946                let defined = args.get(&x).cloned();
947
948                match defined {
949                    Some(x) => {
950                        let assets = x.into_iter().flat_map(|x| x.assets).collect();
951
952                        Ok(ir::Expression::Assets(assets))
953                    }
954                    None => Ok(ir::Expression::EvalInputAssets(x)),
955                }
956            }
957            ir::Expression::Struct(x) => Ok(ir::Expression::Struct(x.apply_inputs(args)?)),
958            ir::Expression::List(x) => Ok(ir::Expression::List(x.apply_inputs(args)?)),
959            ir::Expression::Assets(x) => Ok(ir::Expression::Assets(x.apply_inputs(args)?)),
960            ir::Expression::EvalCustom(x) => Ok(ir::Expression::EvalCustom(x.apply_inputs(args)?)),
961            _ => Ok(self),
962        }
963    }
964
965    fn apply_fees(self, fees: u64) -> Result<Self, Error> {
966        match self {
967            ir::Expression::FeeQuery => Ok(ir::Expression::Assets(vec![ir::AssetExpr {
968                policy: ir::Expression::None,
969                asset_name: ir::Expression::None,
970                amount: ir::Expression::Number(fees as i128),
971            }])),
972            ir::Expression::Struct(x) => Ok(ir::Expression::Struct(x.apply_fees(fees)?)),
973            ir::Expression::List(x) => Ok(ir::Expression::List(x.apply_fees(fees)?)),
974            ir::Expression::Assets(x) => Ok(ir::Expression::Assets(x.apply_fees(fees)?)),
975            ir::Expression::EvalCustom(x) => Ok(ir::Expression::EvalCustom(x.apply_fees(fees)?)),
976            _ => Ok(self),
977        }
978    }
979
980    fn is_constant(&self) -> bool {
981        match self {
982            Self::None => true,
983            Self::Bytes(_) => true,
984            Self::Number(_) => true,
985            Self::Bool(_) => true,
986            Self::String(_) => true,
987            Self::Address(_) => true,
988            Self::Hash(_) => true,
989            Self::UtxoRefs(_) => true,
990            Self::UtxoSet(_) => true,
991            Self::List(x) => x.is_constant(),
992            Self::Struct(x) => x.is_constant(),
993            Self::Assets(x) => x.is_constant(),
994            Self::EvalCustom(x) => x.is_constant(),
995            Self::EvalProperty(x) => x.is_constant(),
996            Self::AdHocDirective(x) => x.is_constant(),
997            Self::EvalInputDatum(..) => false,
998            Self::EvalInputAssets(..) => false,
999            Self::FeeQuery => false,
1000            Self::EvalParameter(..) => false,
1001        }
1002    }
1003
1004    fn params(&self) -> BTreeMap<String, ir::Type> {
1005        match self {
1006            ir::Expression::Struct(x) => x.params(),
1007            ir::Expression::Assets(x) => x.params(),
1008            ir::Expression::EvalCustom(x) => x.params(),
1009            ir::Expression::EvalParameter(x, ty) => BTreeMap::from([(x.to_string(), ty.clone())]),
1010
1011            // the remaining cases are constants, so we can just return them
1012            _ => BTreeMap::new(),
1013        }
1014    }
1015
1016    fn queries(&self) -> BTreeMap<String, ir::InputQuery> {
1017        BTreeMap::new()
1018    }
1019
1020    fn reduce_self(self) -> Result<Self, Error> {
1021        match self {
1022            ir::Expression::EvalCustom(op) => match (&op.left, &op.right) {
1023                (ir::Expression::Number(x), ir::Expression::Number(y)) => {
1024                    let result = match &op.op {
1025                        ir::BinaryOpKind::Add => x + y,
1026                        ir::BinaryOpKind::Sub => x - y,
1027                    };
1028
1029                    Ok(ir::Expression::Number(result))
1030                }
1031                (ir::Expression::Assets(x), ir::Expression::Assets(y)) => {
1032                    let x = ir::AssetExpr::aggregate(x);
1033                    let y = ir::AssetExpr::aggregate(y);
1034
1035                    let result = match &op.op {
1036                        ir::BinaryOpKind::Add => ir::AssetExpr::sum(x, y),
1037                        ir::BinaryOpKind::Sub => ir::AssetExpr::sub(x, y),
1038                    };
1039
1040                    Ok(ir::Expression::Assets(build_assets(result)))
1041                }
1042                _ => Err(Error::InvalidBinaryOp(*op)),
1043            },
1044            ir::Expression::EvalProperty(_x) => {
1045                //TODO: property access of constant objects should be reduced but we're erasing
1046                // field names from the struct, making this impossible. We need to refactor
1047                // either the StructExpr so that it retains field names or the PropertyAccess
1048                // so that the path gets turned into field indexes.
1049                todo!("fix this")
1050            }
1051            _ => Ok(self),
1052        }
1053    }
1054
1055    fn reduce_nested(self) -> Result<Self, Error> {
1056        match self {
1057            ir::Expression::Struct(x) => Ok(ir::Expression::Struct(x.reduce()?)),
1058            ir::Expression::Assets(x) => Ok(ir::Expression::Assets(x.reduce()?)),
1059            ir::Expression::EvalCustom(x) => Ok(ir::Expression::EvalCustom(x.reduce()?)),
1060            _ => Ok(self),
1061        }
1062    }
1063}
1064
1065impl Apply for ir::Output {
1066    fn apply_args(self, args: &BTreeMap<String, ArgValue>) -> Result<Self, Error> {
1067        Ok(Self {
1068            address: self.address.apply_args(args)?,
1069            datum: self.datum.apply_args(args)?,
1070            amount: self.amount.apply_args(args)?,
1071        })
1072    }
1073
1074    fn apply_inputs(self, args: &BTreeMap<String, HashSet<Utxo>>) -> Result<Self, Error> {
1075        Ok(Self {
1076            address: self.address.apply_inputs(args)?,
1077            datum: self.datum.apply_inputs(args)?,
1078            amount: self.amount.apply_inputs(args)?,
1079        })
1080    }
1081
1082    fn apply_fees(self, fees: u64) -> Result<Self, Error> {
1083        Ok(Self {
1084            address: self.address.apply_fees(fees)?,
1085            datum: self.datum.apply_fees(fees)?,
1086            amount: self.amount.apply_fees(fees)?,
1087        })
1088    }
1089
1090    fn is_constant(&self) -> bool {
1091        self.address.is_constant() && self.datum.is_constant() && self.amount.is_constant()
1092    }
1093
1094    fn params(&self) -> BTreeMap<String, ir::Type> {
1095        let mut params = BTreeMap::new();
1096        params.extend(self.address.params());
1097        params.extend(self.datum.params());
1098        params.extend(self.amount.params());
1099        params
1100    }
1101
1102    fn queries(&self) -> BTreeMap<String, ir::InputQuery> {
1103        // outputs don't have queries
1104        BTreeMap::new()
1105    }
1106
1107    fn reduce_self(self) -> Result<Self, Error> {
1108        Ok(self)
1109    }
1110
1111    fn reduce_nested(self) -> Result<Self, Error> {
1112        Ok(Self {
1113            address: self.address.reduce()?,
1114            datum: self.datum.reduce()?,
1115            amount: self.amount.reduce()?,
1116        })
1117    }
1118}
1119
1120impl Apply for ir::Mint {
1121    fn apply_args(self, args: &BTreeMap<String, ArgValue>) -> Result<Self, Error> {
1122        Ok(Self {
1123            amount: self.amount.apply_args(args)?,
1124            redeemer: self.redeemer.apply_args(args)?,
1125        })
1126    }
1127
1128    fn apply_inputs(self, args: &BTreeMap<String, HashSet<Utxo>>) -> Result<Self, Error> {
1129        Ok(Self {
1130            amount: self.amount.apply_inputs(args)?,
1131            redeemer: self.redeemer.apply_inputs(args)?,
1132        })
1133    }
1134
1135    fn apply_fees(self, fees: u64) -> Result<Self, Error> {
1136        Ok(Self {
1137            amount: self.amount.apply_fees(fees)?,
1138            redeemer: self.redeemer.apply_fees(fees)?,
1139        })
1140    }
1141
1142    fn is_constant(&self) -> bool {
1143        self.amount.is_constant() && self.redeemer.is_constant()
1144    }
1145
1146    fn params(&self) -> BTreeMap<String, ir::Type> {
1147        let mut params = BTreeMap::new();
1148        params.extend(self.amount.params());
1149        params.extend(self.redeemer.params());
1150        params
1151    }
1152
1153    fn queries(&self) -> BTreeMap<String, ir::InputQuery> {
1154        // mints don't have queries
1155        BTreeMap::new()
1156    }
1157
1158    fn reduce_self(self) -> Result<Self, Error> {
1159        Ok(self)
1160    }
1161
1162    fn reduce_nested(self) -> Result<Self, Error> {
1163        Ok(Self {
1164            amount: self.amount.reduce()?,
1165            redeemer: self.redeemer.reduce()?,
1166        })
1167    }
1168}
1169
1170impl Apply for ir::AdHocDirective {
1171    fn apply_args(self, args: &BTreeMap<String, ArgValue>) -> Result<Self, Error> {
1172        Ok(Self {
1173            name: self.name,
1174            data: self.data.apply_args(args)?,
1175        })
1176    }
1177
1178    fn apply_inputs(self, args: &BTreeMap<String, HashSet<Utxo>>) -> Result<Self, Error> {
1179        Ok(Self {
1180            name: self.name,
1181            data: self.data.apply_inputs(args)?,
1182        })
1183    }
1184
1185    fn apply_fees(self, fees: u64) -> Result<Self, Error> {
1186        Ok(Self {
1187            name: self.name,
1188            data: self.data.apply_fees(fees)?,
1189        })
1190    }
1191
1192    fn is_constant(&self) -> bool {
1193        self.data.is_constant()
1194    }
1195
1196    fn params(&self) -> BTreeMap<String, ir::Type> {
1197        let mut params = BTreeMap::new();
1198        params.extend(self.data.params());
1199        params
1200    }
1201
1202    fn queries(&self) -> BTreeMap<String, ir::InputQuery> {
1203        BTreeMap::new()
1204    }
1205
1206    fn reduce_self(self) -> Result<Self, Error> {
1207        Ok(self)
1208    }
1209
1210    fn reduce_nested(self) -> Result<Self, Error> {
1211        Ok(Self {
1212            name: self.name,
1213            data: self.data.reduce()?,
1214        })
1215    }
1216}
1217
1218impl Apply for ir::Collateral {
1219    fn apply_args(self, args: &BTreeMap<String, ArgValue>) -> Result<Self, Error> {
1220        Ok(Self {
1221            query: self.query.apply_args(args)?,
1222        })
1223    }
1224
1225    fn apply_inputs(self, args: &BTreeMap<String, HashSet<Utxo>>) -> Result<Self, Error> {
1226        Ok(Self {
1227            query: self.query.apply_inputs(args)?,
1228        })
1229    }
1230
1231    fn apply_fees(self, fees: u64) -> Result<Self, Error> {
1232        Ok(Self {
1233            query: self.query.apply_fees(fees)?,
1234        })
1235    }
1236
1237    fn is_constant(&self) -> bool {
1238        self.query.is_constant()
1239    }
1240
1241    fn params(&self) -> BTreeMap<String, ir::Type> {
1242        let mut params = BTreeMap::new();
1243        params.extend(self.query.params());
1244        params
1245    }
1246
1247    fn queries(&self) -> BTreeMap<String, ir::InputQuery> {
1248        let mut b_tree = BTreeMap::new();
1249        b_tree.insert("collateral".to_string(), self.query.clone());
1250        b_tree
1251    }
1252
1253    fn reduce_self(self) -> Result<Self, Error> {
1254        Ok(self)
1255    }
1256
1257    fn reduce_nested(self) -> Result<Self, Error> {
1258        Ok(Self {
1259            query: self.query.reduce()?,
1260        })
1261    }
1262}
1263
1264impl Apply for ir::Tx {
1265    fn apply_args(self, args: &BTreeMap<String, ArgValue>) -> Result<Self, Error> {
1266        let tx = ir::Tx {
1267            references: self.references.apply_args(args)?,
1268            inputs: self.inputs.apply_args(args)?,
1269            outputs: self.outputs.apply_args(args)?,
1270            mint: self.mint.apply_args(args)?,
1271            fees: self.fees.apply_args(args)?,
1272            adhoc: self.adhoc.apply_args(args)?,
1273            collateral: self.collateral.apply_args(args)?,
1274        };
1275
1276        Ok(tx)
1277    }
1278
1279    fn apply_inputs(self, args: &BTreeMap<String, HashSet<Utxo>>) -> Result<Self, Error> {
1280        Ok(Self {
1281            references: self.references.apply_inputs(args)?,
1282            inputs: self.inputs.apply_inputs(args)?,
1283            outputs: self.outputs.apply_inputs(args)?,
1284            mint: self.mint.apply_inputs(args)?,
1285            fees: self.fees.apply_inputs(args)?,
1286            adhoc: self.adhoc.apply_inputs(args)?,
1287            collateral: self.collateral.apply_inputs(args)?,
1288        })
1289    }
1290
1291    fn apply_fees(self, fees: u64) -> Result<Self, Error> {
1292        Ok(Self {
1293            references: self.references.apply_fees(fees)?,
1294            inputs: self.inputs.apply_fees(fees)?,
1295            outputs: self.outputs.apply_fees(fees)?,
1296            mint: self.mint.apply_fees(fees)?,
1297            fees: self.fees.apply_fees(fees)?,
1298            adhoc: self.adhoc.apply_fees(fees)?,
1299            collateral: self.collateral.apply_fees(fees)?,
1300        })
1301    }
1302
1303    fn is_constant(&self) -> bool {
1304        self.inputs.iter().all(|x| x.is_constant())
1305            && self.outputs.iter().all(|x| x.is_constant())
1306            && self.mint.is_constant()
1307            && self.fees.is_constant()
1308            && self.adhoc.iter().all(|x| x.is_constant())
1309    }
1310
1311    fn params(&self) -> BTreeMap<String, ir::Type> {
1312        // TODO: analyze if necessary to add ref_inputs
1313        let mut params = BTreeMap::new();
1314        params.extend(self.inputs.params());
1315        params.extend(self.outputs.params());
1316        params.extend(self.mint.params());
1317        params.extend(self.fees.params());
1318        params.extend(self.adhoc.params());
1319        params
1320    }
1321
1322    fn queries(&self) -> BTreeMap<String, ir::InputQuery> {
1323        // TODO: analyze if necessary to add ref_inputs
1324        let mut queries = BTreeMap::new();
1325        queries.extend(self.inputs.queries());
1326        queries.extend(self.outputs.queries());
1327        queries.extend(self.mint.queries());
1328        queries.extend(self.fees.queries());
1329        queries.extend(self.adhoc.queries());
1330        queries
1331    }
1332
1333    fn reduce_self(self) -> Result<Self, Error> {
1334        Ok(self)
1335    }
1336
1337    fn reduce_nested(self) -> Result<Self, Error> {
1338        Ok(Self {
1339            references: self.references.reduce()?,
1340            inputs: self.inputs.reduce()?,
1341            outputs: self.outputs.reduce()?,
1342            mint: self.mint.reduce()?,
1343            fees: self.fees.reduce()?,
1344            adhoc: self.adhoc.reduce()?,
1345            collateral: self.collateral.reduce()?,
1346        })
1347    }
1348}
1349
1350pub fn apply_args(template: ir::Tx, args: &BTreeMap<String, ArgValue>) -> Result<ir::Tx, Error> {
1351    template.apply_args(args)
1352}
1353
1354pub fn apply_inputs(
1355    template: ir::Tx,
1356    args: &BTreeMap<String, HashSet<Utxo>>,
1357) -> Result<ir::Tx, Error> {
1358    template.apply_inputs(args)
1359}
1360
1361pub fn apply_fees(template: ir::Tx, fees: u64) -> Result<ir::Tx, Error> {
1362    template.apply_fees(fees)
1363}
1364
1365pub fn reduce(template: ir::Tx) -> Result<ir::Tx, Error> {
1366    template.reduce()
1367}
1368
1369pub fn find_params(template: &ir::Tx) -> BTreeMap<String, ir::Type> {
1370    template.params()
1371}
1372
1373pub fn find_queries(template: &ir::Tx) -> BTreeMap<String, ir::InputQuery> {
1374    template.queries()
1375}
1376
1377#[cfg(test)]
1378mod tests {
1379
1380    use super::*;
1381
1382    const SUBJECT_PROTOCOL: &str = r#"
1383    party Sender;
1384
1385    tx swap(a: Int, b: Int) {
1386        input source {
1387            from: Sender,
1388            min_amount: Ada(a) + Ada(b),
1389        }
1390    }
1391    "#;
1392
1393    #[test]
1394    fn test_apply_args() {
1395        let mut ast = crate::parsing::parse_string(SUBJECT_PROTOCOL).unwrap();
1396        crate::analyzing::analyze(&mut ast).ok().unwrap();
1397
1398        let before = crate::lowering::lower(&ast, "swap").unwrap();
1399
1400        let params = find_params(&before);
1401        assert_eq!(params.len(), 3);
1402        assert_eq!(params.get("sender"), Some(&ir::Type::Address));
1403        assert_eq!(params.get("a"), Some(&ir::Type::Int));
1404        assert_eq!(params.get("b"), Some(&ir::Type::Int));
1405
1406        let args = BTreeMap::from([
1407            ("sender".to_string(), ArgValue::Address(b"abc".to_vec())),
1408            ("a".to_string(), ArgValue::Int(100)),
1409            ("b".to_string(), ArgValue::Int(200)),
1410        ]);
1411
1412        let after = apply_args(before, &args).unwrap();
1413
1414        let params = find_params(&after);
1415        assert_eq!(params.len(), 0);
1416    }
1417
1418    #[test]
1419    fn test_reduce_numeric_binary_op() {
1420        let op = ir::Expression::EvalCustom(
1421            ir::BinaryOp {
1422                op: ir::BinaryOpKind::Add,
1423                left: ir::Expression::Number(1),
1424                right: ir::Expression::EvalCustom(
1425                    ir::BinaryOp {
1426                        op: ir::BinaryOpKind::Sub,
1427                        left: ir::Expression::Number(5),
1428                        right: ir::Expression::Number(3),
1429                    }
1430                    .into(),
1431                ),
1432            }
1433            .into(),
1434        );
1435
1436        let reduced = op.reduce().unwrap();
1437
1438        match reduced {
1439            ir::Expression::Number(3) => (),
1440            _ => panic!("Expected number 3"),
1441        };
1442    }
1443
1444    #[test]
1445    fn test_reduce_single_custom_asset_binary_op() {
1446        let op = ir::Expression::EvalCustom(
1447            ir::BinaryOp {
1448                op: ir::BinaryOpKind::Add,
1449                left: ir::Expression::Assets(vec![ir::AssetExpr {
1450                    policy: ir::Expression::Bytes(b"abc".to_vec()),
1451                    asset_name: ir::Expression::Bytes(b"111".to_vec()),
1452                    amount: ir::Expression::Number(100),
1453                }]),
1454                right: ir::Expression::Assets(vec![ir::AssetExpr {
1455                    policy: ir::Expression::Bytes(b"abc".to_vec()),
1456                    asset_name: ir::Expression::Bytes(b"111".to_vec()),
1457                    amount: ir::Expression::Number(200),
1458                }]),
1459            }
1460            .into(),
1461        );
1462
1463        let reduced = op.reduce().unwrap();
1464
1465        match reduced {
1466            ir::Expression::Assets(assets) => {
1467                assert_eq!(assets.len(), 1);
1468                assert_eq!(assets[0].policy, ir::Expression::Bytes(b"abc".to_vec()));
1469                assert_eq!(assets[0].asset_name, ir::Expression::Bytes(b"111".to_vec()));
1470                assert_eq!(assets[0].amount, ir::Expression::Number(300));
1471            }
1472            _ => panic!("Expected assets"),
1473        };
1474    }
1475
1476    #[test]
1477    fn test_reduce_native_asset_binary_op() {
1478        let op = ir::Expression::EvalCustom(
1479            ir::BinaryOp {
1480                op: ir::BinaryOpKind::Add,
1481                left: ir::Expression::Assets(vec![ir::AssetExpr {
1482                    policy: ir::Expression::None,
1483                    asset_name: ir::Expression::None,
1484                    amount: ir::Expression::Number(100),
1485                }]),
1486                right: ir::Expression::Assets(vec![ir::AssetExpr {
1487                    policy: ir::Expression::None,
1488                    asset_name: ir::Expression::None,
1489                    amount: ir::Expression::Number(200),
1490                }]),
1491            }
1492            .into(),
1493        );
1494
1495        let reduced = op.reduce().unwrap();
1496
1497        match reduced {
1498            ir::Expression::Assets(assets) => {
1499                assert_eq!(assets.len(), 1);
1500                assert_eq!(assets[0].policy, ir::Expression::None);
1501                assert_eq!(assets[0].asset_name, ir::Expression::None);
1502                assert_eq!(assets[0].amount, ir::Expression::Number(300));
1503            }
1504            _ => panic!("Expected assets"),
1505        };
1506    }
1507
1508    #[test]
1509    fn test_reduce_mixed_asset_binary_op() {
1510        let op = ir::Expression::EvalCustom(
1511            ir::BinaryOp {
1512                op: ir::BinaryOpKind::Add,
1513                left: ir::Expression::Assets(vec![ir::AssetExpr {
1514                    policy: ir::Expression::None,
1515                    asset_name: ir::Expression::None,
1516                    amount: ir::Expression::Number(100),
1517                }]),
1518                right: ir::Expression::Assets(vec![ir::AssetExpr {
1519                    policy: ir::Expression::Bytes(b"abc".to_vec()),
1520                    asset_name: ir::Expression::Bytes(b"111".to_vec()),
1521                    amount: ir::Expression::Number(200),
1522                }]),
1523            }
1524            .into(),
1525        );
1526
1527        let reduced = op.reduce().unwrap();
1528
1529        match reduced {
1530            ir::Expression::Assets(assets) => {
1531                assert_eq!(assets.len(), 2);
1532
1533                for asset in assets {
1534                    if asset.policy == ir::Expression::None {
1535                        assert_eq!(asset.asset_name, ir::Expression::None);
1536                        assert_eq!(asset.amount, ir::Expression::Number(100));
1537                    } else {
1538                        assert_eq!(asset.policy, ir::Expression::Bytes(b"abc".to_vec()));
1539                        assert_eq!(asset.asset_name, ir::Expression::Bytes(b"111".to_vec()));
1540                        assert_eq!(asset.amount, ir::Expression::Number(200));
1541                    }
1542                }
1543            }
1544            _ => panic!("Expected assets"),
1545        };
1546    }
1547}